diff --git a/README.md b/README.md index de2eb6e..99f4b1a 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,12 @@ The client is known to work with [OpenVPNĀ®][openvpn] 2.3+ servers. Key renegoti - [x] Handshake and tunneling over UDP or TCP - [x] Ciphers - AES-CBC (128 and 256 bit) - - AES-GCM (128 and 256 bit) + - AES-GCM (128 and 256 bit, 2.4) - [x] HMAC digests - SHA-1 - SHA-256 +- [x] NCP (Negotiable Crypto Parameters, 2.4) + - Server-side - [x] TLS handshake - CA validation - Client certificate diff --git a/TunnelKit/Sources/Core/CoreConfiguration.swift b/TunnelKit/Sources/Core/CoreConfiguration.swift index f0fb723..fa0a953 100644 --- a/TunnelKit/Sources/Core/CoreConfiguration.swift +++ b/TunnelKit/Sources/Core/CoreConfiguration.swift @@ -59,8 +59,9 @@ struct CoreConfiguration { // MARK: Authentication static let peerInfo = [ - "IV_VER=2.3.99", + "IV_VER=2.4", "IV_PROTO=2", + "IV_NCP=2", "" ].joined(separator: "\n") diff --git a/TunnelKit/Sources/Core/DataPath.h b/TunnelKit/Sources/Core/DataPath.h index 9a7cf9a..037e130 100644 --- a/TunnelKit/Sources/Core/DataPath.h +++ b/TunnelKit/Sources/Core/DataPath.h @@ -47,12 +47,11 @@ - (nonnull instancetype)initWithEncrypter:(nonnull id)encrypter decrypter:(nonnull id)decrypter + peerId:(uint32_t)peerId // 24-bit, discard most significant byte + compressionFraming:(CompressionFramingNative)compressionFraming maxPackets:(NSInteger)maxPackets usesReplayProtection:(BOOL)usesReplayProtection; -- (void)setPeerId:(uint32_t)peerId; // 24-bit, discard most significant byte -- (void)setCompressionFraming:(CompressionFramingNative)compressionFraming; - - (NSArray *)encryptPackets:(nonnull NSArray *)packets key:(uint8_t)key error:(NSError **)error; - (NSArray *)decryptPackets:(nonnull NSArray *)packets keepAlive:(nullable bool *)keepAlive error:(NSError **)error; diff --git a/TunnelKit/Sources/Core/DataPath.m b/TunnelKit/Sources/Core/DataPath.m index 137a712..7ba3661 100644 --- a/TunnelKit/Sources/Core/DataPath.m +++ b/TunnelKit/Sources/Core/DataPath.m @@ -80,7 +80,7 @@ return (uint8_t *)addr; } -- (instancetype)initWithEncrypter:(id)encrypter decrypter:(id)decrypter maxPackets:(NSInteger)maxPackets usesReplayProtection:(BOOL)usesReplayProtection +- (instancetype)initWithEncrypter:(id)encrypter decrypter:(id)decrypter peerId:(uint32_t)peerId compressionFraming:(CompressionFramingNative)compressionFraming maxPackets:(NSInteger)maxPackets usesReplayProtection:(BOOL)usesReplayProtection { NSParameterAssert(encrypter); NSParameterAssert(decrypter); @@ -103,7 +103,9 @@ self.inReplay = [[ReplayProtector alloc] init]; } - self.compressionFraming = CompressionFramingNativeDisabled; + [self.encrypter setPeerId:peerId]; + [self.decrypter setPeerId:peerId]; + [self setCompressionFraming:compressionFraming]; } return self; } @@ -150,15 +152,6 @@ return [[self class] alignedPointer:self.decBuffer]; } -- (void)setPeerId:(uint32_t)peerId -{ - NSAssert(self.encrypter, @"Setting peer-id to nil encrypter"); - NSAssert(self.decrypter, @"Setting peer-id to nil decrypter"); - - [self.encrypter setPeerId:peerId]; - [self.decrypter setPeerId:peerId]; -} - - (void)setCompressionFraming:(CompressionFramingNative)compressionFraming { switch (compressionFraming) { diff --git a/TunnelKit/Sources/Core/SessionProxy+PushReply.swift b/TunnelKit/Sources/Core/SessionProxy+PushReply.swift index c7b28c8..2383844 100644 --- a/TunnelKit/Sources/Core/SessionProxy+PushReply.swift +++ b/TunnelKit/Sources/Core/SessionProxy+PushReply.swift @@ -146,6 +146,15 @@ public protocol SessionReply { /// The DNS servers set up for this session. var dnsServers: [String] { get } + + /// The optional authentication token. + var authToken: String? { get } + + /// The optional 24-bit peer-id. + var peerId: UInt32? { get } + + /// The negotiated cipher if any (NCP). + var cipher: SessionProxy.Cipher? { get } } extension SessionProxy { @@ -179,6 +188,8 @@ extension SessionProxy { private static let peerIdRegexp = try! NSRegularExpression(pattern: "peer-id [0-9]+", options: []) + private static let cipherRegexp = try! NSRegularExpression(pattern: "cipher [^\\s]+", options: []) + let ipv4: IPv4Settings? let ipv6: IPv6Settings? @@ -189,6 +200,8 @@ extension SessionProxy { let peerId: UInt32? + let cipher: SessionProxy.Cipher? + init?(message: String) throws { guard message.hasPrefix("PUSH_REPLY") else { return nil @@ -207,6 +220,7 @@ extension SessionProxy { var dnsServers: [String] = [] var authToken: String? var peerId: UInt32? + var cipher: SessionProxy.Cipher? // MARK: Routing (IPv4) @@ -354,10 +368,17 @@ extension SessionProxy { PushReply.peerIdRegexp.enumerateArguments(in: message) { peerId = UInt32($0[0]) } + + // MARK: NCP + + PushReply.cipherRegexp.enumerateArguments(in: message) { + cipher = SessionProxy.Cipher(rawValue: $0[0].uppercased()) + } self.dnsServers = dnsServers self.authToken = authToken self.peerId = peerId + self.cipher = cipher } } } diff --git a/TunnelKit/Sources/Core/SessionProxy+SessionKey.swift b/TunnelKit/Sources/Core/SessionProxy+SessionKey.swift index e6720bc..00a57c1 100644 --- a/TunnelKit/Sources/Core/SessionProxy+SessionKey.swift +++ b/TunnelKit/Sources/Core/SessionProxy+SessionKey.swift @@ -74,8 +74,6 @@ extension SessionProxy { private var isTLSConnected: Bool - private var canHandlePackets: Bool - init(id: UInt8) { self.id = id @@ -83,7 +81,6 @@ extension SessionProxy { state = .invalid softReset = false isTLSConnected = false - canHandlePackets = false } // Ruby: Key.hard_reset_timeout @@ -109,21 +106,11 @@ extension SessionProxy { return isTLSConnected } - func startHandlingPackets(withPeerId peerId: UInt32? = nil, compressionFraming: CompressionFraming = .disabled) { - dataPath?.setPeerId(peerId ?? PacketPeerIdDisabled) - dataPath?.setCompressionFraming(compressionFraming.native) - canHandlePackets = true - } - func encrypt(packets: [Data]) throws -> [Data]? { guard let dataPath = dataPath else { log.warning("Data: Set dataPath first") return nil } - guard canHandlePackets else { - log.warning("Data: Invoke startHandlingPackets() before encrypting") - return nil - } return try dataPath.encryptPackets(packets, key: id) } @@ -132,10 +119,6 @@ extension SessionProxy { log.warning("Data: Set dataPath first") return nil } - guard canHandlePackets else { - log.warning("Data: Invoke startHandlingPackets() before decrypting") - return nil - } var keepAlive = false let decrypted = try dataPath.decryptPackets(packets, keepAlive: &keepAlive) if keepAlive { diff --git a/TunnelKit/Sources/Core/SessionProxy.swift b/TunnelKit/Sources/Core/SessionProxy.swift index 7191a48..b84c86d 100644 --- a/TunnelKit/Sources/Core/SessionProxy.swift +++ b/TunnelKit/Sources/Core/SessionProxy.swift @@ -123,9 +123,7 @@ public class SessionProxy { private var remoteSessionId: Data? - private var authToken: String? - - private var peerId: UInt32? + private var pushReply: SessionReply? private var nextPushRequestDate: Date? @@ -229,7 +227,7 @@ public class SessionProxy { - Returns: `true` if supports link rebinding. */ public func canRebindLink() -> Bool { - return (peerId != nil) + return (pushReply?.peerId != nil) } /** @@ -241,7 +239,7 @@ public class SessionProxy { - Seealso: `canRebindLink()`. */ public func rebindLink(_ link: LinkInterface) { - guard let _ = peerId else { + guard let _ = pushReply?.peerId else { log.warning("Session doesn't support link rebinding!") return } @@ -316,11 +314,10 @@ public class SessionProxy { sessionId = nil remoteSessionId = nil - authToken = nil nextPushRequestDate = nil connectedDate = nil authenticator = nil - peerId = nil + pushReply = nil link = nil if !(tunnel?.isPersistent ?? false) { tunnel = nil @@ -614,7 +611,6 @@ public class SessionProxy { controlPacketIdOut = 0 controlPacketIdIn = 0 authenticator = nil - peerId = nil bytesIn = 0 bytesOut = 0 } @@ -624,6 +620,7 @@ public class SessionProxy { log.debug("Send hard reset") resetControlChannel() + pushReply = nil do { try sessionId = SecureRandom.data(length: ProtocolMacros.sessionIdLength) } catch let e { @@ -662,7 +659,7 @@ public class SessionProxy { negotiationKey.controlState = .preAuth do { - authenticator = try Authenticator(configuration.username, authToken ?? configuration.password) + authenticator = try Authenticator(configuration.username, pushReply?.authToken ?? configuration.password) try authenticator?.putAuth(into: negotiationKey.tls) } catch let e { deferStop(.shutdown, e) @@ -701,11 +698,7 @@ public class SessionProxy { enqueueControlPackets(code: .controlV1, key: negotiationKey.id, payload: cipherTextOut) if negotiationKey.softReset { - authenticator = nil - negotiationKey.startHandlingPackets(withPeerId: peerId) - negotiationKey.controlState = .connected - connectedDate = Date() - transitionKeys() + completeConnection() } nextPushRequestDate = Date().addingTimeInterval(CoreConfiguration.retransmissionLimit) } @@ -725,6 +718,14 @@ public class SessionProxy { } } + private func completeConnection() { + setupEncryption() + authenticator = nil + negotiationKey.controlState = .connected + connectedDate = Date() + transitionKeys() + } + // MARK: Control // Ruby: handle_ctrl_pkt @@ -848,8 +849,6 @@ public class SessionProxy { return } - setupKeys() - negotiationKey.controlState = .preIfConfig nextPushRequestDate = Date().addingTimeInterval(negotiationKey.softReset ? CoreConfiguration.softResetDelay : CoreConfiguration.retransmissionLimit) pushRequest() @@ -884,21 +883,13 @@ public class SessionProxy { return } reply = optionalReply - authToken = reply.authToken - peerId = reply.peerId } catch let e { deferStop(.shutdown, e) return } - authenticator = nil - negotiationKey.startHandlingPackets( - withPeerId: peerId, - compressionFraming: configuration.compressionFraming - ) - negotiationKey.controlState = .connected - connectedDate = Date() - transitionKeys() + pushReply = reply + completeConnection() guard let remoteAddress = link?.remoteAddress else { fatalError("Could not resolve link remote address") @@ -1007,22 +998,25 @@ public class SessionProxy { } // Ruby: setup_keys - private func setupKeys() { + private func setupEncryption() { guard let auth = authenticator else { - fatalError("Setting up keys without having authenticated") + fatalError("Setting up encryption without having authenticated") } guard let sessionId = sessionId else { - fatalError("Setting up keys without a local sessionId") + fatalError("Setting up encryption without a local sessionId") } guard let remoteSessionId = remoteSessionId else { - fatalError("Setting up keys without a remote sessionId") + fatalError("Setting up encryption without a remote sessionId") } guard let serverRandom1 = auth.serverRandom1, let serverRandom2 = auth.serverRandom2 else { - fatalError("Setting up keys without server randoms") + fatalError("Setting up encryption without server randoms") } - + guard let pushReply = pushReply else { + fatalError("Setting up encryption without a former PUSH_REPLY") + } + if CoreConfiguration.logsSensitiveData { - log.debug("Setup keys from the following components:") + log.debug("Set up encryption from the following components:") log.debug("\tpreMaster: \(auth.preMaster.toHex())") log.debug("\trandom1: \(auth.random1.toHex())") log.debug("\trandom2: \(auth.random2.toHex())") @@ -1031,13 +1025,18 @@ public class SessionProxy { log.debug("\tsessionId: \(sessionId.toHex())") log.debug("\tremoteSessionId: \(remoteSessionId.toHex())") } else { - log.debug("Setup keys") + log.debug("Set up encryption") + } + + let pushedCipher = pushReply.cipher + if let negCipher = pushedCipher { + log.debug("Negotiated cipher: \(negCipher.rawValue)") } let bridge: EncryptionBridge do { bridge = try EncryptionBridge( - configuration.cipher, + pushedCipher ?? configuration.cipher, configuration.digest, auth, sessionId, @@ -1051,6 +1050,8 @@ public class SessionProxy { negotiationKey.dataPath = DataPath( encrypter: bridge.encrypter(), decrypter: bridge.decrypter(), + peerId: pushReply.peerId ?? PacketPeerIdDisabled, + compressionFraming: configuration.compressionFraming.native, maxPackets: link?.packetBufferSize ?? 200, usesReplayProtection: CoreConfiguration.usesReplayProtection ) diff --git a/TunnelKitTests/DataPathEncryptionTests.swift b/TunnelKitTests/DataPathEncryptionTests.swift index 5293650..193ddea 100644 --- a/TunnelKitTests/DataPathEncryptionTests.swift +++ b/TunnelKitTests/DataPathEncryptionTests.swift @@ -87,8 +87,14 @@ class DataPathEncryptionTests: XCTestCase { } func privateTestDataPathHigh(peerId: UInt32?) { - let path = DataPath(encrypter: enc, decrypter: dec, maxPackets: 1000, usesReplayProtection: false) - path.setCompressionFraming(.disabled) + let path = DataPath( + encrypter: enc, + decrypter: dec, + peerId: peerId ?? PacketPeerIdDisabled, + compressionFraming: .disabled, + maxPackets: 1000, + usesReplayProtection: false + ) if let peerId = peerId { enc.setPeerId(peerId) diff --git a/TunnelKitTests/DataPathPerformanceTests.swift b/TunnelKitTests/DataPathPerformanceTests.swift index 7ec9ccd..9da9b32 100644 --- a/TunnelKitTests/DataPathPerformanceTests.swift +++ b/TunnelKitTests/DataPathPerformanceTests.swift @@ -54,7 +54,14 @@ class DataPathPerformanceTests: XCTestCase { encrypter = crypto.encrypter() decrypter = crypto.decrypter() - dataPath = DataPath(encrypter: encrypter, decrypter: decrypter, maxPackets: 200, usesReplayProtection: false) + dataPath = DataPath( + encrypter: encrypter, + decrypter: decrypter, + peerId: PacketPeerIdDisabled, + compressionFraming: .disabled, + maxPackets: 200, + usesReplayProtection: false + ) } override func tearDown() { diff --git a/TunnelKitTests/PushTests.swift b/TunnelKitTests/PushTests.swift index b30d1ad..1b7820e 100644 --- a/TunnelKitTests/PushTests.swift +++ b/TunnelKitTests/PushTests.swift @@ -91,4 +91,12 @@ class PushTests: XCTestCase { XCTAssertEqual(reply.ipv6?.defaultGateway, "fe80::601:30ff:feb7:dc02") XCTAssertEqual(reply.dnsServers, ["2001:4860:4860::8888", "2001:4860:4860::8844"]) } + + func testNCP() { + let msg = "PUSH_REPLY,dhcp-option DNS 8.8.8.8,dhcp-option DNS 4.4.4.4,comp-lzo no,route 10.8.0.1,topology net30,ping 10,ping-restart 120,ifconfig 10.8.0.6 10.8.0.5,peer-id 0,cipher AES-256-CBC" + let reply = try! SessionProxy.PushReply(message: msg)! + reply.debug() + + XCTAssertEqual(reply.cipher, .aes256cbc) + } }