diff --git a/Sources/ReplicantSwift/Constants.swift b/Sources/ReplicantSwift/Constants.swift deleted file mode 100644 index 555ff7c..0000000 --- a/Sources/ReplicantSwift/Constants.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Constants.swift -// ReplicantSwift -// -// Created by Adelita Schule on 11/9/18. -// - -import Foundation - -let chunkSize = 4096 -let aesOverhead = 81 -let bufferSize = chunkSize - aesOverhead -let keySize = 64 -let keyDataSize = keySize + 1 -let cryptoHandshakeSize = chunkSize -let cryptoHandshakePaddingSize = cryptoHandshakeSize - keySize -let responseSize = chunkSize diff --git a/Sources/ReplicantSwift/CryptoHandshake.swift b/Sources/ReplicantSwift/CryptoHandshake.swift deleted file mode 100644 index b258b24..0000000 --- a/Sources/ReplicantSwift/CryptoHandshake.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// CryptoHandshake.swift -// ReplicantSwift -// -// Created by Adelita Schule on 11/9/18. -// - -import Foundation -import Datable -import CommonCrypto - -/** - The client will need to know the server’s public key as part of the configuration. The server will not know the client’s public key, so the first thing the client will need to send is it’s public key. After that, it will just send encrypted data. Please note that the client public key when exported to a Data from CommonCrypto will be 65 bytes, where the first byte is always 4. This should be stripped off to remove the redundant 4 and just the remaining 64 bytes should be sent. The key size should then be padded to be the size of one chunk. - The server will then send a response which is the size of one chunk, and which contains random bytes that should be discarded. -*/ -class CryptoHandshake: NSObject -{ - let encryptor = Encryption() - - /// The client will need to know the server’s public key as part of the configuration. - var serverPublicKey: Data - - /// The client public key when exported to a Data from CommonCrypto will be 65 bytes, where the first byte is always 4. This should be stripped off to remove the redundant 4 and just the remaining 64 bytes should be sent. The key size should then be padded to be the size of one chunk. - var clientPublicKey: Data - - init?(withKeyData clientKeyData: Data, andServerKeyData serverKeyData: Data) - { - guard let allDressedUp = encryptor.cleanAndPadKey(keyData: clientKeyData) - else - { - return nil - } - - clientPublicKey = allDressedUp - serverPublicKey = serverKeyData - } - - init?(withKey clientKey: SecKey, andServerKeyData serverKeyData: Data) - { - var error: Unmanaged? - - // Encode public key as data - guard let clientPublicData = SecKeyCopyExternalRepresentation(clientKey, &error) as Data? - else - { - print("\nUnable to generate public key external representation: \(error!.takeRetainedValue() as Error)\n") - return nil - } - - //FIXME: Padding - guard let cleanClientPublicData = encryptor.cleanAndPadKey(keyData: clientPublicData) - else - { - return nil - } - - clientPublicKey = cleanClientPublicData - serverPublicKey = serverKeyData - } - - -} - - diff --git a/Sources/ReplicantSwift/Encryption.swift b/Sources/ReplicantSwift/Encryption.swift index cab6e32..fa18440 100644 --- a/Sources/ReplicantSwift/Encryption.swift +++ b/Sources/ReplicantSwift/Encryption.swift @@ -9,37 +9,13 @@ import Foundation import Security import CommonCrypto +let keySize = 64 +let keyDataSize = keySize + 1 +let aesOverheadSize = 81 + class Encryption: NSObject { - let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorX963SHA256AESGCM - //var privateKey: SecKey - -// public init?(withPrivateKey initKey: Data?) -// { -// if let providedKey = initKey -// { -// guard let secKey = Encryption.decodeKey(fromData: providedKey) -// else -// { -// print("\nFailed to initialize Replicant: Unable to create SecKey from key data provided.") -// return nil -// } -// -// privateKey = secKey -// } -// else -// { -// guard let newKey = Encryption.generatePrivateKey() -// else -// { -// return nil -// } -// -// privateKey = newKey -// } -// -// } - + let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM func generatePrivateKey() -> SecKey? { @@ -78,12 +54,10 @@ class Encryption: NSObject /** Generate a public key from the provided private key and encodes it as data. - - Returns: optional, encoded key as data + - Returns: optional, encoded key as SecKey */ - func generatePublicKey(usingPrivateKey privateKey: SecKey) -> Data? + func generatePublicKey(usingPrivateKey privateKey: SecKey) -> SecKey? { - var error: Unmanaged? - guard let alicePublic = SecKeyCopyPublicKey(privateKey) else { @@ -91,23 +65,64 @@ class Encryption: NSObject return nil } - // Encode public key as data - guard let alicePublicData = SecKeyCopyExternalRepresentation(alicePublic, &error) as Data? + return alicePublic + } + + public func generateKeyPair() -> (privateKey: SecKey, publicKey: SecKey)? + { + guard let privateKey = generatePrivateKey() + else + { + return nil + } + + guard let publicKey = generatePublicKey(usingPrivateKey: privateKey) + else + { + return nil + } + + return (privateKey, publicKey) + } + + /// This is the format needed to send the key to the server. + public func generateAndEncryptPaddedKeyData(fromKey key: SecKey, withChunkSize chunkSize: Int, usingServerKey serverKey: SecKey) -> Data? + { + var error: Unmanaged? + var newKeyData: Data + + // Encode key as data + guard let keyData = SecKeyCopyExternalRepresentation(key, &error) as Data? else { print("\nUnable to generate public key external representation: \(error!.takeRetainedValue() as Error)\n") return nil } - return alicePublicData + newKeyData = keyData + + // Add padding if needed + if let padding = getKeyPadding(chunkSize: chunkSize) + { + newKeyData = keyData + padding + } + + // Encrypt the key + guard let encryptedKeyData = encrypt(payload: newKeyData, usingServerKey: serverKey) + else + { + return nil + } + + return encryptedKeyData } - /// Decode data to get public key + /// Decode data to get public key. This only decodes key data that is NOT padded. static func decodeKey(fromData publicKeyData: Data) -> SecKey? { var error: Unmanaged? - let options: [String: Any] = [kSecAttrKeyType as String: kSecAttrKeyTypeEC, + let options: [String: Any] = [kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClass as String: kSecAttrKeyClassPublic, kSecAttrKeySizeInBits as String: 256] @@ -153,67 +168,18 @@ class Encryption: NSObject return decryptedText } - func cleanKeyData(keyData: Data) -> Data? + func getKeyPadding(chunkSize: Int) -> Data? { - if keyData.count == keyDataSize - { - if keyData.first! == 4 - { - // Strip the redundant 4 from the key data - let cleanKey = keyData.dropFirst() - return cleanKey - } - else - { - print("\nFailed to clean key: Data was 65 bytes but the first byte was not 4.\n") - return nil - } - } - else if keyData.count == keySize - { - print("\nReturning unchanged key data, the byte count was already 64.\n") - return keyData - } - else + let paddingSize = chunkSize - (keySize + aesOverheadSize) + if paddingSize > 0 { - print("Failed to clean key data: unexpected byte count of \(keyData.count)") - return nil - } - } - - func getKeyPadding() -> Data? - { - var bytes = [UInt8](repeating: 0, count: chunkSize - keySize) - let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) - - if status == errSecSuccess - { - // Always test the status. - print(bytes) - // Prints something different every time you run. + let bytes = [UInt8](repeating: 0, count: paddingSize) return Data(array: bytes) } else { - print("\nFailed to gnerate padding: \(status)\n") return nil } } - - func cleanAndPadKey(keyData: Data) -> Data? - { - guard let cleanKey = cleanKeyData(keyData: keyData) - else - { - return nil - } - - guard let padding = getKeyPadding() - else - { - return nil - } - - return cleanKey + padding - } } + diff --git a/Sources/ReplicantSwift/Models/SequenceModel.swift b/Sources/ReplicantSwift/Models/SequenceModel.swift new file mode 100644 index 0000000..7eea73d --- /dev/null +++ b/Sources/ReplicantSwift/Models/SequenceModel.swift @@ -0,0 +1,30 @@ +// +// SequenceModel.swift +// ReplicantSwift +// +// Created by Adelita Schule on 11/15/18. +// + +import Foundation + +public struct SequenceModel +{ + /// Byte Sequence. + var sequence: Data + + /// Target sequence Length. + var length: UInt + + init?(sequence: Data, length: UInt) + { + ///FIXME: Is this still correct? Length must be no larger than 1440 bytes + if length == 0 || length > 65535 + { + print("\nSequenceModel initialization failed: target length was either 0 or larger than 65535\n") + return nil + } + + self.sequence = sequence + self.length = length + } +} diff --git a/Sources/ReplicantSwift/ReplicantConfig.swift b/Sources/ReplicantSwift/ReplicantConfig.swift new file mode 100644 index 0000000..6800f03 --- /dev/null +++ b/Sources/ReplicantSwift/ReplicantConfig.swift @@ -0,0 +1,33 @@ +// +// ReplicantConfig.swift +// ReplicantSwift +// +// Created by Adelita Schule on 11/14/18. +// + +import Foundation + +public struct ReplicantConfig +{ + var serverPublicKey: SecKey + var chunkSize: Int + var chunkTimeout: Int + var addSequences: [SequenceModel]? + var removeSequences: [SequenceModel]? + + + public init?(serverPublicKey: SecKey, chunkSize: Int, chunkTimeout: Int, addSequences: [SequenceModel]?, removeSequences: [SequenceModel]?) + { + guard chunkSize >= keySize + aesOverheadSize + else + { + print("\nUnable to initialize ReplicantConfig: chunkSize (\(chunkSize)) cannot be smaller than keySize + aesOverheadSize (\(keySize + aesOverheadSize))\n") + return nil + } + self.serverPublicKey = serverPublicKey + self.chunkSize = chunkSize + self.chunkTimeout = chunkTimeout + self.addSequences = addSequences + self.removeSequences = removeSequences + } +} diff --git a/Sources/ReplicantSwift/ReplicantSwift.swift b/Sources/ReplicantSwift/ReplicantSwift.swift index 0ba0d2d..e2a2223 100644 --- a/Sources/ReplicantSwift/ReplicantSwift.swift +++ b/Sources/ReplicantSwift/ReplicantSwift.swift @@ -2,13 +2,41 @@ import Foundation import Security import CommonCrypto -/** - * [Using Keys For Encryption](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/using_keys_for_encryption), - * [Storing Keys in the Secure Enclave](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_secure_enclave), - * [seckeyalgorithm](https://developer.apple.com/documentation/security/seckeyalgorithm/2091905-eciesencryptioncofactorx963sha25) - */ public struct Replicant { - + let encryptor = Encryption() + + var config: ReplicantConfig + var serverPublicKey: SecKey + var clientPublicKey: SecKey + var clientPrivateKey: SecKey + + var toneBurst: ToneBurst? + public init?(withConfig config: ReplicantConfig) + { + guard let keyPair = encryptor.generateKeyPair() + else + { + return nil + } + + if let addSequences = config.addSequences, let removeSequences = config.removeSequences + { + self.toneBurst = ToneBurst(addSequences: addSequences, removeSequences: removeSequences) + } + + self.config = config + self.serverPublicKey = config.serverPublicKey + self.clientPublicKey = keyPair.publicKey + self.clientPrivateKey = keyPair.privateKey + } +} + +extension Data +{ + public var bytes: Array + { + return Array(self) + } } diff --git a/Sources/ReplicantSwift/ToneBurst.swift b/Sources/ReplicantSwift/ToneBurst.swift index 004f287..e96b4ca 100644 --- a/Sources/ReplicantSwift/ToneBurst.swift +++ b/Sources/ReplicantSwift/ToneBurst.swift @@ -17,100 +17,22 @@ public class ToneBurst: NSObject /// Sequences that should be removed from the incoming packet stream. var removeSequences: [SequenceModel] - /// Index of the first packet to be injected into the stream. - var firstIndex: UInt - - /// Index of the last packet to be injected into the stream. - var lastIndex: UInt - - /// Current Index into the output stream. - /// This starts at zero and is incremented every time a packet is output. - /// The OutputIndex is compared to the SequenceModel Index. - /// When they are equal, a byte Sequence packet is injected into the output. - var outputIndex: UInt + var addIndex = 0 + var removeIndex = 0 + var receiveBuffer = Data() public init?(addSequences: [SequenceModel], removeSequences: [SequenceModel]) { - guard let firstSequence = addSequences.first - else - { - return nil - } - - guard let lastSequence = addSequences.last - else - { - return nil - } - - // Check to make sure that add sequences do not have the same indices, do the same for remove sequences - let addSeqGroupedByIndex = Dictionary(grouping: addSequences) { $0.index } - for index in addSeqGroupedByIndex - { - if index.value.count > 1 - { - return nil - } - } - - let removeSeqGroupedByIndex = Dictionary(grouping: removeSequences) { $0.index } - for index in removeSeqGroupedByIndex - { - if index.value.count > 1 - { - return nil - } - } - self.addSequences = addSequences self.removeSequences = removeSequences - self.firstIndex = firstSequence.index - self.lastIndex = lastSequence.index - self.outputIndex = 0 } - /// Inject packets - func inject(results: [Data]) -> [Data] - { - var nextPacket = addSequences.first(where: {(sequence) in sequence.index == outputIndex}) - var newResults = results - - while nextPacket != nil - { - guard let newPacket = makePacket(model: nextPacket!) - else - { - break - } - - newResults.append(newPacket) - outputIndex += 1 - nextPacket = addSequences.first(where: {(sequence) in sequence.index == outputIndex}) - } - - return newResults - } /// With a Sequence model, generate a packet to inject into the stream. func makePacket(model: SequenceModel) -> Data? { var result = Data() -// // Add the bytes before the Sequence. -// if model.offset > 0 -// { -// var randomBytes = [UInt8](repeating: 0, count: Int(model.offset)) -// let status = SecRandomCopyBytes(kSecRandomDefault, Int(model.offset), &randomBytes) -// if status == errSecSuccess -// { -// result.append(Data(bytes: randomBytes)) -// } -// else -// { -// return nil -// } -// } - // Add the Sequence result.append(model.sequence) @@ -134,108 +56,102 @@ public class ToneBurst: NSObject return result } - /// For a byte Sequence, see if there is a matching Sequence to remove. - func findMatchingPacket(sequence: Data) -> Bool + func findRemoveSequenceInBuffer() -> MatchState { - for index in 0 ..< removeSequences.count - { - let sequenceModel = removeSequences[index] -// let index1 = Int(sequenceModel.offset) -// let index2 = index1 + sequenceModel.sequence.count - - if sequence.count >= sequenceModel.sequence.count - { - //let source = Data(sequence[index1 ..< index2]) - - //if source.bytes == sequenceModel.sequence.bytes - if sequence == sequenceModel.sequence - { - //Remove matched packet so that it's not matched again - removeSequences.remove(at: index) - - return true - } - } - } + let sequenceModel = removeSequences[removeIndex] + let sequence = sequenceModel.sequence - return false - } - - /// Inject header. - public func transform(buffer: Data) -> [Data] - { - var results: [Data] = [] - - // Check if the current Index into the packet stream is within the range where a packet injection could possibly occur. - if outputIndex <= lastIndex + if receiveBuffer.count >= sequenceModel.length { - // Injection has not finished, but may not have started yet. - if Int(outputIndex) >= Int(firstIndex) - 1 + let source = Data(receiveBuffer[0 ..< sequence.count]) + if source.bytes == sequence.bytes { - // Injection has started and has not finished, so check to see if it is time to inject a packet. - // Inject fake packets before the real packet - results = inject(results: results) - - // Inject the real packet - results.append(buffer) - outputIndex += 1 - - //Inject fake packets after the real packet - results = inject(results: results) + removeIndex += 1 + receiveBuffer = Data(receiveBuffer[sequenceModel.length...]) + return .success } else { - // Injection has not started yet. Keep track of the Index. - results = [buffer] - outputIndex += 1 + return .failure } - - return results } else { - // Injection has finished and will not occur again. Take the fast path and just return the buffer. - return [buffer] + return .insufficientData } } - /// Remove injected packets. - public func restore(buffer: Data) -> [Data] + func generate() -> SendState { - if findMatchingPacket(sequence: buffer) + guard addIndex < addSequences.count + else + { + return .failure + } + + guard let newPacket = makePacket(model: addSequences[addIndex]) + else { - return [] + return .failure + } + + addIndex += 1 + + if addIndex == addSequences.count + { + return .completion(newPacket) } else { - return [buffer] + return .generating(newPacket) } } -} - -public struct SequenceModel -{ - /// Index of the packet into the stream. - var index: UInt - - /// Byte Sequence. - var sequence: Data - - /// Target sequence Length. - var length: UInt - - init?(index: UInt, offset: UInt, sequence: Data, length: UInt) + public func remove(newData: Data) -> ReceiveState { - ///Length must be no larger than 1440 bytes - if length == 0 || length > 1440 + receiveBuffer.append(newData) + + switch findRemoveSequenceInBuffer() { - print("\nByteSequenceShaper initialization failed: target length was either 0 or larger than 1440\n") - return nil + case .success: + if removeIndex == removeSequences.count + { + return .completion(receiveBuffer) + } + else if removeIndex < removeSequences.count + { + return .waiting + } + else + { + return .failure + } + case .insufficientData: + return .waiting + case .failure: + return .failure } - - self.index = index - self.sequence = sequence - self.length = length } + +} + +public enum ReceiveState +{ + case waiting + case completion(Data) + case failure +} + +public enum SendState +{ + case generating(Data) + case completion(Data) + case failure +} + +enum MatchState +{ + case insufficientData + case success + case failure } diff --git a/Tests/ReplicantSwiftTests/ReplicantSwiftTests.swift b/Tests/ReplicantSwiftTests/ReplicantSwiftTests.swift index b0cdfad..36dc1c3 100644 --- a/Tests/ReplicantSwiftTests/ReplicantSwiftTests.swift +++ b/Tests/ReplicantSwiftTests/ReplicantSwiftTests.swift @@ -1,4 +1,6 @@ import XCTest +import Foundation + @testable import ReplicantSwift final class ReplicantSwiftTests: XCTestCase @@ -13,12 +15,6 @@ final class ReplicantSwiftTests: XCTestCase // MARK: Encryption Tests -// func testInitEncryptionNoKey() -// { -// let maybeEncryptor = Encryption(withPrivateKey: nil) -// XCTAssertNotNil(maybeEncryptor) -// } - func testGeneratePrivateUsingPublic() { guard let privateKey = encryptor.generatePrivateKey() @@ -41,7 +37,7 @@ final class ReplicantSwiftTests: XCTestCase return } - guard let alicePuplicKeyData = encryptor.generatePublicKey(usingPrivateKey: privateKey) + guard let alicePuplicKey = encryptor.generatePublicKey(usingPrivateKey: privateKey) else { print("\nUnable to generate publicKeyData from private key.\n") @@ -49,8 +45,50 @@ final class ReplicantSwiftTests: XCTestCase return } - let alicePublicKey = Encryption.decodeKey(fromData: alicePuplicKeyData) - XCTAssertNotNil(alicePublicKey) + var error: Unmanaged? + + // Encode public key as data + guard let keyData = SecKeyCopyExternalRepresentation(alicePuplicKey, &error) as Data? + else + { + XCTFail() + return + } + + guard let decodedKey = Encryption.decodeKey(fromData: keyData) + else + { + XCTFail() + return + } + + XCTAssertTrue(compare(secKey1: decodedKey, secKey2: alicePuplicKey)) + } + + func compare(secKey1: SecKey, secKey2: SecKey) -> Bool + { + var error: Unmanaged? + + guard let secKey1Data = SecKeyCopyExternalRepresentation(secKey1, &error) as Data? + else + { + return false + } + + guard let secKey2Data = SecKeyCopyExternalRepresentation(secKey2, &error) as Data? + else + { + return false + } + + if secKey1Data == secKey2Data + { + return true + } + else + { + return false + } } func testEncryptData() @@ -112,46 +150,89 @@ final class ReplicantSwiftTests: XCTestCase XCTAssertEqual(maybeDecoded, plainText) } - // MARK: CryptoHandshake - func testHandshakeInit() + // MARK: ToneBurst + + let sequence1 = Data(string: "OH HELLO") + let sequence2 = Data(string: "You say hello, and I say goodbye.") + let sequence3 = Data(string: "I don't know why you say 'Goodbye', I say 'Hello'.") + + func testToneBurstInit() { - var error: Unmanaged? - - // Generate private server key - guard let bobPrivate = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else + let sequence = SequenceModel(sequence: sequence1, length: 256)! + let toneBurst = ToneBurst(addSequences: [sequence], removeSequences: [sequence]) + XCTAssertNotNil(toneBurst) + } + + func testFindMatchingPacket() + { + let sequence = SequenceModel(sequence: sequence1, length: 256)! + guard let toneBurst = ToneBurst(addSequences: [sequence], removeSequences: [sequence]) + else { - print(error!) XCTFail() return } - // Generate public server key - let bobPublic = SecKeyCopyPublicKey(bobPrivate)! - - // Encode public key as data - guard let bobPublicData = SecKeyCopyExternalRepresentation(bobPublic, &error) as Data? else + let sendState = toneBurst.generate() + let matchState = toneBurst.findRemoveSequenceInBuffer() + XCTAssertTrue(matchState == .success) + switch matchState { - print("\n\(error!.takeRetainedValue())\n") + case .success: + print("\nRemove sequence found!\n") + case .failure: + print("\nFailed to find remove sequence.\n") + XCTFail() + case .insufficientData: + print("\nBuffer was smaller than the remove sequence we were looking for.\n") XCTFail() - return } - - // Generate private key - guard let alicePrivate = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else + } + + func testOneSequence() + { + let sequence = SequenceModel(sequence: sequence1, length: 256)! + guard let toneBurst = ToneBurst(addSequences: [sequence], removeSequences: [sequence]) + else { - print(error!) XCTFail() return } - // Generate public key - let alicePublic = SecKeyCopyPublicKey(alicePrivate)! - - let cryptoHandshake = CryptoHandshake(withKey: alicePublic, andServerKeyData: bobPublicData) + let transformState = toneBurst.generate() - XCTAssertNotNil(cryptoHandshake) + switch transformState + { + case .failure: + XCTFail() + return + + case .generating(let transformResult): + let restoreState = toneBurst.remove(newData: transformResult) + print("\nBuffer to transform: \n \(sequence1.bytes)\n") + print("\nTransform Result: \n \(transformResult.bytes)\n") + + switch restoreState + { + case .completion(let restoreResult): + print("\nRestore Result: \n \(restoreResult.bytes)\n") + default: + XCTFail() + } + + case .completion(let transformResult): + let restoreState = toneBurst.remove(newData: transformResult) + print("\nBuffer to transform: \n \(sequence1.bytes)\n") + print("\nTransform Result: \n \(transformResult.bytes)\n") + + switch restoreState + { + case .completion(let restoreResult): + print("\nRestore Result: \n \(restoreResult.bytes)\n") + default: + XCTFail() + } + } } - - }