diff --git a/NanoTDF.xcodeproj/project.pbxproj b/NanoTDF.xcodeproj/project.pbxproj index 77253a8..7a4cd48 100644 --- a/NanoTDF.xcodeproj/project.pbxproj +++ b/NanoTDF.xcodeproj/project.pbxproj @@ -7,10 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + DA07A6952C165A83003DC210 /* CryptoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA07A6942C165A83003DC210 /* CryptoHelper.swift */; }; + DA07A6972C165ABB003DC210 /* CryptoHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA07A6962C165ABB003DC210 /* CryptoHelperTests.swift */; }; + DA07A6992C166E7E003DC210 /* BinaryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA07A6982C166E7E003DC210 /* BinaryParser.swift */; }; + DA07A69B2C167904003DC210 /* NanoTDFCreationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA07A69A2C167904003DC210 /* NanoTDFCreationTests.swift */; }; DA2B7B1B2BF1BB19002F3150 /* NanoTDFTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2B7B162BF1BB19002F3150 /* NanoTDFTests.swift */; }; DA2B7B1E2BF1BB19002F3150 /* WebSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2B7B192BF1BB19002F3150 /* WebSocketManager.swift */; }; DA3C23F42BE0765100B4B883 /* NanoTDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3C23F32BE0765100B4B883 /* NanoTDF.swift */; }; DA3C24052BE0787700B4B883 /* libNanoTDF.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA3C23F02BE0765100B4B883 /* libNanoTDF.a */; }; + DABEB4202C160B61004540E6 /* InitializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABEB41F2C160B61004540E6 /* InitializationTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,11 +41,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + DA07A6942C165A83003DC210 /* CryptoHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoHelper.swift; sourceTree = ""; }; + DA07A6962C165ABB003DC210 /* CryptoHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoHelperTests.swift; sourceTree = ""; }; + DA07A6982C166E7E003DC210 /* BinaryParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryParser.swift; sourceTree = ""; }; + DA07A69A2C167904003DC210 /* NanoTDFCreationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NanoTDFCreationTests.swift; sourceTree = ""; }; DA2B7B162BF1BB19002F3150 /* NanoTDFTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NanoTDFTests.swift; sourceTree = ""; }; DA2B7B192BF1BB19002F3150 /* WebSocketManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketManager.swift; sourceTree = ""; }; DA3C23F02BE0765100B4B883 /* libNanoTDF.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libNanoTDF.a; sourceTree = BUILT_PRODUCTS_DIR; }; DA3C23F32BE0765100B4B883 /* NanoTDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NanoTDF.swift; sourceTree = ""; }; DA3C24012BE0787700B4B883 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DABEB41F2C160B61004540E6 /* InitializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitializationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -66,7 +76,10 @@ isa = PBXGroup; children = ( DA2B7B162BF1BB19002F3150 /* NanoTDFTests.swift */, + DABEB41F2C160B61004540E6 /* InitializationTests.swift */, DA2B7B192BF1BB19002F3150 /* WebSocketManager.swift */, + DA07A6962C165ABB003DC210 /* CryptoHelperTests.swift */, + DA07A69A2C167904003DC210 /* NanoTDFCreationTests.swift */, ); path = Tests; sourceTree = ""; @@ -74,8 +87,8 @@ DA3C23E72BE0765100B4B883 = { isa = PBXGroup; children = ( - DA2B7B1A2BF1BB19002F3150 /* Tests */, DA3C23F22BE0765100B4B883 /* NanoTDF */, + DA2B7B1A2BF1BB19002F3150 /* Tests */, DA3C23F12BE0765100B4B883 /* Products */, ); sourceTree = ""; @@ -92,7 +105,9 @@ DA3C23F22BE0765100B4B883 /* NanoTDF */ = { isa = PBXGroup; children = ( + DA07A6942C165A83003DC210 /* CryptoHelper.swift */, DA3C23F32BE0765100B4B883 /* NanoTDF.swift */, + DA07A6982C166E7E003DC210 /* BinaryParser.swift */, ); path = NanoTDF; sourceTree = ""; @@ -188,6 +203,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DA07A6992C166E7E003DC210 /* BinaryParser.swift in Sources */, + DA07A6952C165A83003DC210 /* CryptoHelper.swift in Sources */, DA3C23F42BE0765100B4B883 /* NanoTDF.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -197,6 +214,9 @@ buildActionMask = 2147483647; files = ( DA2B7B1B2BF1BB19002F3150 /* NanoTDFTests.swift in Sources */, + DABEB4202C160B61004540E6 /* InitializationTests.swift in Sources */, + DA07A6972C165ABB003DC210 /* CryptoHelperTests.swift in Sources */, + DA07A69B2C167904003DC210 /* NanoTDFCreationTests.swift in Sources */, DA2B7B1E2BF1BB19002F3150 /* WebSocketManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/NanoTDF/BinaryParser.swift b/NanoTDF/BinaryParser.swift new file mode 100644 index 0000000..f46c0a0 --- /dev/null +++ b/NanoTDF/BinaryParser.swift @@ -0,0 +1,321 @@ +import Foundation + +class BinaryParser { + var data: Data + var cursor: Int = 0 + + init(data: Data) { + self.data = data + } + + func read(length: Int) -> Data? { + guard cursor + length <= data.count else { return nil } + let range = cursor ..< (cursor + length) + cursor += length + return data.subdata(in: range) + } + + private func readResourceLocator() -> ResourceLocator? { + guard let protocolData = read(length: 1), + let protocolEnum = protocolData.first, + let protocolEnumValue = ProtocolEnum(rawValue: protocolEnum), + let bodyLengthData = read(length: 1), + let bodyLength = bodyLengthData.first, + let body = read(length: Int(bodyLength)), + let bodyString = String(data: body, encoding: .utf8) + else { + return nil + } + let bodyLengthlHex = String(format: "%02x", bodyLength) + print("Body Length Hex:", bodyLengthlHex) + let bodyHexString = body.map { String(format: "%02x", $0) }.joined(separator: " ") + print("Body Hex:", bodyHexString) + print("bodyString: \(bodyString)") + return ResourceLocator(protocolEnum: protocolEnumValue, body: bodyString) + } + + private func readPolicyField(bindingMode: PolicyBindingConfig) -> Policy? { + guard let policyTypeData = read(length: 1), + let policyType = Policy.PolicyType(rawValue: policyTypeData[0]) + else { + return nil + } + + switch policyType { + case .remote: + guard let resourceLocator = readResourceLocator() else { + print("Failed to read Remote Policy resource locator") + return nil + } + // Binding + guard let binding = readPolicyBinding(bindingMode: bindingMode) else { + print("Failed to read Remote Policy binding") + return nil + } + return Policy(type: .remote, body: nil, remote: resourceLocator, binding: binding) + case .embeddedPlaintext, .embeddedEncrypted, .embeddedEncryptedWithPolicyKeyAccess: + let policyData = readEmbeddedPolicyBody(policyType: policyType, bindingMode: bindingMode) + // Binding + guard let binding = readPolicyBinding(bindingMode: bindingMode) else { + print("Failed to read Remote Policy binding") + return nil + } + return Policy(type: .embeddedPlaintext, body: policyData, remote: nil, binding: binding) + } + } + + private func readEmbeddedPolicyBody(policyType: Policy.PolicyType, bindingMode: PolicyBindingConfig) -> EmbeddedPolicyBody? { + guard let contentLengthData = read(length: 2) + else { + print("Failed to read Embedded Policy content length") + return nil + } + let plaintextCiphertextLengthData = contentLengthData.prefix(2) // contentLengthData.first + + let contentLength = plaintextCiphertextLengthData.withUnsafeBytes { + $0.load(as: UInt16.self) + } + print("Policy Body Length: \(contentLength)") + + // if no policy added then no read + // Note 3.4.2.3.2 Body for Embedded Policy states Minimum Length is 1 + if contentLength == 0 { + return EmbeddedPolicyBody(length: 1, body: Data([0x00]), keyAccess: nil) + } + + guard let plaintextCiphertext = read(length: Int(contentLength)) else { + print("Failed to read Embedded Policy plaintext / ciphertext") + return nil + } + // Policy Key Access + let keyAccess = policyType == .embeddedEncryptedWithPolicyKeyAccess ? readPolicyKeyAccess(bindingMode: bindingMode) : nil + + return EmbeddedPolicyBody(length: plaintextCiphertext.count, body: plaintextCiphertext, keyAccess: keyAccess) + } + + func readEccAndBindingMode() -> PolicyBindingConfig? { + guard let eccAndBindingModeData = read(length: 1), + let eccAndBindingMode = eccAndBindingModeData.first + else { + print("Failed to read BindingMode") + return nil + } + let eccModeHex = String(format: "%02x", eccAndBindingMode) + print("ECC Mode Hex:", eccModeHex) + let ecdsaBinding = (eccAndBindingMode & (1 << 7)) != 0 + let ephemeralECCParamsEnumValue = Curve(rawValue: eccAndBindingMode & 0x7) + + guard let ephemeralECCParamsEnum = ephemeralECCParamsEnumValue else { + print("Unsupported Ephemeral ECC Params Enum value") + return nil + } + + print("ecdsaBinding: \(ecdsaBinding)") + print("ephemeralECCParamsEnum: \(ephemeralECCParamsEnum)") + + return PolicyBindingConfig(ecdsaBinding: ecdsaBinding, curve: ephemeralECCParamsEnum) + } + + func readSymmetricAndPayloadConfig() -> SignatureAndPayloadConfig? { + guard let data = read(length: 1) + else { + return nil + } + print("SymmetricAndPayloadConfig read serialized data:", data.map { String($0, radix: 16) }) + guard data.count == 1 else { return nil } + let byte = data[0] + let signed = (byte & 0b1000_0000) != 0 + let signatureECCMode = Curve(rawValue: (byte & 0b0111_0000) >> 4) + let cipher = Cipher(rawValue: byte & 0b0000_1111) + + guard let signatureMode = signatureECCMode, let symmetricCipher = cipher else { + return nil + } + + return SignatureAndPayloadConfig(signed: signed, signatureCurve: signatureMode, payloadCipher: symmetricCipher) + } + + func readPolicyBinding(bindingMode: PolicyBindingConfig) -> Data? { + var bindingSize: Int + print("bindingMode", bindingMode) + if bindingMode.ecdsaBinding { + switch bindingMode.curve { + case .secp256r1, .xsecp256k1: + bindingSize = 64 + case .secp384r1: + bindingSize = 96 + case .secp521r1: + bindingSize = 132 + } + } else { + // GMAC Tag Binding + bindingSize = 16 + } + print("bindingSize", bindingSize) + return read(length: bindingSize) + } + + func readPolicyKeyAccess(bindingMode: PolicyBindingConfig) -> PolicyKeyAccess? { + let keySize: Int + switch bindingMode.curve { + case .secp256r1: + keySize = 65 + case .secp384r1: + keySize = 97 + case .secp521r1: + keySize = 133 + case .xsecp256k1: + keySize = 65 + } + + guard let resourceLocator = readResourceLocator(), + let ephemeralPublicKey = read(length: keySize) + else { + return nil + } + + return PolicyKeyAccess(resourceLocator: resourceLocator, ephemeralPublicKey: ephemeralPublicKey) + } + + func parseHeader() throws -> Header { + guard let magicNumber = read(length: FieldSize.magicNumberSize), + let version = read(length: FieldSize.versionSize), + let kas = readResourceLocator(), + let eccMode = readEccAndBindingMode(), + let payloadSigMode = readSymmetricAndPayloadConfig(), + let policy = readPolicyField(bindingMode: eccMode) + else { + throw ParsingError.invalidFormat + } + + let ephemeralKeySize: Int + switch eccMode.curve { + case .secp256r1: + ephemeralKeySize = 33 + case .secp384r1: + ephemeralKeySize = 49 + case .secp521r1: + ephemeralKeySize = 67 + case .xsecp256k1: + ephemeralKeySize = 33 + } + guard let ephemeralKey = read(length: ephemeralKeySize) else { + throw ParsingError.invalidFormat + } + + guard let header = Header(magicNumber: magicNumber, version: version, kas: kas, eccMode: eccMode, payloadSigMode: payloadSigMode, policy: policy, ephemeralKey: ephemeralKey) else { + throw ParsingError.invalidMagicNumber + } + return header + } + + func parsePayload(config: SignatureAndPayloadConfig) throws -> Payload { + guard let lengthData = read(length: FieldSize.payloadLengthSize) + else { + throw ParsingError.invalidFormat + } + let byte1 = UInt32(lengthData[0]) << 16 + let byte2 = UInt32(lengthData[1]) << 8 + let byte3 = UInt32(lengthData[2]) + let length: UInt32 = byte1 | byte2 | byte3 + print("parsePayload length", length) + // IV nonce + guard let iv = read(length: FieldSize.payloadIvSize) + else { + throw ParsingError.invalidFormat + } + // MAC Auth tag + let payloadMACSize: Int + switch config.payloadCipher { + case .aes256GCM64: + payloadMACSize = 8 + case .aes256GCM96: + payloadMACSize = 12 + case .aes256GCM104: + payloadMACSize = 13 + case .aes256GCM112: + payloadMACSize = 14 + case .aes256GCM120: + payloadMACSize = 15 + case .aes256GCM128: + payloadMACSize = 16 + case .none: + throw ParsingError.invalidFormat + } + // cipherText + let cipherTextLength = Int(length) - payloadMACSize - FieldSize.payloadIvSize + print("cipherTextLength", cipherTextLength) + guard let ciphertext = read(length: cipherTextLength), + let payloadMAC = read(length: payloadMACSize) + else { + throw ParsingError.invalidPayload + } + let payload = Payload(length: length, iv: iv, ciphertext: ciphertext, mac: payloadMAC) + return payload + } + + func parseSignature(config: SignatureAndPayloadConfig) throws -> Signature? { + if !config.signed { + return nil + } + let publicKeyLength: Int + let signatureLength: Int + print("config.signatureECCMode", config) + switch config.signatureCurve { + case .secp256r1, .xsecp256k1: + publicKeyLength = 33 + signatureLength = 64 + case .secp384r1: + publicKeyLength = 49 + signatureLength = 96 + case .secp521r1: + publicKeyLength = 67 + signatureLength = 132 + case .none: + print("signatureECCMode not found") + throw ParsingError.invalidFormat + } + print("publicKeyLength", publicKeyLength) + print("signatureLength", signatureLength) + guard let publicKey = read(length: publicKeyLength), + let signature = read(length: signatureLength) + else { + print("publicKey or signatureLength read error") + throw ParsingError.invalidFormat + } + return Signature(publicKey: publicKey, signature: signature) + } +} + +// see https://github.com/opentdf/spec/tree/main/schema/nanotdf +enum FieldSize { + static let magicNumberSize = 2 + static let versionSize = 1 + static let minKASSize = 3 + static let maxKASSize = 257 + static let eccModeSize = 1 + static let payloadSigModeSize = 1 + static let minPolicySize = 3 + static let maxPolicySize = 257 + static let minEphemeralKeySize = 33 + static let maxEphemeralKeySize = 133 + static let payloadLengthSize = 3 + static let payloadIvSize = 3 + static let minPayloadMacSize = 8 + static let maxPayloadMacSize = 32 +} + +enum ParsingError: Error { + case invalidFormat + case invalidMagicNumber + case invalidVersion + case invalidKAS + case invalidECCMode + case invalidPayloadSigMode + case invalidPolicy + case invalidEphemeralKey + case invalidPayload + case invalidPublicKeyLength + case invalidSignatureLength + case invalidSigning +} diff --git a/NanoTDF/CryptoHelper.swift b/NanoTDF/CryptoHelper.swift new file mode 100644 index 0000000..e4a0aef --- /dev/null +++ b/NanoTDF/CryptoHelper.swift @@ -0,0 +1,138 @@ +import CryptoKit +import Foundation + +enum CryptoHelperError: Error { + case unsupportedCurve +} + +enum CryptoHelper { + // Step 3: Generate Ephemeral Keypair based on curve + static func generateEphemeralKeyPair(curveType: Curve) -> (privateKey: Any, publicKey: Any)? { + switch curveType { + case .secp256r1: + let privateKey = P256.KeyAgreement.PrivateKey() + let publicKey = privateKey.publicKey + return (privateKey, publicKey) + case .secp384r1: + let privateKey = P384.KeyAgreement.PrivateKey() + let publicKey = privateKey.publicKey + return (privateKey, publicKey) + case .secp521r1: + let privateKey = P521.KeyAgreement.PrivateKey() + let publicKey = privateKey.publicKey + return (privateKey, publicKey) + case .xsecp256k1: + return nil + } + } + + // Step 3: Derive shared secret using ECDH + static func deriveSharedSecret(curveType: Curve, ephemeralPrivateKey: Any, recipientPublicKey: Any) throws -> SharedSecret? { + switch curveType { + case .secp256r1: + let privateKey = ephemeralPrivateKey as! P256.KeyAgreement.PrivateKey + let publicKey = recipientPublicKey as! P256.KeyAgreement.PublicKey + return try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + case .secp384r1: + let privateKey = ephemeralPrivateKey as! P384.KeyAgreement.PrivateKey + let publicKey = recipientPublicKey as! P384.KeyAgreement.PublicKey + return try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + case .secp521r1: + let privateKey = ephemeralPrivateKey as! P521.KeyAgreement.PrivateKey + let publicKey = recipientPublicKey as! P521.KeyAgreement.PublicKey + return try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + case .xsecp256k1: + return nil + } + } + + // Step 4: Derive symmetric key using HKDF + static func deriveSymmetricKey(sharedSecret: SharedSecret, salt: Data = Data(), info: Data = Data(), outputByteCount: Int = 32) -> SymmetricKey { + let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: info, outputByteCount: outputByteCount) + return symmetricKey + } + + // Generate GMAC tag for the policy body + static func createGMACBinding(policyBody: Data, symmetricKey: SymmetricKey) throws -> Data { + let gmac = try AES.GCM.seal(policyBody, using: symmetricKey) + return gmac.tag + } + + // Step 5: Generate nonce (IV) + static func generateNonce(length: Int = 12) -> Data { + var nonce = Data(count: length) + _ = nonce.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) } + return nonce + } + + // Pad or trim nonce (IV) to the required length + static func adjustNonce(_ nonce: Data, to length: Int) -> Data { + if nonce.count == length { + return nonce + } else if nonce.count > length { + return nonce.prefix(length) + } else { + var paddedNonce = nonce + paddedNonce.append(contentsOf: [UInt8](repeating: 0, count: length - nonce.count)) + return paddedNonce + } + } + + // Step 6: Encrypt payload using symmetric key and nonce (IV) + static func encryptPayload(plaintext: Data, symmetricKey: SymmetricKey, nonce: Data) throws -> (ciphertext: Data, tag: Data) { + let sealedBox = try AES.GCM.seal(plaintext, using: symmetricKey, nonce: AES.GCM.Nonce(data: nonce)) + return (sealedBox.ciphertext, sealedBox.tag) + } + + // Helper function to generate ECDSA signature + static func generateECDSASignature(privateKey: P256.Signing.PrivateKey, message: Data) throws -> Data? { + let derSignature = try privateKey.signature(for: message).derRepresentation + return extractRawECDSASignature(from: derSignature) + } + + // Helper function to extract r and s values from DER-encoded ECDSA signature + static func extractRawECDSASignature(from derSignature: Data) -> Data? { + var r: Data? + var s: Data? + + // Decode DER signature + // DER structure: 0x30 (SEQUENCE) + length + 0x02 (INTEGER) + r length + r + 0x02 (INTEGER) + s length + s + guard derSignature.count > 8 else { return nil } + + var index = 0 + guard derSignature[index] == 0x30 else { return nil } + index += 1 + + _ = derSignature[index] // length of the sequence + index += 1 + + guard derSignature[index] == 0x02 else { return nil } + index += 1 + + let rLength = Int(derSignature[index]) + index += 1 + + r = derSignature[index ..< (index + rLength)] + index += rLength + + guard derSignature[index] == 0x02 else { return nil } + index += 1 + + let sLength = Int(derSignature[index]) + index += 1 + + s = derSignature[index ..< (index + sLength)] + + // Ensure r and s are present and have correct lengths + guard let rData = r, let sData = s else { return nil } + + // Remove leading zero if present + let rTrimmed = rData.count == 33 ? rData.dropFirst() : rData + let sTrimmed = sData.count == 33 ? sData.dropFirst() : sData + + // Ensure r and s have correct lengths + guard rTrimmed.count == 32, sTrimmed.count == 32 else { return nil } + + return rTrimmed + sTrimmed + } +} diff --git a/NanoTDF/NanoTDF.swift b/NanoTDF/NanoTDF.swift index 2e946d0..4892c32 100644 --- a/NanoTDF/NanoTDF.swift +++ b/NanoTDF/NanoTDF.swift @@ -17,24 +17,46 @@ struct NanoTDF { } } +protocol NanoTDFDecorator { + func compressNanoTDF() -> NanoTDF + func encryptNanoTDF() -> NanoTDF + func signAndBindNanoTDF() -> NanoTDF +} + struct Header { let magicNumber: Data let version: Data let kas: ResourceLocator - let eccMode: ECCAndBindingMode - var payloadSigMode: SymmetricAndPayloadConfig + let policyBindingConfig: PolicyBindingConfig + var payloadSignatureConfig: SignatureAndPayloadConfig let policy: Policy - let ephemeralKey: Data + let ephemeralPublicKey: Data + + init?(magicNumber: Data, version: Data, kas: ResourceLocator, eccMode: PolicyBindingConfig, payloadSigMode: SignatureAndPayloadConfig, policy: Policy, ephemeralKey: Data) { + // Validate magicNumber + let expectedMagicNumber = Data([0x4C, 0x31]) // 0x4C31 (L1L) - first 18 bits + guard magicNumber.prefix(2) == expectedMagicNumber else { + print("Header.init magicNumber", magicNumber) + return nil + } + self.magicNumber = magicNumber + self.version = version + self.kas = kas + policyBindingConfig = eccMode + payloadSignatureConfig = payloadSigMode + self.policy = policy + ephemeralPublicKey = ephemeralKey + } func toData() -> Data { var data = Data() data.append(magicNumber) data.append(version) data.append(kas.toData()) - data.append(eccMode.toData()) - data.append(payloadSigMode.toData()) + data.append(policyBindingConfig.toData()) + data.append(payloadSignatureConfig.toData()) data.append(policy.toData()) - data.append(ephemeralKey) + data.append(ephemeralPublicKey) return data } } @@ -44,11 +66,12 @@ struct Payload { let iv: Data let ciphertext: Data let mac: Data - + func toData() -> Data { var data = Data() - let lengthBytes = withUnsafeBytes(of: length.bigEndian) { Array($0) } - data.append(contentsOf: lengthBytes[1...3]) // Append the last 3 bytes to represent a 3-byte length + data.append(UInt8((length >> 16) & 0xFF)) + data.append(UInt8((length >> 8) & 0xFF)) + data.append(UInt8(length & 0xFF)) data.append(iv) data.append(ciphertext) data.append(mac) @@ -68,35 +91,37 @@ struct Signature { } } -struct ECCAndBindingMode { - var useECDSABinding: Bool - var ephemeralECCParamsEnum: ECDSAParams - +struct PolicyBindingConfig { + // true ECDSA using creator key. The signature is used as the binding + // false GMAC tag is computed over the policy body using the derived symmetric key. + var ecdsaBinding: Bool + var curve: Curve + func toData() -> Data { var byte: UInt8 = 0 - if useECDSABinding { - byte |= 0b10000000 // Set the USE_ECDSA_BINDING bit (bit 7) + if ecdsaBinding { + byte |= 0b1000_0000 // Set the USE_ECDSA_BINDING bit (bit 7) } - byte |= (ephemeralECCParamsEnum.rawValue & 0b00000111) // Set the Ephemeral ECC Params Enum bits (bits 0-2) + byte |= (curve.rawValue & 0b0000_0111) // Set the Ephemeral ECC Params Enum bits (bits 0-2) return Data([byte]) } } -struct SymmetricAndPayloadConfig { - var hasSignature: Bool - var signatureECCMode: ECDSAParams? - let symmetricCipherEnum: SymmetricCiphers? - +struct SignatureAndPayloadConfig { + var signed: Bool + var signatureCurve: Curve? + let payloadCipher: Cipher? + func toData() -> Data { var byte: UInt8 = 0 - if hasSignature { - byte |= 0b10000000 // Set the HAS_SIGNATURE bit (bit 7) + if signed { + byte |= 0b1000_0000 // Set the HAS_SIGNATURE bit (bit 7) } - if let signatureECCMode = signatureECCMode { - byte |= (signatureECCMode.rawValue & 0b00000111) << 4 // Set the Signature ECC Mode bits (bits 4-6) + if let signatureECCMode = signatureCurve { + byte |= (signatureECCMode.rawValue & 0b0000_0111) << 4 // Set the Signature ECC Mode bits (bits 4-6) } - if let symmetricCipherEnum = symmetricCipherEnum { - byte |= (symmetricCipherEnum.rawValue & 0b00001111) // Set the Symmetric Cipher Enum bits (bits 0-3) + if let symmetricCipherEnum = payloadCipher { + byte |= (symmetricCipherEnum.rawValue & 0b0000_1111) // Set the Symmetric Cipher Enum bits (bits 0-3) } print("SymmetricAndPayloadConfig write serialized data:", Data([byte]).map { String($0, radix: 16) }) return Data([byte]) @@ -106,13 +131,26 @@ struct SymmetricAndPayloadConfig { enum ProtocolEnum: UInt8 { case http = 0x00 case https = 0x01 - case unreserved = 0x02 + // BEGIN out-of-spec + case ws = 0x02 + case wss = 0x03 + // END out-of-spec case sharedResourceDirectory = 0xFF } struct ResourceLocator { let protocolEnum: ProtocolEnum let body: String + + init?(protocolEnum: ProtocolEnum, body: String) { + guard body.utf8.count >= 1, body.utf8.count <= 255 else { + print(body.utf8.count) + return nil + } + self.protocolEnum = protocolEnum + self.body = body + } + func toData() -> Data { var data = Data() data.append(protocolEnum.rawValue) @@ -129,15 +167,15 @@ struct Policy { case remote = 0x00 case embeddedPlaintext = 0x01 case embeddedEncrypted = 0x02 + // IV value 00 00 00 is reserved for use with an encrypted policy. case embeddedEncryptedWithPolicyKeyAccess = 0x03 } let type: PolicyType - let body: Data? + let body: EmbeddedPolicyBody? let remote: ResourceLocator? - let binding: Data? - let keyAccess: PolicyKeyAccess? - + var binding: Data? + func toData() -> Data { var data = Data() data.append(type.rawValue) @@ -148,10 +186,7 @@ struct Policy { } case .embeddedPlaintext, .embeddedEncrypted, .embeddedEncryptedWithPolicyKeyAccess: if let body = body { - data.append(body) - } - if let keyAccess = keyAccess { - data.append(keyAccess.toData()) + data.append(body.toData()) } } if let binding = binding { @@ -162,15 +197,25 @@ struct Policy { } struct EmbeddedPolicyBody { - let contentLength: UInt16 - let plaintextCiphertext: Data? - let policyKeyAccess: PolicyKeyAccess? + let length: Int + let body: Data + let keyAccess: PolicyKeyAccess? + + func toData() -> Data { + var data = Data() + data.append(UInt8(body.count)) // length + data.append(body) + if let keyAccess = keyAccess { + data.append(keyAccess.toData()) + } + return data + } } struct PolicyKeyAccess { let resourceLocator: ResourceLocator let ephemeralPublicKey: Data - + func toData() -> Data { var data = Data() data.append(resourceLocator.toData()) @@ -179,394 +224,30 @@ struct PolicyKeyAccess { } } -enum ECDSAParams: UInt8 { +enum Curve: UInt8 { case secp256r1 = 0x00 case secp384r1 = 0x01 case secp521r1 = 0x02 - case secp256k1 = 0x03 + // BEGIN in-spec unsupported + case xsecp256k1 = 0x03 + // END in-spec unsupported } -enum SymmetricCiphers: UInt8 { - case GCM_64 = 0x00 - case GCM_96 = 0x01 - case GCM_104 = 0x02 - case GCM_112 = 0x03 - case GCM_120 = 0x04 - case GCM_128 = 0x05 -} - -class BinaryParser { - var data: Data - var cursor: Int = 0 - - init(data: Data) { - self.data = data - } - - func read(length: Int) -> Data? { - guard cursor + length <= data.count else { return nil } - let range = cursor ..< (cursor + length) - cursor += length - return data.subdata(in: range) - } - - private func readResourceLocator() -> ResourceLocator? { - guard let protocolData = read(length: 1), - let protocolEnum = protocolData.first, - let protocolEnumValue = ProtocolEnum(rawValue: protocolEnum), - let bodyLengthData = read(length: 1), - let bodyLength = bodyLengthData.first, - let body = read(length: Int(bodyLength)), - let bodyString = String(data: body, encoding: .utf8) - else { - return nil - } - let bodyLengthlHex = String(format: "%02x", bodyLength) - print("Body Length Hex:", bodyLengthlHex) - let bodyHexString = body.map { String(format: "%02x", $0) }.joined(separator: " ") - print("Body Hex:", bodyHexString) - print("bodyString: \(bodyString)") - return ResourceLocator(protocolEnum: protocolEnumValue, body: bodyString) - } - - private func readPolicyField(bindingMode: ECCAndBindingMode) -> Policy? { - guard let policyTypeData = read(length: 1), - let policyType = Policy.PolicyType(rawValue: policyTypeData[0]) else { - return nil - } - - switch policyType { - case .remote: - guard let resourceLocator = readResourceLocator() else { - print("Failed to read Remote Policy resource locator") - return nil - } - // Binding - guard let binding = readPolicyBinding(bindingMode: bindingMode) else { - print("Failed to read Remote Policy binding") - return nil - } - return Policy(type: .remote, body: nil, remote: resourceLocator, binding: binding, keyAccess: nil) - case .embeddedPlaintext, .embeddedEncrypted, .embeddedEncryptedWithPolicyKeyAccess: - let policyData = readEmbeddedPolicyBody(policyType: policyType, bindingMode: bindingMode) - // Binding - guard let binding = readPolicyBinding(bindingMode: bindingMode) else { - print("Failed to read Remote Policy binding") - return nil - } - return Policy(type: .embeddedPlaintext, body: policyData?.plaintextCiphertext, remote: nil, binding: binding, keyAccess: policyData?.policyKeyAccess) - } - } - - private func readEmbeddedPolicyBody(policyType: Policy.PolicyType, bindingMode: ECCAndBindingMode) -> EmbeddedPolicyBody? { - guard let contentLengthData = read(length: 2) - else { - print("Failed to read Embedded Policy content length") - return nil - } - let plaintextCiphertextLengthData = contentLengthData.prefix(2) // contentLengthData.first - - let contentLength = plaintextCiphertextLengthData.withUnsafeBytes { - $0.load(as: UInt16.self) - } - print("Policy Body Length: \(contentLength)") - - // if no policy added then no read - // Note 3.4.2.3.2 Body for Embedded Policy states Minimum Length is 1 - if contentLength == 0 { - return EmbeddedPolicyBody(contentLength: contentLength, plaintextCiphertext: nil, policyKeyAccess: nil) - } - - guard let plaintextCiphertext = read(length: Int(contentLength)) else { - print("Failed to read Embedded Policy plaintext / ciphertext") - return nil - } - let keyAccess = policyType == .embeddedEncryptedWithPolicyKeyAccess ? readPolicyKeyAccess(bindingMode: bindingMode) : nil - - return EmbeddedPolicyBody(contentLength: contentLength, plaintextCiphertext: plaintextCiphertext, policyKeyAccess: keyAccess) - } - - func readEccAndBindingMode() -> ECCAndBindingMode? { - guard let eccAndBindingModeData = read(length: 1), - let eccAndBindingMode = eccAndBindingModeData.first - else { - print("Failed to read BindingMode") - return nil - } - let eccModeHex = String(format: "%02x", eccAndBindingMode) - print("ECC Mode Hex:", eccModeHex) - let useECDSABinding = (eccAndBindingMode & (1 << 7)) != 0 - let ephemeralECCParamsEnumValue = ECDSAParams(rawValue: eccAndBindingMode & 0x7) - - guard let ephemeralECCParamsEnum = ephemeralECCParamsEnumValue else { - print("Unsupported Ephemeral ECC Params Enum value") - return nil - } - - print("useECDSABinding: \(useECDSABinding)") - print("ephemeralECCParamsEnum: \(ephemeralECCParamsEnum)") - - return ECCAndBindingMode(useECDSABinding: useECDSABinding, ephemeralECCParamsEnum: ephemeralECCParamsEnum) - } - - func readSymmetricAndPayloadConfig() -> SymmetricAndPayloadConfig? { - guard let data = read(length: 1) - else { - return nil - } - print("SymmetricAndPayloadConfig read serialized data:", data.map { String($0, radix: 16) }) - guard data.count == 1 else { return nil } - let byte = data[0] - let hasSignature = (byte & 0b10000000) != 0 - let signatureECCMode = ECDSAParams(rawValue: (byte & 0b01110000) >> 4) - let symmetricCipherEnum = SymmetricCiphers(rawValue: byte & 0b00001111) - - guard let signatureMode = signatureECCMode, let symmetricCipher = symmetricCipherEnum else { - return nil - } - - return SymmetricAndPayloadConfig(hasSignature: hasSignature, signatureECCMode: signatureMode, symmetricCipherEnum: symmetricCipher) - } - - func readPolicyBinding(bindingMode: ECCAndBindingMode) -> Data? { - var bindingSize: Int - print("bindingMode", bindingMode) - if bindingMode.useECDSABinding { - bindingSize = 64 - } - else { - switch bindingMode.ephemeralECCParamsEnum { - case .secp256r1, .secp256k1: - bindingSize = 64 - case .secp384r1: - bindingSize = 96 - case .secp521r1: - bindingSize = 132 - } - } - print("bindingSize", bindingSize) - if bindingMode.useECDSABinding { - bindingSize = 64 - } - return read(length: bindingSize) - } - - func readPolicyKeyAccess(bindingMode: ECCAndBindingMode) -> PolicyKeyAccess? { - let keySize: Int - switch bindingMode.ephemeralECCParamsEnum { - case .secp256r1: - keySize = 65 - case .secp384r1: - keySize = 97 - case .secp521r1: - keySize = 133 - case .secp256k1: - keySize = 65 - } - - guard let resourceLocator = readResourceLocator(), - let ephemeralPublicKey = read(length: keySize) else { - return nil - } - - return PolicyKeyAccess(resourceLocator: resourceLocator, ephemeralPublicKey: ephemeralPublicKey) - } - - func parseHeader() throws -> Header { - guard let magicNumber = read(length: FieldSize.magicNumberSize), - let version = read(length: FieldSize.versionSize), - let kas = readResourceLocator(), - let eccMode = readEccAndBindingMode(), - let payloadSigMode = readSymmetricAndPayloadConfig(), - let policy = readPolicyField(bindingMode: eccMode) - else { - throw ParsingError.invalidFormat - } - - let ephemeralKeySize: Int - switch eccMode.ephemeralECCParamsEnum { - case .secp256r1: - ephemeralKeySize = 33 - case .secp384r1: - ephemeralKeySize = 49 - case .secp521r1: - ephemeralKeySize = 67 - case .secp256k1: - ephemeralKeySize = 33 - } - guard let ephemeralKey = read(length: ephemeralKeySize) else { - throw ParsingError.invalidFormat - } - - return Header(magicNumber: magicNumber, version: version, kas: kas, eccMode: eccMode, payloadSigMode: payloadSigMode, policy: policy, ephemeralKey: ephemeralKey) - } - - func parsePayload(config: SymmetricAndPayloadConfig) throws -> Payload { - guard let lengthData = read(length: FieldSize.payloadCipherTextSize) - else { - throw ParsingError.invalidFormat - } - var length: UInt32 = 0 - let count = lengthData.count - for i in 0.. Signature? { - if !config.hasSignature { - return nil - } - let publicKeyLength: Int - let signatureLength: Int - print("config.signatureECCMode", config) - switch config.signatureECCMode { - case .secp256r1, .secp256k1: - publicKeyLength = 33 - signatureLength = 64 - case .secp384r1: - publicKeyLength = 49 - signatureLength = 96 - case .secp521r1: - publicKeyLength = 67 - signatureLength = 132 - case .none: - print("signatureECCMode not found") - throw ParsingError.invalidFormat - } - print("publicKeyLength", publicKeyLength) - print("signatureLength", signatureLength) - guard let publicKey = read(length: publicKeyLength), - let signature = read(length: signatureLength) else { - print("publicKey or signatureLength read error") - throw ParsingError.invalidFormat - } - return Signature(publicKey: publicKey, signature: signature) - } -} - -// see https://github.com/opentdf/spec/tree/main/schema/nanotdf -enum FieldSize { - static let magicNumberSize = 2 - static let versionSize = 1 - static let minKASSize = 3 - static let maxKASSize = 257 - static let eccModeSize = 1 - static let payloadSigModeSize = 1 - static let minPolicySize = 3 - static let maxPolicySize = 257 - static let minEphemeralKeySize = 33 - static let maxEphemeralKeySize = 133 - static let payloadCipherTextSize = 3 - static let payloadIvSize = 3 - static let minPayloadMacSize = 8 - static let maxPayloadMacSize = 32 -} - -enum ParsingError: Error { - case invalidFormat - case invalidMagicNumber - case invalidVersion - case invalidKAS - case invalidECCMode - case invalidPayloadSigMode - case invalidPolicy - case invalidEphemeralKey - case invalidPayload - case invalidPublicKeyLength - case invalidSignatureLength - case invalidSigning -} - -// Helper function to extract r and s values from DER-encoded ECDSA signature -func extractRawECDSASignature(from derSignature: Data) -> Data? { - var r: Data? - var s: Data? - - // Decode DER signature - // DER structure: 0x30 (SEQUENCE) + length + 0x02 (INTEGER) + r length + r + 0x02 (INTEGER) + s length + s - guard derSignature.count > 8 else { return nil } - - var index = 0 - guard derSignature[index] == 0x30 else { return nil } - index += 1 - - let _ = derSignature[index] // length of the sequence - index += 1 - - guard derSignature[index] == 0x02 else { return nil } - index += 1 - - let rLength = Int(derSignature[index]) - index += 1 - - r = derSignature[index..<(index + rLength)] - index += rLength - - guard derSignature[index] == 0x02 else { return nil } - index += 1 - - let sLength = Int(derSignature[index]) - index += 1 - - s = derSignature[index..<(index + sLength)] - - // Ensure r and s are present and have correct lengths - guard let rData = r, let sData = s else { return nil } - - // Remove leading zero if present - let rTrimmed = rData.count == 33 ? rData.dropFirst() : rData - let sTrimmed = sData.count == 33 ? sData.dropFirst() : sData - - // Ensure r and s have correct lengths - guard rTrimmed.count == 32, sTrimmed.count == 32 else { return nil } - - return rTrimmed + sTrimmed -} - -// Helper function to generate ECDSA signature -func generateECDSASignature(privateKey: P256.Signing.PrivateKey, message: Data) throws -> Data? { - let derSignature = try privateKey.signature(for: message).derRepresentation - return extractRawECDSASignature(from: derSignature) +enum Cipher: UInt8 { + case aes256GCM64 = 0x00 + case aes256GCM96 = 0x01 + case aes256GCM104 = 0x02 + case aes256GCM112 = 0x03 + case aes256GCM120 = 0x04 + // CryptoKit’s AES.GCM uses a 128-bit authentication tag by default, + // and you don't need to (nor can you) specify different tag lengths. + case aes256GCM128 = 0x05 } // Function to add a signature to a NanoTDF -func addSignatureToNanoTDF(nanoTDF: inout NanoTDF, privateKey: P256.Signing.PrivateKey, config: SymmetricAndPayloadConfig) throws { +func addSignatureToNanoTDF(nanoTDF: inout NanoTDF, privateKey: P256.Signing.PrivateKey, config: SignatureAndPayloadConfig) throws { let message = nanoTDF.header.toData() + nanoTDF.payload.toData() - guard let signatureData = try generateECDSASignature(privateKey: privateKey, message: message) else { + guard let signatureData = try CryptoHelper.generateECDSASignature(privateKey: privateKey, message: message) else { throw ParsingError.invalidSigning } print("signatureData", signatureData.count) @@ -576,9 +257,9 @@ func addSignatureToNanoTDF(nanoTDF: inout NanoTDF, privateKey: P256.Signing.Priv let publicKeyLength: Int let signatureLength: Int - print("config.signatureECCMode", config.signatureECCMode as Any) - switch config.signatureECCMode { - case .secp256r1, .secp256k1: + print("config.signatureECCMode", config.signatureCurve as Any) + switch config.signatureCurve { + case .secp256r1, .xsecp256k1: publicKeyLength = 33 signatureLength = 64 case .secp384r1: @@ -602,6 +283,110 @@ func addSignatureToNanoTDF(nanoTDF: inout NanoTDF, privateKey: P256.Signing.Priv let signature = Signature(publicKey: publicKeyData, signature: signatureData) nanoTDF.signature = signature - nanoTDF.header.payloadSigMode.hasSignature = true - nanoTDF.header.payloadSigMode.signatureECCMode = config.signatureECCMode // Use the provided config + nanoTDF.header.payloadSignatureConfig.signed = true + nanoTDF.header.payloadSignatureConfig.signatureCurve = config.signatureCurve // Use the provided config +} + +// Initialize a NanoTDF small +func initializeSmallNanoTDF(kasResourceLocator: ResourceLocator) -> NanoTDF { + let magicNumber = Data([0x4C, 0x31]) // 0x4C31 (L1L) - first 18 bits + let version = Data([0x0C]) // version[0] & 0x3F (12) last 6 bits for version + let curve: Curve = .secp256r1 + let header = Header(magicNumber: magicNumber, + version: version, + kas: kasResourceLocator, + eccMode: PolicyBindingConfig(ecdsaBinding: false, + curve: curve), + payloadSigMode: SignatureAndPayloadConfig(signed: false, + signatureCurve: nil, + payloadCipher: .aes256GCM128), + policy: Policy(type: .embeddedPlaintext, + body: nil, + remote: nil, + binding: nil), + ephemeralKey: Data([0x04, 0x05, 0x06])) + + let payload = Payload(length: 7, + iv: Data([0x07, 0x08, 0x09]), + ciphertext: Data([0x00]), + mac: Data([0x13, 0x14, 0x15])) + + return NanoTDF(header: header!, + payload: payload, + signature: nil) +} + +struct KasMetadata { + let resourceLocator: ResourceLocator + let publicKey: Any + let curve: Curve +} + +func createNanoTDF(kas: KasMetadata, policy: inout Policy, plaintext: Data) throws -> NanoTDF { + // Step 1: Generate an ephemeral key pair + guard let (ephemeralPrivateKey, ephemeralPublicKey) = CryptoHelper.generateEphemeralKeyPair(curveType: kas.curve) else { + throw NSError(domain: "CryptoError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to generate ephemeral key pair"]) + } + + // Step 2: Derive shared secret + guard let sharedSecret = try CryptoHelper.deriveSharedSecret(curveType: kas.curve, ephemeralPrivateKey: ephemeralPrivateKey, recipientPublicKey: kas.publicKey) else { + throw NSError(domain: "CryptoError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to derive shared secret"]) + } + + // Step 3: Derive symmetric key + let symmetricKey = CryptoHelper.deriveSymmetricKey(sharedSecret: sharedSecret) + var policyBody: Data + switch policy.type { + case .remote: + policyBody = policy.remote!.toData() + case .embeddedPlaintext, .embeddedEncrypted, .embeddedEncryptedWithPolicyKeyAccess: + policyBody = policy.body!.toData() + } + let gmacTag = try CryptoHelper.createGMACBinding(policyBody: policyBody, symmetricKey: symmetricKey) + policy.binding = gmacTag + // Step 4: Generate nonce (IV) + // 3.3.2.2 IV + Ciphertext + MAClength 3 + let nonce3 = CryptoHelper.adjustNonce(CryptoHelper.generateNonce(), to: 3) + let nonce12 = CryptoHelper.adjustNonce(nonce3, to: 12) + + // Step 5: Encrypt payload + let (ciphertext, tag) = try CryptoHelper.encryptPayload(plaintext: plaintext, symmetricKey: symmetricKey, nonce: nonce12) + + // Step 6: Create Policy Key Access structure +// let policyKeyAccessEphemeralKeyPair = CryptoHelper.generateEphemeralKeyPair(curveType: kas.curve)! +// let policyKeyAccess = PolicyKeyAccess( +// resourceLocator: kas.resourceLocator, +// ephemeralPublicKey: policyKeyAccessEphemeralKeyPair.publicKey +// ) + + // If including nonce in payload, add its length + let payloadLength = ciphertext.count + tag.count + nonce3.count + print("createNanoTDF payloadLength", payloadLength) + // Payload + let payload = Payload(length: UInt32(payloadLength), + iv: nonce3, + ciphertext: ciphertext, + mac: tag) + // Header + let magicNumber = Data([0x4C, 0x31]) // 0x4C31 (L1L) - first 18 bits + let version = Data([0x4C]) // version[0] & 0x3F (12) last 6 bits for version + let curve: Curve = .secp256r1 + var ephemeralPublicKeyData: Data = Data() + if let ephemeralPublicKey = ephemeralPublicKey as? P256.KeyAgreement.PublicKey { + ephemeralPublicKeyData = ephemeralPublicKey.compressedRepresentation + } + print("ephemeralPublicKeyData.count", ephemeralPublicKeyData.count) + let header = Header(magicNumber: magicNumber, + version: version, + kas: kas.resourceLocator, + eccMode: PolicyBindingConfig(ecdsaBinding: false, + curve: curve), + payloadSigMode: SignatureAndPayloadConfig(signed: false, + signatureCurve: .secp256r1, + payloadCipher: .aes256GCM128), + policy: policy, + ephemeralKey: ephemeralPublicKeyData) + return NanoTDF(header: header!, + payload: payload, + signature: nil) } diff --git a/Package.swift b/Package.swift index 65293c3..85db0e3 100644 --- a/Package.swift +++ b/Package.swift @@ -7,12 +7,13 @@ let package = Package( .macOS(.v13), // Update to the latest macOS version .iOS(.v16), // Update to the latest iOS version .tvOS(.v16), // Update to the latest tvOS version - .watchOS(.v9) // Update to the latest watchOS version + .watchOS(.v9), // Update to the latest watchOS version ], products: [ .library( name: "NanoTDF", - targets: ["NanoTDF"]), + targets: ["NanoTDF"] + ), ], dependencies: [ // Dependencies @@ -21,10 +22,12 @@ let package = Package( .target( name: "NanoTDF", dependencies: [], - path: "NanoTDF"), + path: "NanoTDF" + ), .testTarget( name: "Tests", dependencies: ["NanoTDF"], - path: "Tests") + path: "Tests" + ), ] ) diff --git a/README.md b/README.md index 797598c..2a20caf 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ Implementation of the [OpenTDF nanotdf specification](https://github.com/opentdf/spec/tree/main/schema/nanotdf) + +## NanoTDF creation sequence +```mermaid +sequenceDiagram + participant App + participant TDFBuilder + participant CryptoLibrary + participant KAS + + App->>KAS: Request recipient public key + KAS-->>App: Recipient public key + + App->>TDFBuilder: Provide cleartext and policy + TDFBuilder->>CryptoLibrary: Generate ephemeral key pair + CryptoLibrary-->>TDFBuilder: Ephemeral private key, Ephemeral public key + + TDFBuilder->>CryptoLibrary: Derive shared secret (ephemeral private key, recipient public key) + CryptoLibrary-->>TDFBuilder: Shared secret + + TDFBuilder->>CryptoLibrary: Derive symmetric key (shared secret) + CryptoLibrary-->>TDFBuilder: Symmetric key + + TDFBuilder->>CryptoLibrary: Generate Nonce (IV) + CryptoLibrary-->>TDFBuilder: Nonce (IV) + + TDFBuilder->>CryptoLibrary: Encrypt payload (cleartext, symmetric key, Nonce (IV)) + CryptoLibrary-->>TDFBuilder: Ciphertext, Authentication tag (MAC) + + alt GMAC Binding + TDFBuilder->>CryptoLibrary: Generate GMAC tag (symmetric key, policy body) + CryptoLibrary-->>TDFBuilder: GMAC tag + else ECDSA Binding + TDFBuilder->>CryptoLibrary: Sign policy body (creator's private key) + CryptoLibrary-->>TDFBuilder: ECDSA signature + end + + alt Policy Key Access + TDFBuilder->>CryptoLibrary: Generate key for policy (ephemeral private key, recipient public key) + CryptoLibrary-->>TDFBuilder: Policy encryption key + TDFBuilder->>CryptoLibrary: Encrypt policy (policy, policy encryption key) + CryptoLibrary-->>TDFBuilder: Encrypted policy + + TDFBuilder->>TDFBuilder: Add Policy Key Access section (Resource Locator, Ephemeral Public Key) + end + + alt Signature + TDFBuilder->>CryptoLibrary: Create signature for header and payload (creator's private key) + CryptoLibrary-->>TDFBuilder: ECDSA signature + end + + TDFBuilder->>TDFBuilder: Construct header (metadata, ephemeral public key, policy binding, optional signature, optional encrypted policy, optional Policy Key Access) + TDFBuilder->>TDFBuilder: Combine header, encrypted payload, and signature + TDFBuilder-->>App: Return NanoTDF +``` \ No newline at end of file diff --git a/Tests/CryptoHelperTests.swift b/Tests/CryptoHelperTests.swift new file mode 100644 index 0000000..c5f5f94 --- /dev/null +++ b/Tests/CryptoHelperTests.swift @@ -0,0 +1,40 @@ +import CryptoKit +import Foundation +import XCTest + +@testable import NanoTDF + +final class CryptoHelperTests: XCTestCase { + func testInitializeSmallNanoTDFPositive() throws { + // Step 1: Initial Key Exchange + // Recipient Compressed Public Key + let recipientBase64 = "A2ifhGOpE0DjR4R0FPXvZ6YBOrcjayIpxwtxeXTudOts" + guard let recipientDER = Data(base64Encoded: recipientBase64) else { + throw NSError(domain: "invalid base64 encoding", code: 0, userInfo: nil) + } + // Assume we have recipient's public key + let recipientPublicKey = try P256.KeyAgreement.PublicKey(compressedRepresentation: recipientDER) + // Generate ephemeral key pair for P256 + if let (ephemeralPrivateKey, _) = CryptoHelper.generateEphemeralKeyPair(curveType: .secp256r1) { + // Step 3: Derive shared secret + if let sharedSecret = try CryptoHelper.deriveSharedSecret(curveType: .secp256r1, ephemeralPrivateKey: ephemeralPrivateKey, recipientPublicKey: recipientPublicKey) { + print("Shared Secret: \(sharedSecret)") + // Step 4: Derive symmetric key + let symmetricKey = CryptoHelper.deriveSymmetricKey(sharedSecret: sharedSecret) + print("Symmetric Key: \(symmetricKey)") + // Create GMAC binding for the policy body + let policyBody = "classification:secret".data(using: .utf8)! + let gmacTag = try CryptoHelper.createGMACBinding(policyBody: policyBody, symmetricKey: symmetricKey) + print("GMAC Tag: \(gmacTag.base64EncodedString())") + // Step 5: Generate nonce (IV) + let nonce = CryptoHelper.generateNonce() + print("Nonce (IV): \(nonce)") + // Step 6: Encrypt payload + let plaintext = Data("This is a secret message".utf8) + let (ciphertext, tag) = try CryptoHelper.encryptPayload(plaintext: plaintext, symmetricKey: symmetricKey, nonce: nonce) + print("Ciphertext: \(ciphertext)") + print("Tag: \(tag)") + } + } + } +} diff --git a/Tests/InitializationTests.swift b/Tests/InitializationTests.swift new file mode 100644 index 0000000..983ba99 --- /dev/null +++ b/Tests/InitializationTests.swift @@ -0,0 +1,76 @@ +// +// InitializationTests.swift +// Tests +// +// Created by Paul Flynn on 6/9/24. +// + +@testable import NanoTDF +import XCTest + +final class InitializationTests: XCTestCase { + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testInitializeSmallNanoTDFPositive() throws { + let locator = ResourceLocator(protocolEnum: .http, body: "localhost:8080") + XCTAssertNotNil(locator) + let nanoTDF = initializeSmallNanoTDF(kasResourceLocator: locator!) + // Validate the Header + XCTAssertEqual(nanoTDF.header.magicNumber, Data([0x4C, 0x31])) + XCTAssertEqual(nanoTDF.header.version, Data([0x0C])) + XCTAssertEqual(nanoTDF.header.kas.protocolEnum, locator!.protocolEnum) + XCTAssertEqual(nanoTDF.header.kas.body, locator!.body) + // Validate the Payload + XCTAssertEqual(nanoTDF.payload.length, 7) + XCTAssertEqual(nanoTDF.payload.iv, Data([0x07, 0x08, 0x09])) + // As there signature is nil in this scenario + XCTAssertNil(nanoTDF.signature) + } + + func testInitializeSmallNanoTDFNegative() throws { + // out of spec - too small + var locator = ResourceLocator(protocolEnum: .http, body: "") + XCTAssertNil(locator) + // out of spec - too large + let body256Bytes = String(repeating: "a", count: 256) + locator = ResourceLocator(protocolEnum: .http, body: body256Bytes) + XCTAssertNil(locator) + locator = ResourceLocator(protocolEnum: .http, body: "localhost:8080") + let header = Header(magicNumber: Data([0xFF, 0xFF]), + version: Data([0xFF]), + kas: locator!, + eccMode: PolicyBindingConfig(ecdsaBinding: false, + curve: .secp256r1), + payloadSigMode: SignatureAndPayloadConfig(signed: false, + signatureCurve: nil, + payloadCipher: .aes256GCM128), + policy: Policy(type: .embeddedPlaintext, + body: nil, + remote: nil, + binding: nil), + ephemeralKey: Data([0x04, 0x05, 0x06])) + XCTAssertNil(header) + } + + func testSmallNanoTDFSize() throws { + let locator = ResourceLocator(protocolEnum: .http, body: "localhost:8080") + XCTAssertNotNil(locator) + let nanoTDF = initializeSmallNanoTDF(kasResourceLocator: locator!) + let data = nanoTDF.toData() + print("data.count", data.count) + XCTAssertLessThan(data.count, 240) + } + + func testSmallNanoTDFPerformance() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } +} diff --git a/Tests/NanoTDFCreationTests.swift b/Tests/NanoTDFCreationTests.swift new file mode 100644 index 0000000..c96c07b --- /dev/null +++ b/Tests/NanoTDFCreationTests.swift @@ -0,0 +1,66 @@ +import XCTest +import CryptoKit +@testable import NanoTDF + +class NanoTDFCreationTests: XCTestCase { + func testCreateNanoTDF() throws { + let kasRL = ResourceLocator(protocolEnum: .http, body: "localhost:8080") + XCTAssertNotNil(kasRL) + let recipientBase64 = "A2ifhGOpE0DjR4R0FPXvZ6YBOrcjayIpxwtxeXTudOts" + guard let recipientDER = Data(base64Encoded: recipientBase64) else { + throw NSError(domain: "invalid base64 encoding", code: 0, userInfo: nil) + } + let kasPK = try P256.KeyAgreement.PublicKey(compressedRepresentation: recipientDER) + let kasMetadata = KasMetadata(resourceLocator: kasRL!, publicKey: kasPK, curve: .secp256r1) +// let policyBody = "classification:secret".data(using: .utf8)! +// let embeddedPolicy = EmbeddedPolicyBody(length: policyBody.count, body: policyBody, keyAccess: nil) + let remotePolicy = ResourceLocator(protocolEnum: .https, body: "localhost/123") + var policy = Policy(type: .remote, body: nil, remote: remotePolicy, binding: nil) + let plaintext = "Keep this message secret".data(using: .utf8)! + // create + let nanoTDF = try createNanoTDF(kas: kasMetadata, policy: &policy, plaintext: plaintext) + XCTAssertNotNil(nanoTDF, "NanoTDF should not be nil") + XCTAssertNotNil(nanoTDF.header, "Header should not be nil") + XCTAssertNotNil(nanoTDF.header.policy.remote, "Policy body should not be nil") + XCTAssertNotNil(nanoTDF.header.ephemeralPublicKey, "Ephemeral PublicKey should not be nil") + XCTAssertNotNil(nanoTDF.payload, "Payload should not be nil") + XCTAssertNotNil(nanoTDF.payload.iv, "Payload nonce should not be nil") + XCTAssertNotNil(nanoTDF.payload.ciphertext, "Payload ciphertext should not be nil") + XCTAssertEqual(nanoTDF.payload.length, 43) + print(nanoTDF) + // round trip - serialize + let serializedData = nanoTDF.toData() + var counter = 0 + let serializedHexString = serializedData.map { byte -> String in + counter += 1 + let newline = counter % 20 == 0 ? "\n" : " " + return String(format: "%02x", byte) + newline + }.joined() + print("Created:") + print(serializedHexString) + // round trip - parse + let parser = BinaryParser(data: serializedData) + let header = try parser.parseHeader() + print("Parsed Header:", header) + let pheader = header.toData() + counter = 0 + let pheaderHexString = pheader.map { byte -> String in + counter += 1 + let newline = counter % 20 == 0 ? "\n" : " " + return String(format: "%02x", byte) + newline + }.joined() + print("Parsed Header:") + print(pheaderHexString) + // Policy + let policyHexString = header.policy.toData().map { String(format: "%02x", $0) }.joined(separator: " ") + print("Policy:", policyHexString) + // Ephemeral Key + let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") + print("Ephemeral Key:", ephemeralKeyHexString) + let payload = try parser.parsePayload(config: header.payloadSignatureConfig) + let snanoTDF = NanoTDF(header: header, payload: payload, signature: nil) + // Print final the signature NanoTDF + print(snanoTDF) + XCTAssertEqual(payload.length, 43) + } +} diff --git a/Tests/NanoTDFTests.swift b/Tests/NanoTDFTests.swift index aeb9cf8..4299000 100644 --- a/Tests/NanoTDFTests.swift +++ b/Tests/NanoTDFTests.swift @@ -42,16 +42,16 @@ final class NanoTDFTests: XCTestCase { print("Parsed Header:", header) // KAS print("KAS:", header.kas.body) - if "kas.virtru.com" != header.kas.body { + if header.kas.body != "kas.virtru.com" { XCTFail("") } // Ephemeral Key - let ephemeralKeyHexString = header.ephemeralKey.map { String(format: "%02x", $0) }.joined(separator: " ") + let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") print("Ephemeral Key:", ephemeralKeyHexString) let compareHexString = """ - 02 f7 7f ba e5 26 09 da c5 e8 eb f7 86 e1 1b 7a ed d7 0f 89 - 80 f9 48 0c 7e 67 1c ba ab 8e 24 50 92 - """.replacingOccurrences(of: "\n", with: " ") + 02 f7 7f ba e5 26 09 da c5 e8 eb f7 86 e1 1b 7a ed d7 0f 89 + 80 f9 48 0c 7e 67 1c ba ab 8e 24 50 92 + """.replacingOccurrences(of: "\n", with: " ") if ephemeralKeyHexString == compareHexString { print("Ephemeral Key equals comparison string.") } else { @@ -84,7 +84,7 @@ final class NanoTDFTests: XCTestCase { let parser = BinaryParser(data: binaryData!) let header = try parser.parseHeader() // EccMode - let serializedEccMode = header.eccMode.toData() + let serializedEccMode = header.policyBindingConfig.toData() let serializedEccModeHexString = serializedEccMode.map { String(format: "%02x", $0) }.joined(separator: " ") var compareHexString = """ 80 @@ -95,7 +95,7 @@ final class NanoTDFTests: XCTestCase { print(serializedEccModeHexString) XCTFail("EccMode does not equal comparison string.") } - let payload = try parser.parsePayload(config: header.payloadSigMode) + let payload = try parser.parsePayload(config: header.payloadSignatureConfig) let serializedPayloadMacHexString = payload.mac.map { String(format: "%02x", $0) }.joined(separator: " ") compareHexString = """ f9 fd 80 14 af 7c cb 06 @@ -106,7 +106,7 @@ final class NanoTDFTests: XCTestCase { print(serializedPayloadMacHexString) XCTFail("MAC does not equal comparison string.") } - let signature = try parser.parseSignature(config: header.payloadSigMode) + let signature = try parser.parseSignature(config: header.payloadSignatureConfig) let nano = NanoTDF(header: header, payload: payload, signature: signature) let serializedNanoTDF = nano.toData() var counter = 0 @@ -125,13 +125,13 @@ final class NanoTDFTests: XCTestCase { // back again let bparser = BinaryParser(data: serializedNanoTDF) let bheader = try bparser.parseHeader() - _ = try bparser.parsePayload(config: bheader.payloadSigMode) - _ = try bparser.parseSignature(config: bheader.payloadSigMode) + _ = try bparser.parsePayload(config: bheader.payloadSignatureConfig) + _ = try bparser.parseSignature(config: bheader.payloadSignatureConfig) } catch { XCTFail("Failed to parse data: \(error)") } } - + // 6.1.5 nanotdf func testSpecExampleDecryptPayload() { let hexString = """ @@ -154,10 +154,10 @@ final class NanoTDFTests: XCTestCase { do { let header = try parser.parseHeader() // Ephemeral Key - let ephemeralKeyHexString = header.ephemeralKey.map { String(format: "%02x", $0) }.joined(separator: " ") + let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") print("Ephemeral Key:", ephemeralKeyHexString) // Parse payload - let payload = try parser.parsePayload(config: header.payloadSigMode) + let payload = try parser.parsePayload(config: header.payloadSignatureConfig) let payloadIvHexString = payload.iv.map { String(format: "%02x", $0) }.joined(separator: " ") print("Payload IV:", payloadIvHexString) var compareHexString = """ @@ -180,14 +180,14 @@ final class NanoTDFTests: XCTestCase { } // Create the symmetric key _ = SymmetricKey(size: .bits256) - + // Combine the IV-nonce, ciphertext, and MAC-tag let _ = payload.iv + payload.ciphertext + payload.mac - + // Decrypt the payload // let sealedBox = try AES.GCM.SealedBox(combined: combinedData) // let decryptedData = try AES.GCM.open(sealedBox, using: symmetricKey) -// +// // // Assert the decrypted data is as expected // let expectedDecryptedData = "DON'T" // XCTAssertEqual(decryptedData, decryptedData) @@ -195,7 +195,7 @@ final class NanoTDFTests: XCTestCase { XCTFail("Decryption failed: \(error)") } } - + // 6.2 No Signature Example func testNoSignatureSpecExampleBinaryParser() throws { let hexString = """ @@ -223,19 +223,19 @@ final class NanoTDFTests: XCTestCase { print("Version Hex:", versionHexString) // KAS print("KAS:", header.kas.body) - if "kas.example.com" != header.kas.body { + if header.kas.body != "kas.example.com" { XCTFail("KAS incorrect") } - if "kas.example.com/policy/abcdef" != header.policy.remote?.body { + if header.policy.remote?.body != "kas.example.com/policy/abcdef" { XCTFail("Policy Body incorrect") } // Ephemeral Key - let ephemeralKeyHexString = header.ephemeralKey.map { String(format: "%02x", $0) }.joined(separator: " ") + let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") print("Ephemeral Key:", ephemeralKeyHexString) let compareHexString = """ - 03 e8 b3 3f 44 9a 73 92 77 13 d4 a4 a2 b4 e5 e9 45 2e 2f 05 - 34 33 9d 35 91 1b df a1 5e e1 8b 3a db - """.replacingOccurrences(of: "\n", with: " ") + 03 e8 b3 3f 44 9a 73 92 77 13 d4 a4 a2 b4 e5 e9 45 2e 2f 05 + 34 33 9d 35 91 1b df a1 5e e1 8b 3a db + """.replacingOccurrences(of: "\n", with: " ") print("Compare Key:", compareHexString) if ephemeralKeyHexString == compareHexString { print("Ephemeral Key equals comparison string.") @@ -243,24 +243,24 @@ final class NanoTDFTests: XCTestCase { XCTFail("Ephemeral Key does not equal comparison string.") } // ECC and Binding Mode - if header.eccMode.toData() == Data([0x80]) { + if header.policyBindingConfig.toData() == Data([0x80]) { print("EccMode equals comparison.") } else { XCTFail("EccMode does not equal comparison.") } // Symmetric and Payload Config - if header.payloadSigMode.toData() == Data([0x35]) { + if header.payloadSignatureConfig.toData() == Data([0x35]) { print("SigMode equals comparison.") } else { XCTFail("SigMode does not equal comparison.") } // Signature - XCTAssertFalse(header.payloadSigMode.hasSignature) + XCTAssertFalse(header.payloadSignatureConfig.signed) } catch { XCTFail("Failed to parse data: \(error)") } } - + func testNoPolicyBinaryParser() throws { let stringWithSpaces = """ 4c 31 4c 01 1a 70 6c 61 74 66 6f 72 6d 2e 76 69 72 74 72 75 @@ -306,13 +306,13 @@ final class NanoTDFTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - let payload = try parser.parsePayload(config: header.payloadSigMode) + let payload = try parser.parsePayload(config: header.payloadSignatureConfig) var nanoTDF = NanoTDF(header: header, payload: payload, signature: nil) // Generate an ECDSA private key let privateKey = P256.Signing.PrivateKey() // Add the signature to the NanoTDF print("Adding signature") - try addSignatureToNanoTDF(nanoTDF: &nanoTDF, privateKey: privateKey, config: header.payloadSigMode) + try addSignatureToNanoTDF(nanoTDF: &nanoTDF, privateKey: privateKey, config: header.payloadSignatureConfig) // Print the updated NanoTDF print(nanoTDF) var serializedSignature = nanoTDF.signature?.toData() @@ -330,8 +330,8 @@ final class NanoTDFTests: XCTestCase { // round trip - parse let sparser = BinaryParser(data: serializedWithSignature) let sheader = try sparser.parseHeader() - let spayload = try sparser.parsePayload(config: sheader.payloadSigMode) - let ssignature = try sparser.parseSignature(config: sheader.payloadSigMode) + let spayload = try sparser.parsePayload(config: sheader.payloadSignatureConfig) + let ssignature = try sparser.parseSignature(config: sheader.payloadSignatureConfig) let snanoTDF = NanoTDF(header: sheader, payload: spayload, signature: ssignature) // Print final the signature NanoTDF print(snanoTDF) @@ -355,19 +355,19 @@ final class NanoTDFTests: XCTestCase { XCTFail("Failed to parse data: \(error)") } } - + func testCreateAddVerifySignature() throws { throw XCTSkip("revise during creation feature") // Create a NanoTDF without a signature - let header = Header(magicNumber: Data([0x00, 0x01]), version: Data([0x01]), kas: ResourceLocator(protocolEnum: .https, body: "example.com"), eccMode: ECCAndBindingMode(useECDSABinding: true, ephemeralECCParamsEnum: .secp256r1), payloadSigMode: SymmetricAndPayloadConfig(hasSignature: false, signatureECCMode: .secp256r1, symmetricCipherEnum: .GCM_128), policy: Policy(type: .embeddedPlaintext, body: Data([0x01, 0x02, 0x03]), remote: nil, binding: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]), keyAccess: nil), ephemeralKey: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20])) + let header = Header(magicNumber: Data([0x00, 0x01]), version: Data([0x01]), kas: ResourceLocator(protocolEnum: .https, body: "example.com")!, eccMode: PolicyBindingConfig(ecdsaBinding: true, curve: .secp256r1), payloadSigMode: SignatureAndPayloadConfig(signed: false, signatureCurve: .secp256r1, payloadCipher: .aes256GCM128), policy: Policy(type: .embeddedPlaintext, body: nil, remote: nil, binding: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])), ephemeralKey: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20])) let payload = Payload(length: 123, iv: Data([0x01, 0x02, 0x03]), ciphertext: Data([0x01, 0x02, 0x03, 0x04, 0x05]), mac: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10])) - var nanoTDF = NanoTDF(header: header, payload: payload, signature: nil) + var nanoTDF = NanoTDF(header: header!, payload: payload, signature: nil) // Generate an ECDSA private key let privateKey = P256.Signing.PrivateKey() // Add the signature to the NanoTDF - try addSignatureToNanoTDF(nanoTDF: &nanoTDF, privateKey: privateKey, config: header.payloadSigMode) + try addSignatureToNanoTDF(nanoTDF: &nanoTDF, privateKey: privateKey, config: header!.payloadSignatureConfig) // Serialize the NanoTDF let serializedData = nanoTDF.toData() @@ -375,17 +375,17 @@ final class NanoTDFTests: XCTestCase { // Parse the NanoTDF let parser = BinaryParser(data: serializedData) let parsedHeader = try parser.parseHeader() - _ = try parser.parsePayload(config: parsedHeader.payloadSigMode) - let signature = try parser.parseSignature(config: parsedHeader.payloadSigMode) - + _ = try parser.parsePayload(config: parsedHeader.payloadSignatureConfig) + let signature = try parser.parseSignature(config: parsedHeader.payloadSignatureConfig) + // Verify the parsed NanoTDF - XCTAssertEqual(parsedHeader.payloadSigMode.hasSignature, true) - XCTAssertEqual(parsedHeader.payloadSigMode.signatureECCMode, .secp256r1) - XCTAssertEqual(parsedHeader.payloadSigMode.symmetricCipherEnum, .GCM_128) + XCTAssertEqual(parsedHeader.payloadSignatureConfig.signed, true) + XCTAssertEqual(parsedHeader.payloadSignatureConfig.signatureCurve, .secp256r1) + XCTAssertEqual(parsedHeader.payloadSignatureConfig.payloadCipher, .aes256GCM128) XCTAssertEqual(signature?.publicKey, privateKey.publicKey.rawRepresentation) XCTAssertEqual(signature?.signature, nanoTDF.signature?.signature) } - + func testPerformanceExample() throws { // This is an example of a performance test case. measure { @@ -403,7 +403,7 @@ final class NanoTDFTests: XCTestCase { // wait Thread.sleep(forTimeInterval: 2.0) // Optionally, disconnect when done or needed - webSocketManager.disconnect() + webSocketManager.disconnect() } } @@ -426,78 +426,76 @@ extension Data { } class SymmetricAndPayloadConfigTests: XCTestCase { - func testToDataWithSignature() { - let config = SymmetricAndPayloadConfig(hasSignature: true, signatureECCMode: .secp256r1, symmetricCipherEnum: .GCM_128) + let config = SignatureAndPayloadConfig(signed: true, signatureCurve: .secp256r1, payloadCipher: .aes256GCM128) let data = config.toData() XCTAssertEqual(data.count, 1) - XCTAssertEqual(data[0], 0b10000101) // 0b10000000 (HAS_SIGNATURE) | 0b00000000 (Signature ECC Mode secp256r1) | 0b00000101 (Symmetric Cipher Enum GCM_128) + XCTAssertEqual(data[0], 0b1000_0101) // 0b10000000 (HAS_SIGNATURE) | 0b00000000 (Signature ECC Mode secp256r1) | 0b00000101 (Symmetric Cipher Enum GCM_128) } func testToDataWithoutSignature() { - let config = SymmetricAndPayloadConfig(hasSignature: false, signatureECCMode: nil, symmetricCipherEnum: .GCM_128) + let config = SignatureAndPayloadConfig(signed: false, signatureCurve: nil, payloadCipher: .aes256GCM128) let data = config.toData() XCTAssertEqual(data.count, 1) - XCTAssertEqual(data[0], 0b00000101) // 0b00000101 (Symmetric Cipher Enum GCM_128) + XCTAssertEqual(data[0], 0b0000_0101) // 0b00000101 (Symmetric Cipher Enum GCM_128) } func testParseWithSignature() { - let data = Data([0b10000101]) - let config = SymmetricAndPayloadConfig.parse(from: data) + let data = Data([0b1000_0101]) + let config = SignatureAndPayloadConfig.parse(from: data) XCTAssertNotNil(config) - XCTAssertEqual(config?.hasSignature, true) - XCTAssertEqual(config?.signatureECCMode, .secp256r1) - XCTAssertEqual(config?.symmetricCipherEnum, .GCM_128) + XCTAssertEqual(config?.signed, true) + XCTAssertEqual(config?.signatureCurve, .secp256r1) + XCTAssertEqual(config?.payloadCipher, .aes256GCM128) } func testParseWithoutSignature() { - let data = Data([0b00000101]) - let config = SymmetricAndPayloadConfig.parse(from: data) + let data = Data([0b0000_0101]) + let config = SignatureAndPayloadConfig.parse(from: data) XCTAssertNotNil(config) - XCTAssertEqual(config?.hasSignature, false) - XCTAssertNil(config?.signatureECCMode) - XCTAssertEqual(config?.symmetricCipherEnum, .GCM_128) + XCTAssertEqual(config?.signed, false) + XCTAssertNil(config?.signatureCurve) + XCTAssertEqual(config?.payloadCipher, .aes256GCM128) } func testParseInvalidData() { - let data = Data([0b11111111]) // Invalid combination - let config = SymmetricAndPayloadConfig.parse(from: data) + let data = Data([0b1111_1111]) // Invalid combination + let config = SignatureAndPayloadConfig.parse(from: data) XCTAssertNil(config) } } // Extend SymmetricAndPayloadConfig to include a parsing method -extension SymmetricAndPayloadConfig { - static func parse(from data: Data) -> SymmetricAndPayloadConfig? { +extension SignatureAndPayloadConfig { + static func parse(from data: Data) -> SignatureAndPayloadConfig? { guard data.count == 1 else { return nil } let byte = data[0] - let hasSignature = (byte & 0b10000000) != 0 - let signatureECCMode = ECDSAParams(rawValue: (byte & 0b01110000) >> 4) - let symmetricCipherEnum = SymmetricCiphers(rawValue: byte & 0b00001111) - + let signed = (byte & 0b1000_0000) != 0 + let signatureECCMode = Curve(rawValue: (byte & 0b0111_0000) >> 4) + let symmetricCipherEnum = Cipher(rawValue: byte & 0b0000_1111) + guard let signatureMode = signatureECCMode, let symmetricCipher = symmetricCipherEnum else { return nil } - if !hasSignature { - return SymmetricAndPayloadConfig(hasSignature: hasSignature, signatureECCMode: nil, symmetricCipherEnum: symmetricCipher) + if !signed { + return SignatureAndPayloadConfig(signed: signed, signatureCurve: nil, payloadCipher: symmetricCipher) } - return SymmetricAndPayloadConfig(hasSignature: hasSignature, signatureECCMode: signatureMode, symmetricCipherEnum: symmetricCipher) + return SignatureAndPayloadConfig(signed: signed, signatureCurve: signatureMode, payloadCipher: symmetricCipher) } } class PayloadTests: XCTestCase { - func testToData() { let payload = Payload(length: 12345, iv: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C]), ciphertext: Data([0x0D, 0x0E, 0x0F, 0x10]), mac: Data([0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20])) let data = payload.toData() - + let expectedData = Data([0x00, 0x30, 0x39]) + - Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C]) + - Data([0x0D, 0x0E, 0x0F, 0x10]) + - Data([0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20]) - + Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C]) + + Data([0x0D, 0x0E, 0x0F, 0x10]) + + Data([0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20]) + XCTAssertEqual(data, expectedData) } @@ -518,7 +516,7 @@ class PayloadTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - let payload = try parser.parsePayload(config: header.payloadSigMode) + let payload = try parser.parsePayload(config: header.payloadSignatureConfig) XCTAssertEqual(payload.iv.count, 3) XCTAssertEqual(payload.mac.count, 16) } catch { @@ -544,7 +542,7 @@ class PayloadTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - let payload = try parser.parsePayload(config: header.payloadSigMode) + let payload = try parser.parsePayload(config: header.payloadSignatureConfig) print(payload) XCTFail("Negative should have failed") } catch { diff --git a/Tests/WebSocketManager.swift b/Tests/WebSocketManager.swift index a914e4c..a3674ec 100644 --- a/Tests/WebSocketManager.swift +++ b/Tests/WebSocketManager.swift @@ -5,8 +5,8 @@ // Created by Paul Flynn on 5/10/24. // -import Foundation import CryptoKit +import Foundation struct PublicKeyMessage: Codable { let messageType: Data @@ -39,13 +39,13 @@ class WebSocketManager { private func receiveMessage() { webSocketTask?.receive { [weak self] result in switch result { - case .failure(let error): + case let .failure(error): print("Failed to receive message: \(error)") - case .success(let message): + case let .success(message): switch message { - case .string(let text): + case let .string(text): print("Received string: \(text)") - case .data(let data): + case let .data(data): print("Received data: \(data)") @unknown default: fatalError() @@ -77,10 +77,9 @@ class WebSocketManager { } } } - + func disconnect() { // Close the WebSocket connection webSocketTask?.cancel(with: .goingAway, reason: nil) } } -