diff --git a/.swiftlint.yml b/.swiftlint.yml index 51d31342..653773dd 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -20,6 +20,7 @@ disabled_rules: - non_optional_string_data_conversion large_tuple: 5 +function_parameter_count: 6 opt_in_rules: - force_unwrapping diff --git a/JOSESwift.xcodeproj/project.pbxproj b/JOSESwift.xcodeproj/project.pbxproj index b3026b6a..3970f3d5 100644 --- a/JOSESwift.xcodeproj/project.pbxproj +++ b/JOSESwift.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 396DD6D1257FB068008353B9 /* ECDHKeyAgreement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396DD6D0257FB068008353B9 /* ECDHKeyAgreement.swift */; }; + 396DD6D5257FB0E8008353B9 /* ECKeyEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396DD6D4257FB0E8008353B9 /* ECKeyEncryption.swift */; }; + 396DD6DF257FB13C008353B9 /* ECKeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396DD6DE257FB13C008353B9 /* ECKeyPair.swift */; }; + 396DD6E3257FB260008353B9 /* ECDHTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396DD6E2257FB260008353B9 /* ECDHTests.swift */; }; + 396DD6E7257FB2B5008353B9 /* JWEECTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396DD6E6257FB2B5008353B9 /* JWEECTests.swift */; }; 5FB760497BB7711EFB470B5A /* ECKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB76A41AECF99F3673C1C40 /* ECKeys.swift */; }; 5FB76093CE81BF1F8E7C254A /* JWKECDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7625CF5EF5D8F2135967F /* JWKECDecodingTests.swift */; }; 5FB7628EC6EA2C4263853DE9 /* DataECPrivateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7604267B94DC5091D7105 /* DataECPrivateKey.swift */; }; @@ -142,6 +147,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 396DD6D0257FB068008353B9 /* ECDHKeyAgreement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ECDHKeyAgreement.swift; sourceTree = ""; }; + 396DD6D4257FB0E8008353B9 /* ECKeyEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ECKeyEncryption.swift; sourceTree = ""; }; + 396DD6DE257FB13C008353B9 /* ECKeyPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ECKeyPair.swift; sourceTree = ""; }; + 396DD6E2257FB260008353B9 /* ECDHTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ECDHTests.swift; sourceTree = ""; }; + 396DD6E6257FB2B5008353B9 /* JWEECTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWEECTests.swift; sourceTree = ""; }; 5FB7604267B94DC5091D7105 /* DataECPrivateKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataECPrivateKey.swift; sourceTree = ""; }; 5FB760DB390F90F91102DB74 /* EC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EC.swift; sourceTree = ""; }; 5FB7625CF5EF5D8F2135967F /* JWKECDecodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWKECDecodingTests.swift; sourceTree = ""; }; @@ -309,6 +319,7 @@ 8A30B8F922118FE6001834E3 /* JWECompressionTests.swift */, 65676D8A1FC220C70031B26D /* JWEDeserializationTests.swift */, C803EFEC1FA8849C00B71335 /* JWERSATests.swift */, + 396DD6E6257FB2B5008353B9 /* JWEECTests.swift */, 65F2558D23FBE6E000A3FC44 /* JWEAESKeyWrapTests.swift */, C803EFEE1FA884C100B71335 /* JWEHeaderTests.swift */, 653365E420ECCB71002630D7 /* JWEDirectEncryptionTests.swift */, @@ -340,6 +351,7 @@ 5FB768A267E1A15571CC58AA /* ECVerifierTests.swift */, 5FB76B5896AAA87ACD56D0D0 /* ECSignerTests.swift */, 65F2558F23FBE75300A3FC44 /* AESKeyWrapTests.swift */, + 396DD6E2257FB260008353B9 /* ECDHTests.swift */, ); name = Crypto; sourceTree = ""; @@ -370,6 +382,7 @@ isa = PBXGroup; children = ( 65617FCC1F90FB7600D8C743 /* RSAKeyEncryptionMode.swift */, + 396DD6D4257FB0E8008353B9 /* ECKeyEncryption.swift */, ); name = KeyEncryption; sourceTree = ""; @@ -599,6 +612,8 @@ 5FB7604267B94DC5091D7105 /* DataECPrivateKey.swift */, 5FB76E4A3A52AAC72B1F33F0 /* SecKeyECPrivateKey.swift */, 612B0C93248E1C83009F1929 /* Thumbprint.swift */, + 396DD6D0257FB068008353B9 /* ECDHKeyAgreement.swift */, + 396DD6DE257FB13C008353B9 /* ECKeyPair.swift */, ); path = CryptoImplementation; sourceTree = ""; @@ -759,6 +774,7 @@ 6501503723FBDB42000D7D0B /* AESKeyWrapKeyManagementModeTests.swift in Sources */, 65684A4D2031935200E56C68 /* RSAPublicKeyToDataTests.swift in Sources */, 6575696D203EF9CE004A0EFD /* JWSValidationTests.swift in Sources */, + 396DD6E3257FB260008353B9 /* ECDHTests.swift in Sources */, 65F2559023FBE75300A3FC44 /* AESKeyWrapTests.swift in Sources */, 65A103A1202B03BB00D22BF5 /* ASN1DERParsingTests.swift in Sources */, 7402BEAF26274DA40012801E /* HMACCryptoTestCase.swift in Sources */, @@ -792,6 +808,7 @@ 65F2558E23FBE6E000A3FC44 /* JWEAESKeyWrapTests.swift in Sources */, 5FB7692ED087062737C589E2 /* SecKeyECPublicKeyTests.swift in Sources */, 5FB76834DC4275C5B7D2F742 /* JWSECTests.swift in Sources */, + 396DD6E7257FB2B5008353B9 /* JWEECTests.swift in Sources */, 5FB76E7A42B44E4F4416CB7F /* JWKECKeysTests.swift in Sources */, 5FB76093CE81BF1F8E7C254A /* JWKECDecodingTests.swift in Sources */, 5FB765B20A256B93440E79E2 /* JWKECEncodingTests.swift in Sources */, @@ -812,6 +829,7 @@ 65617FCD1F90FB7600D8C743 /* RSAKeyEncryptionMode.swift in Sources */, 65617FCB1F90F8C700D8C743 /* Encrypter.swift in Sources */, 65E733CC1FEBE8320009EAC6 /* JWKExtensions.swift in Sources */, + 396DD6DF257FB13C008353B9 /* ECKeyPair.swift in Sources */, 6536560D2035DF8300A3AC3B /* JWKSetCodable.swift in Sources */, 6546FB0F2029DD10002E421F /* SecKeyRSAPublicKey.swift in Sources */, 6572C2F21F96428800D4186D /* Decrypter.swift in Sources */, @@ -838,6 +856,7 @@ C8610F092029B15600859FCC /* Algorithms.swift in Sources */, 658EE1AF23F4067D00B6E967 /* AlgorithmExtensions.swift in Sources */, 65D8E8E820F499EF0059506A /* SymmetricKeys.swift in Sources */, + 396DD6D5257FB0E8008353B9 /* ECKeyEncryption.swift in Sources */, 6558164423F44E8D00EA5FEC /* DirectEncryptionMode.swift in Sources */, 65D1D0651F7A4DB3006377CD /* DataConvertible.swift in Sources */, 6533552E1F8FB61000A660C6 /* JWE.swift in Sources */, @@ -867,6 +886,7 @@ 5FB76B8D7F1214FF01FA1A8B /* ECVerifier.swift in Sources */, 5FB763AFF6BE04E8A6AE4DFC /* DataECPublicKey.swift in Sources */, 5FB7628EC6EA2C4263853DE9 /* DataECPrivateKey.swift in Sources */, + 396DD6D1257FB068008353B9 /* ECDHKeyAgreement.swift in Sources */, 5FB76BA2D0344083B8F79D53 /* SecKeyECPrivateKey.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/JOSESwift/Sources/AESCBCEncryption.swift b/JOSESwift/Sources/AESCBCEncryption.swift index ab716702..d01b6d6a 100644 --- a/JOSESwift/Sources/AESCBCEncryption.swift +++ b/JOSESwift/Sources/AESCBCEncryption.swift @@ -135,9 +135,9 @@ struct AESCBCEncryption { } extension AESCBCEncryption: ContentEncrypter { - func encrypt(header: JWEHeader, payload: Payload) throws -> ContentEncryptionContext { + func encrypt(headerData: Data, payload: Payload) throws -> ContentEncryptionContext { let plaintext = payload.data() - let additionalAuthenticatedData = header.data().base64URLEncodedData() + let additionalAuthenticatedData = headerData.base64URLEncodedData() return try encrypt(plaintext, additionalAuthenticatedData: additionalAuthenticatedData) } diff --git a/JOSESwift/Sources/AESGCMEncryption.swift b/JOSESwift/Sources/AESGCMEncryption.swift index e6c369b4..ef9b0289 100644 --- a/JOSESwift/Sources/AESGCMEncryption.swift +++ b/JOSESwift/Sources/AESGCMEncryption.swift @@ -54,9 +54,9 @@ struct AESGCMEncryption { } extension AESGCMEncryption: ContentEncrypter { - func encrypt(header: JWEHeader, payload: Payload) throws -> ContentEncryptionContext { + func encrypt(headerData: Data, payload: Payload) throws -> ContentEncryptionContext { let plaintext = payload.data() - let additionalAuthenticatedData = header.data().base64URLEncodedData() + let additionalAuthenticatedData = headerData.base64URLEncodedData() return try encrypt(plaintext, additionalAuthenticatedData: additionalAuthenticatedData) } } diff --git a/JOSESwift/Sources/Algorithms.swift b/JOSESwift/Sources/Algorithms.swift index b9becde8..a09f23a6 100644 --- a/JOSESwift/Sources/Algorithms.swift +++ b/JOSESwift/Sources/Algorithms.swift @@ -72,6 +72,36 @@ public enum KeyManagementAlgorithm: String, CaseIterable { case A256KW /// Direct encryption using a shared symmetric key as the content encryption key case direct = "dir" + /// Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF + case ECDH_ES = "ECDH-ES" + /// ECDH-ES using Concat KDF and CEK wrapped with "A128KW" + case ECDH_ES_A128KW = "ECDH-ES+A128KW" + /// ECDH-ES using Concat KDF and CEK wrapped with "A192KW" + case ECDH_ES_A192KW = "ECDH-ES+A192KW" + /// ECDH-ES using Concat KDF and CEK wrapped with "A256KW" + case ECDH_ES_A256KW = "ECDH-ES+A256KW" + + public var keyWrapAlgorithm: KeyManagementAlgorithm? { + switch self { + case .ECDH_ES_A128KW: + return .A128KW + case .ECDH_ES_A192KW: + return .A192KW + case .ECDH_ES_A256KW: + return .A256KW + default: + return nil + } + } + + var shouldContainEphemeralPublicKey: Bool { + switch self { + case .ECDH_ES, .ECDH_ES_A128KW, .ECDH_ES_A192KW, .ECDH_ES_A256KW: + return true + default: + return false + } + } } /// Cryptographic algorithms for content encryption. @@ -86,6 +116,26 @@ public enum ContentEncryptionAlgorithm: String { case A256GCM = "A256GCM" /// Content encryption using AES GCM with 128-bit key case A128GCM = "A128GCM" + + var keyBitSize: Int { + switch self { + case .A128GCM: + return 128 + case .A128CBCHS256, .A256GCM: + return 256 + case .A256CBCHS512: + return 512 + } + } + + var tagLength: Int { + switch self { + case .A128CBCHS256, .A128GCM, .A256GCM: + return 16 + case .A256CBCHS512: + return 32 + } + } } /// An algorithm for HMAC calculation. diff --git a/JOSESwift/Sources/ContentEncryption.swift b/JOSESwift/Sources/ContentEncryption.swift index c71237c0..b8d2269d 100644 --- a/JOSESwift/Sources/ContentEncryption.swift +++ b/JOSESwift/Sources/ContentEncryption.swift @@ -37,7 +37,7 @@ struct ContentDecryptionContext { } protocol ContentEncrypter { - func encrypt(header: JWEHeader, payload: Payload) throws -> ContentEncryptionContext + func encrypt(headerData: Data, payload: Payload) throws -> ContentEncryptionContext } protocol ContentDecrypter { diff --git a/JOSESwift/Sources/CryptoImplementation/AES.swift b/JOSESwift/Sources/CryptoImplementation/AES.swift index 391b4d49..f8573ca4 100644 --- a/JOSESwift/Sources/CryptoImplementation/AES.swift +++ b/JOSESwift/Sources/CryptoImplementation/AES.swift @@ -54,7 +54,7 @@ fileprivate extension ContentEncryptionAlgorithm { } } -fileprivate extension KeyManagementAlgorithm { +extension KeyManagementAlgorithm { func checkAESKeyLength(for key: Data) -> Bool? { switch self { case .A128KW: @@ -67,6 +67,19 @@ fileprivate extension KeyManagementAlgorithm { return nil } } + + var keyBitSize: Int? { + switch self { + case .A128KW: + return 128 + case .A192KW: + return 192 + case .A256KW: + return 256 + default: + return nil + } + } } enum AES { @@ -180,11 +193,7 @@ enum AES { throw AESError.keyLengthNotSatisfied } - // See https://tools.ietf.org/html/rfc3394#section-2.2.3.1 - // The default iv is defined to be the hexadecimal constant A6A6A6A6A6A6A6A6 - let iv = Data(bytes: CCrfc3394_iv, count: CCrfc3394_ivLen) - - let wrapped = ccAESKeyWrap(rawKey: rawKey, keyEncryptionKey: keyEncryptionKey, iv: iv) + let wrapped = ccAESKeyWrap(rawKey: rawKey, keyEncryptionKey: keyEncryptionKey) guard let wrappedKey = wrapped.data, wrapped.status == kCCSuccess else { throw AESError.encryptingFailed(description: "Key wrap failed with status: \(wrapped.status).") @@ -209,11 +218,7 @@ enum AES { throw AESError.keyLengthNotSatisfied } - // See https://tools.ietf.org/html/rfc3394#section-2.2.3.1 - // The default iv is defined to be the hexadecimal constant A6A6A6A6A6A6A6A6 - let iv = Data(bytes: CCrfc3394_iv, count: CCrfc3394_ivLen) - - let unwrapped = ccAESKeyUnwrap(wrappedKey: wrappedKey, keyEncryptionKey: keyEncryptionKey, iv: iv) + let unwrapped = ccAESKeyUnwrap(wrappedKey: wrappedKey, keyEncryptionKey: keyEncryptionKey) guard let unwrappedKey = unwrapped.data, unwrapped.status == kCCSuccess else { throw AESError.decryptingFailed(description: "Key unwrap failed with status: \(unwrapped.status).") @@ -224,7 +229,6 @@ enum AES { } extension AES { - // swiftlint:disable:next function_parameter_count private static func ccAESCBCCrypt( operation: CCOperation, data: Data, @@ -281,34 +285,30 @@ extension AES { extension AES { private static func ccAESKeyWrap( rawKey: Data, - keyEncryptionKey: Data, - iv: Data - ) -> (data: Data?, status: Int32) { - let alg = CCWrappingAlgorithm(kCCWRAPAES) + keyEncryptionKey: Data) -> (data: Data?, status: Int32) { + let alg = CCWrappingAlgorithm(kCCWRAPAES) var wrappedKeyLength: size_t = CCSymmetricWrappedSize(alg, rawKey.count) var wrappedKey = Data(count: wrappedKeyLength) let status = wrappedKey.withUnsafeMutableBytes { wrappedKeyBytes in rawKey.withUnsafeBytes { rawKeyBytes in - iv.withUnsafeBytes { ivBytes in - keyEncryptionKey.withUnsafeBytes { keyEncryptionKeyBytes -> Int32 in - guard - let wrappedKeyBytes = wrappedKeyBytes.bindMemory(to: UInt8.self).baseAddress, - let rawKeyBytes = rawKeyBytes.bindMemory(to: UInt8.self).baseAddress, - let ivBytes = ivBytes.bindMemory(to: UInt8.self).baseAddress, - let keyEncryptionKeyBytes = keyEncryptionKeyBytes.bindMemory(to: UInt8.self).baseAddress - else { - return Int32(kCCMemoryFailure) - } - return CCSymmetricKeyWrap( - alg, - ivBytes, iv.count, - keyEncryptionKeyBytes, keyEncryptionKey.count, - rawKeyBytes, rawKey.count, - wrappedKeyBytes, &wrappedKeyLength - ) + keyEncryptionKey.withUnsafeBytes { keyEncryptionKeyBytes -> Int32 in + guard + let wrappedKeyBytes = wrappedKeyBytes.bindMemory(to: UInt8.self).baseAddress, + let rawKeyBytes = rawKeyBytes.bindMemory(to: UInt8.self).baseAddress, + let keyEncryptionKeyBytes = keyEncryptionKeyBytes.bindMemory(to: UInt8.self).baseAddress + else { + return Int32(kCCMemoryFailure) } + return CCSymmetricKeyWrap( + alg, + CCrfc3394_iv, + CCrfc3394_ivLen, + keyEncryptionKeyBytes, + keyEncryptionKey.count, + rawKeyBytes, rawKey.count, + wrappedKeyBytes, &wrappedKeyLength) } } } @@ -323,9 +323,8 @@ extension AES { private static func ccAESKeyUnwrap( wrappedKey: Data, - keyEncryptionKey: Data, - iv: Data - ) -> (data: Data?, status: Int32) { + keyEncryptionKey: Data) -> (data: Data?, status: Int32) { + let alg = CCWrappingAlgorithm(kCCWRAPAES) var rawKeyLength: size_t = CCSymmetricUnwrappedSize(alg, wrappedKey.count) @@ -333,30 +332,31 @@ extension AES { let status = rawKey.withUnsafeMutableBytes { rawKeyBytes in wrappedKey.withUnsafeBytes { wrappedKeyBytes in - iv.withUnsafeBytes { ivBytes in - keyEncryptionKey.withUnsafeBytes { keyEncryptionKeyBytes -> Int32 in - guard - let rawKeyBytes = rawKeyBytes.bindMemory(to: UInt8.self).baseAddress, - let wrappedKeyBytes = wrappedKeyBytes.bindMemory(to: UInt8.self).baseAddress, - let ivBytes = ivBytes.bindMemory(to: UInt8.self).baseAddress, - let keyEncryptionKeyBytes = keyEncryptionKeyBytes.bindMemory(to: UInt8.self).baseAddress - else { - return Int32(kCCMemoryFailure) - } - return CCSymmetricKeyUnwrap( - alg, - ivBytes, iv.count, - keyEncryptionKeyBytes, keyEncryptionKey.count, - wrappedKeyBytes, wrappedKey.count, - rawKeyBytes, &rawKeyLength - ) + keyEncryptionKey.withUnsafeBytes { keyEncryptionKeyBytes -> Int32 in + guard + let rawKeyBytes = rawKeyBytes.bindMemory(to: UInt8.self).baseAddress, + let wrappedKeyBytes = wrappedKeyBytes.bindMemory(to: UInt8.self).baseAddress, + let keyEncryptionKeyBytes = keyEncryptionKeyBytes.bindMemory(to: UInt8.self).baseAddress + else { + return Int32(kCCMemoryFailure) } + return CCSymmetricKeyUnwrap( + alg, + CCrfc3394_iv, + CCrfc3394_ivLen, + keyEncryptionKeyBytes, + keyEncryptionKey.count, + wrappedKeyBytes, + wrappedKey.count, + rawKeyBytes, + &rawKeyLength + ) } } } guard status == kCCSuccess else { - return (nil, status) + return (nil, status) // kCCDecodeError } rawKey.removeSubrange(rawKeyLength..? @@ -207,7 +214,7 @@ internal struct EC { throw ECError.invalidCurveDigestAlgorithm } guard let secKeyAlgorithm = algorithm.secKeyAlgorithm else { - throw ECError.algorithmNotSupported + throw ECError.unknownOrUnsupportedAlgorithm } if signature.count != (curveType.coordinateOctetLength * 2) { throw ECError.verifyingFailed(description: "Signature is \(signature.count) bytes long instead of the expected \(curveType.coordinateOctetLength * 2).") @@ -270,4 +277,99 @@ internal struct EC { } } + /// Encrypts a plain text using a given `EC` algorithm and the corresponding public key. + /// + /// - Parameters: + /// - publicKey: The public key. + /// - algorithm: The algorithm used for the key management. + /// - encryption: The algorithm used to encrypt the plain text. + /// - header: The JWE header. + /// - options: The encryption options. + /// - Returns: Encrypted data. + /// - Throws: `EncryptionError` if any errors occur while encrypting the plain text. + static func encryptionContextFor(_ publicKey: PublicKey, + algorithm: KeyManagementAlgorithm, + encryption: ContentEncryptionAlgorithm, + header: JWEHeader, + options: [String: Any] = [:] + ) throws -> Data { + var ephemeralKeyPair: ECKeyPair + if let eKeyPair = options["ephemeralKeyPair"] as? ECKeyPair { + ephemeralKeyPair = eKeyPair + } else { + ephemeralKeyPair = try ECKeyPair.generateWith(publicKey.crv) + } + + let kek = try keyAgreementCompute(with: algorithm, + encryption: encryption, + privateKey: ephemeralKeyPair.getPrivate(), + publicKey: publicKey, + apu: Data(base64URLEncoded: header.apu ?? "") ?? Data(), + apv: Data(base64URLEncoded: header.apv ?? "") ?? Data()) + + var contentKey: Data + var encryptedKey: Data + if let keyWrapAlgorithm = algorithm.keyWrapAlgorithm { + if let injectedKey = options["key"] as? Data { + contentKey = injectedKey + } else { + contentKey = randomBytes(size: encryption.keyBitSize / 8) + } + encryptedKey = try AES.wrap(rawKey: contentKey, keyEncryptionKey: kek, algorithm: keyWrapAlgorithm) + } else { + contentKey = kek + encryptedKey = Data() + } + + var updatedHeader = header + if let epk = updatedHeader.epk, !ephemeralKeyPair.getPrivate().isCorrespondWith(epk) { + updatedHeader.epk = ephemeralKeyPair.getPublic() + } else if updatedHeader.epk == nil { + updatedHeader.epk = ephemeralKeyPair.getPublic() + } + + let context = Encrypter.ECEncryptionContext(headerData: updatedHeader.headerData, + encryptedKey: encryptedKey, + contentKey: contentKey) + let result = try JSONEncoder().encode(context) + return result + } + + /// Decrypts a cipher text using a given `EC` algorithm and the corresponding private key. + /// + /// - Parameters: + /// - encryptedKey: The cipher text to decrypt. + /// - privateKey: The private key. + /// - algorithm: The algorithm used for the key management. + /// - encryption: The algorithm used to decrypt the cipher text. + /// - header: The JWE header. + /// - Returns: The plain text data. + /// - Throws: `EncryptionError` if any errors occur while decrypting the cipher text. + static func decrypt(_ encryptedKey: Data, + privateKey: PrivateKey, + algorithm: KeyManagementAlgorithm, + encryption: ContentEncryptionAlgorithm, + header: JWEHeader? + ) throws -> Data { + + guard let jweHeader = header else { + throw ECError.invalidJWK(reason: "Missing header") + } + + guard let ephemeralPubKey = jweHeader.epk else { + throw ECError.invalidJWK(reason: "missing ephemeral public key in header") + } + + // apu and apv have to be base64URL encoded as described here : https://datatracker.ietf.org/doc/html/rfc7518#page-17 + let apu = Data(base64URLEncoded: jweHeader.apu ?? "") ?? Data() + let apv = Data(base64URLEncoded: jweHeader.apv ?? "") ?? Data() + let kek = try keyAgreementCompute(with: algorithm, encryption: encryption, privateKey: privateKey, publicKey: ephemeralPubKey, apu: apu, apv: apv) + + if let keyWrapAlgorithm = algorithm.keyWrapAlgorithm { + let unwrap = try AES.unwrap(wrappedKey: encryptedKey, keyEncryptionKey: kek, algorithm: keyWrapAlgorithm) + return unwrap + } else { + return kek + } + } } diff --git a/JOSESwift/Sources/CryptoImplementation/ECDHKeyAgreement.swift b/JOSESwift/Sources/CryptoImplementation/ECDHKeyAgreement.swift new file mode 100644 index 00000000..a416957b --- /dev/null +++ b/JOSESwift/Sources/CryptoImplementation/ECDHKeyAgreement.swift @@ -0,0 +1,203 @@ +// +// ECDHKeyAgreement.swift +// JOSESwift +// +// Created by Mikael Rucinsky on 07.12.20. +// + +import Foundation +import CommonCrypto + +/// keyAgreementCompute +/// +/// - Parameters: +/// - algorithm: KeyManagementAlgorithm. +/// - encryption: ContentEncryptionAlgorithm. +/// - privateKey: EC private JWK. +/// - publicKey: EC public JWK. +/// - apu: agreementPartyUInfo. +/// - apv: agreementPartyVInfo. +/// - Returns: Result of key agreement operation as a Data +/// - Throws: `ECError.deriveKeyFail` if any error occurs while derivation. +func keyAgreementCompute(with algorithm: KeyManagementAlgorithm, encryption: ContentEncryptionAlgorithm, privateKey: ECPrivateKey, publicKey: ECPublicKey, apu: Data, apv: Data) throws -> Data { + + let z = try ecdhDeriveBits(for: privateKey, publicKey: publicKey) + var algId: Data + var keyDataLen: Int + if algorithm == .ECDH_ES { + guard let ident = encryption.rawValue.data(using: .utf8) else { + throw ECError.deriveKeyFail(reason: "AlgorithmID Problem - @See Section 5.8.1.2 of [NIST.800-56A]") + } + algId = ident + keyDataLen = encryption.keyBitSize + } else { + guard let ident = algorithm.rawValue.data(using: .utf8) else { + throw ECError.deriveKeyFail(reason: "AlgorithmID Problem - @See Section 5.8.1.2 of [NIST.800-56A]") + } + algId = ident + keyDataLen = algorithm.keyWrapAlgorithm?.keyBitSize ?? 0 + } + let algorithmID = prefixedBigEndenLen(from: algId) + let partyUInfo = prefixedBigEndenLen(from: apu) + let partyVInfo = prefixedBigEndenLen(from: apv) + let suppPubInfo = intToData(value: UInt32(keyDataLen).bigEndian) + return try concatKDF(hash: Hash.SHA256, z: z, keyDataLen: keyDataLen, algorithmID: algorithmID, partyUInfo: partyUInfo, partyVInfo: partyVInfo, suppPubInfo: suppPubInfo) +} + +/// Derive ECDH Key Data +/// +/// - Parameters: +/// - privateKey: EC private JWK. +/// - publicKey: EC public JWK. +/// - bitLen: key size +/// - Returns: Result of key exchange operation as a Data +/// - Throws: `ECError.deriveKeyFail` if any error occurs while derivation. +func ecdhDeriveBits(for privateKey: ECPrivateKey, publicKey: ECPublicKey, bitLen: Int = 0) throws -> Data { + if privateKey.crv != publicKey.crv { + throw ECError.deriveKeyFail(reason: "Private Key curve and Public Key curve are different") + } + let pubKey = try publicKey.converted(to: SecKey.self) + let privKey = try privateKey.converted(to: SecKey.self) + let parameters = [String: Any]() + var error: Unmanaged? + + guard let derivedData = SecKeyCopyKeyExchangeResult(privKey, SecKeyAlgorithm.ecdhKeyExchangeStandard, pubKey, parameters as CFDictionary, &error) else { + let errStr = error?.takeRetainedValue().localizedDescription ?? "Derive Key Fail" + throw ECError.deriveKeyFail(reason: errStr) + } + return bitLen > 0 ? truncateBitLen(from: (derivedData as Data), bitLen: bitLen) : (derivedData as Data) as Data +} + +func truncateBitLen(from: Data, bitLen: Int) -> Data { + if bitLen >= from.count * 8 { + return from + } else if bitLen % 8 == 0 { + return from[0 ..< (bitLen / 8)] + } + let lastPos = Int(bitLen / 8) + var result = from[0 ..< (lastPos + 1)] + result[lastPos] = result[lastPos] & (~(0xFF >> (UInt(bitLen) % 8))) + return result +} + +func prefixedBigEndenLen(from: Data) -> Data { + let prefix = intToData(value: UInt32(from.count).bigEndian) + return prefix + from +} + +func intToData(value: T) -> Data where T: FixedWidthInteger { + var int = value + return Data(bytes: &int, count: MemoryLayout.size) +} + +/// Concat KDF see https://tools.ietf.org/html/rfc7518#section-4.6.2 +/// +/// - Parameters: +/// - hash: HASH algorithm +/// - z: The shared secret Z +/// - keyDataLen: The number of bits in the desired output key. +/// - algorithmID: AlgorithmID @See Section 5.8.1.2 of [NIST.800-56A] +/// - partyUInfo: PartyUInfo @See Section 5.8.1.2 of [NIST.800-56A] +/// - partyVInfo: PartyVInfo @See Section 5.8.1.2 of [NIST.800-56A] +/// - suppPubInfo: SuppPubInfo @See Section 5.8.1.2 of [NIST.800-56A] +/// - suppPrivInfo: SuppPrivInfo @See Section 5.8.1.2 of [NIST.800-56A] +/// - Returns: Derived Keying Material as a Data +/// - Throws: `ECDHError` if any error occurs while derivation. +func concatKDF(hash: Hash, z: Data, keyDataLen: Int, algorithmID: Data, partyUInfo: Data, partyVInfo: Data, suppPubInfo: Data = Data(), suppPrivInfo: Data = Data()) throws -> Data { + if keyDataLen == 0 { + return Data() + } + let modLen = keyDataLen % hash.bitLength + let reps = (keyDataLen / hash.bitLength) + (modLen > 0 ? 1 : 0) + + let concatedData = z + algorithmID + partyUInfo + partyVInfo + suppPubInfo + suppPrivInfo + let hashInputLen = 4 + concatedData.count + guard hashInputLen <= 0xFFFF else { + throw ECError.deriveKeyFail(reason: "Derivation parameter (couter + Z + otherInfor) is more than max HASH input length") + } + + var derivedKeyingMaterial = Data() + for i in 1 ..< reps { + derivedKeyingMaterial += hash.digest(intToData(value: UInt32(i).bigEndian) + concatedData) + } + + if modLen == 0 { + derivedKeyingMaterial += hash.digest(intToData(value: UInt32(reps).bigEndian) + concatedData) + } else { + let digest = hash.digest(intToData(value: UInt32(reps).bigEndian) + concatedData) + derivedKeyingMaterial += truncateBitLen(from: digest, bitLen: modLen) + } + return derivedKeyingMaterial +} + +func randomBytes(size: Int) -> Data { + var bytes = [UInt8](repeating: 0, count: size) + guard errSecSuccess == SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) else { + return Data(count: size) + } + return Data(bytes) +} + +// + +enum Hash: String { + case SHA256 = "SHA-256" + case SHA384 = "SHA-384" + case SHA512 = "SHA-512" + + func digest(_ value: Data) -> Data { + var digestData = [UInt8](repeating: 0, count: digestByteLength) + _ = digestFunc(Array(value), UInt32(value.count), &digestData) + return Data(digestData) + } + + func mac(key: Data, value: Data) -> Data { + var outData = [UInt8](repeating: 0, count: digestByteLength) + CCHmac(ccHmacAlgorithm, Array(key), key.count, Array(value), value.count, &outData) + return Data(outData) + } + + fileprivate var ccHmacAlgorithm: CCHmacAlgorithm { + switch self { + case .SHA256: + return CCHmacAlgorithm(kCCHmacAlgSHA256) + case .SHA384: + return CCHmacAlgorithm(kCCHmacAlgSHA384) + case .SHA512: + return CCHmacAlgorithm(kCCHmacAlgSHA512) + } + } + + fileprivate var digestFunc: (UnsafeRawPointer?, UInt32, UnsafeMutablePointer?) -> UnsafeMutablePointer? { + switch self { + case .SHA256: + return CC_SHA256 + case .SHA384: + return CC_SHA384 + case .SHA512: + return CC_SHA512 + } + } + + var bitLength: Int { + switch self { + case .SHA256: + return 256 + case .SHA384: + return 384 + case .SHA512: + return 512 + } + } + + var digestByteLength: Int { + switch self { + case .SHA256: + return Int(CC_SHA256_DIGEST_LENGTH) + case .SHA384: + return Int(CC_SHA384_DIGEST_LENGTH) + case .SHA512: + return Int(CC_SHA512_DIGEST_LENGTH) + } + } +} diff --git a/JOSESwift/Sources/CryptoImplementation/ECKeyPair.swift b/JOSESwift/Sources/CryptoImplementation/ECKeyPair.swift new file mode 100644 index 00000000..b1dee910 --- /dev/null +++ b/JOSESwift/Sources/CryptoImplementation/ECKeyPair.swift @@ -0,0 +1,51 @@ +// +// ECKeyPair.swift +// JOSESwift +// +// Created by Mikael Rucinsky on 07.12.20. +// + +import Foundation + +// MARK: Key Pair + +public extension ECKeyPair { + + func getPrivate() -> ECPrivateKey { + return self as ECPrivateKey + } + + static func generateWith(_ curveType: ECCurveType) throws -> ECKeyPair { + let attributes: [String: Any] = [kSecAttrKeySizeInBits as String: curveType.keyBitLength, + kSecAttrKeyType as String: kSecAttrKeyTypeEC, + kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false]] + var error: Unmanaged? + if let eckey: SecKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) { + return try ECPrivateKey(privateKey: eckey, additionalParameters: ["kid": UUID().uuidString]) + } + throw ECKeyPairError.generateECKeyPairFail + } +} + +public extension ECPrivateKey { + + func getPublic() -> ECPublicKey { + let parametersForPublic = parameters.filter { $0.key != ECParameter.privateKey.rawValue } + return ECPublicKey(crv: crv, x: x, y: y, additionalParameters: parametersForPublic) + } + + func isCorrespondWith(_ key: ECPublicKey) -> Bool { + guard + crv == key.crv, + x == key.x, + y == key.y + else { + return false + } + return true + } +} + +public enum ECKeyPairError: Error { + case generateECKeyPairFail +} diff --git a/JOSESwift/Sources/Decrypter.swift b/JOSESwift/Sources/Decrypter.swift index 8e298635..fe6e56d4 100644 --- a/JOSESwift/Sources/Decrypter.swift +++ b/JOSESwift/Sources/Decrypter.swift @@ -65,7 +65,14 @@ public struct Decrypter { throw JWEError.contentEncryptionAlgorithmMismatch } - let contentEncryptionKey = try keyManagementMode.determineContentEncryptionKey(from: context.encryptedKey) + var contentEncryptionKey = Data() + + if alg.shouldContainEphemeralPublicKey { + contentEncryptionKey = try keyManagementMode.determineContentEncryptionKey(from: context.encryptedKey, + header: context.protectedHeader) + } else { + contentEncryptionKey = try keyManagementMode.determineContentEncryptionKey(from: context.encryptedKey) + } let contentDecryptionContext = ContentDecryptionContext( ciphertext: context.ciphertext, diff --git a/JOSESwift/Sources/ECKeyEncryption.swift b/JOSESwift/Sources/ECKeyEncryption.swift new file mode 100644 index 00000000..809364aa --- /dev/null +++ b/JOSESwift/Sources/ECKeyEncryption.swift @@ -0,0 +1,83 @@ +// +// ECKeyEncryption.swift +// JOSESwift +// +// Created by Mikael Rucinsky on 07.12.20. +// + +import Foundation + +/// recipient using an asymmetric ECDH encryption algorithm. The resulting ciphertext is the JWE encrypted key. +enum ECKeyEncryption { + typealias KeyType = EC.KeyType + typealias PrivateKey = EC.PrivateKey + typealias PublicKey = EC.PublicKey + + struct EncryptionMode { + private let keyManagementAlgorithm: KeyManagementAlgorithm + private let contentEncryptionAlgorithm: ContentEncryptionAlgorithm + private let recipientPublicKey: PublicKey + private let agreementPartyUInfo: Data + private let agreementPartyVInfo: Data + private let options: [String: Any] + + init( + keyManagementAlgorithm: KeyManagementAlgorithm, + contentEncryptionAlgorithm: ContentEncryptionAlgorithm, + recipientPublicKey: PublicKey, + agreementPartyUInfo: Data, + agreementPartyVInfo: Data, + options: [String: Any] = [:] + ) { + self.keyManagementAlgorithm = keyManagementAlgorithm + self.contentEncryptionAlgorithm = contentEncryptionAlgorithm + self.recipientPublicKey = recipientPublicKey + self.agreementPartyUInfo = agreementPartyUInfo + self.agreementPartyVInfo = agreementPartyVInfo + self.options = options + } + } + + struct DecryptionMode { + private let keyManagementAlgorithm: KeyManagementAlgorithm + private let contentEncryptionAlgorithm: ContentEncryptionAlgorithm + private let recipientPrivateKey: PrivateKey + + init( + keyManagementAlgorithm: KeyManagementAlgorithm, + contentEncryptionAlgorithm: ContentEncryptionAlgorithm, + recipientPrivateKey: PrivateKey + ) { + self.keyManagementAlgorithm = keyManagementAlgorithm + self.contentEncryptionAlgorithm = contentEncryptionAlgorithm + self.recipientPrivateKey = recipientPrivateKey + } + } +} + +extension ECKeyEncryption.EncryptionMode: EncryptionKeyManagementMode { + func determineContentEncryptionKey(for header: JWEHeader) throws -> Data { + + return try EC.encryptionContextFor(recipientPublicKey, + algorithm: keyManagementAlgorithm, + encryption: contentEncryptionAlgorithm, + header: header, + options: options) + } +} + +extension ECKeyEncryption.DecryptionMode: DecryptionKeyManagementMode { + func determineContentEncryptionKey(from encryptedKey: Data, header: JWEHeader) throws -> Data { + + let randomContentEncryptionKey = try SecureRandom.generate(count: contentEncryptionAlgorithm.keyLength) + + let decryptedKey = try EC.decrypt(encryptedKey, + privateKey: recipientPrivateKey, + algorithm: keyManagementAlgorithm, + encryption: contentEncryptionAlgorithm, + header: header) + + guard decryptedKey.count == contentEncryptionAlgorithm.keyLength else { return randomContentEncryptionKey } + return decryptedKey + } +} diff --git a/JOSESwift/Sources/Encrypter.swift b/JOSESwift/Sources/Encrypter.swift index 307327c6..e7e33871 100644 --- a/JOSESwift/Sources/Encrypter.swift +++ b/JOSESwift/Sources/Encrypter.swift @@ -23,8 +23,7 @@ import Foundation -// Todo [#214]: Move generic type to initializer in next major release. -public struct Encrypter { +public struct Encrypter { private let keyManagementMode: EncryptionKeyManagementMode private let keyManagementAlgorithm: KeyManagementAlgorithm private let contentEncryptionAlgorithm: ContentEncryptionAlgorithm @@ -40,23 +39,27 @@ public struct Encrypter { /// encrypted. /// - For _direct encryption_ it is the secret symmetric key (`Data`) shared between the sender and the /// recipient. - public init?( + public init?( keyManagementAlgorithm: KeyManagementAlgorithm, contentEncryptionAlgorithm: ContentEncryptionAlgorithm, - encryptionKey: KeyType + encryptionKey: KeyType, + agreementPartyUInfo: Data? = nil, + agreementPartyVInfo: Data? = nil ) { self.keyManagementAlgorithm = keyManagementAlgorithm self.contentEncryptionAlgorithm = contentEncryptionAlgorithm let mode = keyManagementAlgorithm.makeEncryptionKeyManagementMode( contentEncryptionAlgorithm: contentEncryptionAlgorithm, - encryptionKey: encryptionKey + encryptionKey: encryptionKey, + agreementPartyUInfo: agreementPartyUInfo, + agreementPartyVInfo: agreementPartyVInfo ) guard let keyManagementMode = mode else { return nil } self.keyManagementMode = keyManagementMode } - func encrypt(header: JWEHeader, payload: Payload) throws -> EncryptionContext { + func encrypt(header: inout JWEHeader, payload: Payload) throws -> EncryptionContext { guard let alg = header.keyManagementAlgorithm, alg == keyManagementAlgorithm else { throw JWEError.keyManagementAlgorithmMismatch } @@ -65,18 +68,43 @@ public struct Encrypter { throw JWEError.contentEncryptionAlgorithmMismatch } - let (contentEncryptionKey, encryptedKey) = try keyManagementMode.determineContentEncryptionKey() + if keyManagementAlgorithm.shouldContainEphemeralPublicKey { + let encryptedKey = try keyManagementMode.determineContentEncryptionKey(for: header) - let contentEncryptionContext = try contentEncryptionAlgorithm - .makeContentEncrypter(contentEncryptionKey: contentEncryptionKey) - .encrypt(header: header, payload: payload) + guard let context = try? JSONDecoder().decode(Encrypter.ECEncryptionContext.self, from: encryptedKey) else { + throw JWEError.hmacNotAuthenticated + } - return EncryptionContext( - encryptedKey: encryptedKey, - ciphertext: contentEncryptionContext.ciphertext, - authenticationTag: contentEncryptionContext.authenticationTag, - initializationVector: contentEncryptionContext.initializationVector - ) + if let contextHeader = JWEHeader(context.headerData) { + header = contextHeader + } + + let contentEncryptionContext = try contentEncryptionAlgorithm + .makeContentEncrypter(contentEncryptionKey: context.contentKey) + .encrypt(headerData: context.headerData, + payload: payload) + + return EncryptionContext( + encryptedKey: context.encryptedKey, + ciphertext: contentEncryptionContext.ciphertext, + authenticationTag: contentEncryptionContext.authenticationTag, + initializationVector: contentEncryptionContext.initializationVector + ) + } else { + let (contentEncryptionKey, encryptedKey) = try keyManagementMode.determineContentEncryptionKey() + + let contentEncryptionContext = try contentEncryptionAlgorithm + .makeContentEncrypter(contentEncryptionKey: contentEncryptionKey) + .encrypt(headerData: header.data(), + payload: payload) + + return EncryptionContext( + encryptedKey: encryptedKey, + ciphertext: contentEncryptionContext.ciphertext, + authenticationTag: contentEncryptionContext.authenticationTag, + initializationVector: contentEncryptionContext.initializationVector + ) + } } } @@ -87,18 +115,24 @@ extension Encrypter { let authenticationTag: Data let initializationVector: Data } + + struct ECEncryptionContext: Codable { + let headerData: Data + let encryptedKey: Data + let contentKey: Data + } } // MARK: - Deprecated API extension Encrypter { @available(*, deprecated, message: "Use `init?(keyManagementAlgorithm:contentEncryptionAlgorithm:encryptionKey:)` instead") - public init?(keyEncryptionAlgorithm: AsymmetricKeyAlgorithm, encryptionKey key: KeyType, contentEncyptionAlgorithm: SymmetricKeyAlgorithm) { + public init?(keyEncryptionAlgorithm: AsymmetricKeyAlgorithm, encryptionKey key: KeyType, contentEncyptionAlgorithm: SymmetricKeyAlgorithm) { self.init(keyManagementAlgorithm: keyEncryptionAlgorithm, contentEncryptionAlgorithm: contentEncyptionAlgorithm, encryptionKey: key) } @available(*, deprecated, message: "Use `init?(keyManagementAlgorithm:contentEncryptionAlgorithm:encryptionKey:)` instead") - public init?(keyEncryptionAlgorithm: AsymmetricKeyAlgorithm, keyEncryptionKey kek: KeyType, contentEncyptionAlgorithm: SymmetricKeyAlgorithm) { + public init?(keyEncryptionAlgorithm: AsymmetricKeyAlgorithm, keyEncryptionKey kek: KeyType, contentEncyptionAlgorithm: SymmetricKeyAlgorithm) { self.init(keyEncryptionAlgorithm: keyEncryptionAlgorithm, encryptionKey: kek, contentEncyptionAlgorithm: contentEncyptionAlgorithm) } } diff --git a/JOSESwift/Sources/JWE.swift b/JOSESwift/Sources/JWE.swift index 9e5c7c14..8d6b3775 100644 --- a/JOSESwift/Sources/JWE.swift +++ b/JOSESwift/Sources/JWE.swift @@ -37,7 +37,7 @@ internal enum JWEError: Error { /// As discussed, it is the responsibility of the framework user to cache e.g. the plaintext. Of course this will have to be communicated clearly. public struct JWE { /// The JWE's JOSE Header. - public let header: JWEHeader + public var header: JWEHeader /// The encrypted content encryption key (CEK). public let encryptedKey: Data @@ -70,12 +70,12 @@ public struct JWE { /// - payload: A fully initialized `Payload`. /// - encrypter: The `Encrypter` used to encrypt the JWE from the header and payload. /// - Throws: `JOSESwiftError` if any error occurs while encrypting. - public init(header: JWEHeader, payload: Payload, encrypter: Encrypter) throws { + public init(header: JWEHeader, payload: Payload, encrypter: Encrypter) throws { self.header = header - var encryptionContext: Encrypter.EncryptionContext + var encryptionContext: Encrypter.EncryptionContext do { - encryptionContext = try encrypter.encrypt(header: header, payload: payload.compressed(using: header.compressionAlgorithm)) + encryptionContext = try encrypter.encrypt(header: &self.header, payload: payload.compressed(using: header.compressionAlgorithm)) } catch JOSESwiftError.compressionFailed { throw JOSESwiftError.compressionFailed } catch JOSESwiftError.compressionAlgorithmNotSupported { diff --git a/JOSESwift/Sources/JWEHeader.swift b/JOSESwift/Sources/JWEHeader.swift index bc8c9752..d41eeb97 100644 --- a/JOSESwift/Sources/JWEHeader.swift +++ b/JOSESwift/Sources/JWEHeader.swift @@ -83,6 +83,31 @@ public struct JWEHeader: JOSEHeader { try! self.init(parameters: parameters) } + /// Initializes a `JWEHeader` with the specified algorithm, signing algorithm, agreementPartyUInfo, agreementPartyVInfo and ephemeralPublicKey + public init(keyManagementAlgorithm: KeyManagementAlgorithm, + contentEncryptionAlgorithm: ContentEncryptionAlgorithm, + agreementPartyUInfo: String, + agreementPartyVInfo: String, + ephemeralPublicKey: ECPublicKey + ) { + + let parameters = [ + "alg": keyManagementAlgorithm.rawValue, + "enc": contentEncryptionAlgorithm.rawValue, + "apu": agreementPartyUInfo, + "apv": agreementPartyVInfo, + "epk": ephemeralPublicKey.parameters + ] as [String: Any] + + // Forcing the try is ok here, since [String: String] can be converted to JSON. + // swiftlint:disable:next force_try + let headerData = try! JSONSerialization.data(withJSONObject: parameters, options: []) + + // Forcing the try is ok here, since "alg" and "enc" are the only required header parameters. + // swiftlint:disable:next force_try + try! self.init(parameters: parameters, headerData: headerData) + } + /// Initializes a `JWEHeader` with the specified parameters. public init(parameters: [String: Any]) throws { try self.init( @@ -275,6 +300,44 @@ extension JWEHeader: CommonHeaderParameterSpace { parameters["crit"] = newValue } } + + /// Header Parameters Used for ECDH Key Agreement - Ephemeral Public Key + public var epk: ECPublicKey? { + get { + guard let jwkParameters = parameters["epk"] as? [String: String] else { + return nil + } + + guard let json = try? JSONEncoder().encode(jwkParameters) else { + return nil + } + + return try? ECPublicKey(data: json) + } + set { + parameters["epk"] = newValue?.parameters + } + } + + /// Header Parameters Used for ECDH Key Agreement - Agreement PartyUInfo + public var apu: String? { + get { + return parameters["apu"] as? String + } + set { + parameters["apu"] = newValue + } + } + + /// Header Parameters Used for ECDH Key Agreement - Agreement PartyVInfo + public var apv: String? { + get { + return parameters["apv"] as? String + } + set { + parameters["apv"] = newValue + } + } } // MARK: - Deprecated API diff --git a/JOSESwift/Sources/KeyManagementMode.swift b/JOSESwift/Sources/KeyManagementMode.swift index cf3c1e1f..872b8e2a 100644 --- a/JOSESwift/Sources/KeyManagementMode.swift +++ b/JOSESwift/Sources/KeyManagementMode.swift @@ -25,16 +25,30 @@ import Foundation protocol EncryptionKeyManagementMode { func determineContentEncryptionKey() throws -> (contentEncryptionKey: Data, encryptedKey: Data) + func determineContentEncryptionKey(for header: JWEHeader) throws -> Data +} + +extension EncryptionKeyManagementMode { + func determineContentEncryptionKey() throws -> (contentEncryptionKey: Data, encryptedKey: Data) { return (Data(), Data()) } + func determineContentEncryptionKey(for header: JWEHeader) throws -> Data { return Data() } } protocol DecryptionKeyManagementMode { func determineContentEncryptionKey(from encryptedKey: Data) throws -> Data + func determineContentEncryptionKey(from encryptedKey: Data, header: JWEHeader) throws -> Data +} + +extension DecryptionKeyManagementMode { + func determineContentEncryptionKey(from encryptedKey: Data) throws -> Data { return Data() } + func determineContentEncryptionKey(from encryptedKey: Data, header: JWEHeader) throws -> Data { return Data() } } extension KeyManagementAlgorithm { func makeEncryptionKeyManagementMode( contentEncryptionAlgorithm: ContentEncryptionAlgorithm, - encryptionKey: KeyType + encryptionKey: KeyType, + agreementPartyUInfo: Data? = nil, + agreementPartyVInfo: Data? = nil ) -> EncryptionKeyManagementMode? { switch self { case .RSA1_5, .RSAOAEP, .RSAOAEP256: @@ -62,6 +76,16 @@ extension KeyManagementAlgorithm { } return DirectEncryptionMode(sharedSymmetricKey: sharedSymmetricKey) + case .ECDH_ES, .ECDH_ES_A128KW, .ECDH_ES_A192KW, .ECDH_ES_A256KW: + guard let recipientPublicKey = cast(encryptionKey, to: ECKeyEncryption.PublicKey.self) else { + return nil + } + + return ECKeyEncryption.EncryptionMode(keyManagementAlgorithm: self, + contentEncryptionAlgorithm: contentEncryptionAlgorithm, + recipientPublicKey: recipientPublicKey, + agreementPartyUInfo: agreementPartyUInfo ?? Data(), + agreementPartyVInfo: agreementPartyVInfo ?? Data()) } } @@ -96,6 +120,15 @@ extension KeyManagementAlgorithm { } return DirectEncryptionMode(sharedSymmetricKey: sharedSymmetricKey) + case .ECDH_ES, .ECDH_ES_A128KW, .ECDH_ES_A192KW, .ECDH_ES_A256KW: + guard let recipientPrivateKey = cast(decryptionKey, to: ECKeyEncryption.PrivateKey.self) else { + return nil + } + + return ECKeyEncryption.DecryptionMode(keyManagementAlgorithm: self, + contentEncryptionAlgorithm: contentEncryptionAlgorithm, + recipientPrivateKey: recipientPrivateKey + ) } } } diff --git a/README.md b/README.md index ce35bcdd..bd4537db 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,10 @@ If you are missing a specific feature, algorithm, or serialization, feel free to RS384:white_check_mark: A192KW:white_check_mark: A192GCM RS512:white_check_mark: A256KW:white_check_mark: A256GCM:white_check_mark: ES256:white_check_mark: dir:white_check_mark: - ES384:white_check_mark: ECDH-ES - ES512:white_check_mark: ECDH-ES+A128KW - PS256:white_check_mark: ECDH-ES+A192KW - PS384:white_check_mark: ECDH-ES+A256KW + ES384:white_check_mark: ECDH-ES:white_check_mark: + ES512:white_check_mark: ECDH-ES+A128KW:white_check_mark: + PS256:white_check_mark: ECDH-ES+A192KW:white_check_mark: + PS384:white_check_mark: ECDH-ES+A256KW:white_check_mark: PS512:white_check_mark: A128GCMKW A192GCMKW A256GCMKW diff --git a/Tests/AESCBCContentEncryptionTests.swift b/Tests/AESCBCContentEncryptionTests.swift index abcb6c7b..b1028d26 100644 --- a/Tests/AESCBCContentEncryptionTests.swift +++ b/Tests/AESCBCContentEncryptionTests.swift @@ -174,7 +174,7 @@ class AESCBCContentEncryptionTests: XCTestCase { ])) let symmetricEncryptionContext = try AESCBCEncryption(contentEncryptionAlgorithm: .A128CBCHS256, secretKey: secretKey) - .encrypt(header: header, payload: Payload(plaintext)) + .encrypt(headerData: header.headerData, payload: Payload(plaintext)) // Check if the symmetric encryption was successful by using the CommonCrypto framework and not the implemented decrypt method. let hmacKey = secretKey.subdata(in: 0..<16) diff --git a/Tests/AESGCMEncryptionTests.swift b/Tests/AESGCMEncryptionTests.swift index 86335052..7e80537d 100644 --- a/Tests/AESGCMEncryptionTests.swift +++ b/Tests/AESGCMEncryptionTests.swift @@ -108,7 +108,7 @@ class AESGCMEncryptionTests: XCTestCase { let plaintext = "Live long and prosper.".data(using: .ascii)! let header = JWEHeader(keyManagementAlgorithm: .RSAOAEP256, contentEncryptionAlgorithm: .A256GCM) let symmetricEncryptionContext = try AESGCMEncryption(contentEncryptionAlgorithm: .A256GCM, contentEncryptionKey: contentEncryptionKey) - .encrypt(header: header, payload: Payload(plaintext)) + .encrypt(headerData: header.data(), payload: Payload(plaintext)) // Check if the symmetric encryption was successful by using the CryptoKit framework and not the implemented decrypt method. let additionalAuthenticatedData = header.data().base64URLEncodedData() diff --git a/Tests/ECDHTests.swift b/Tests/ECDHTests.swift new file mode 100644 index 00000000..2e48abf8 --- /dev/null +++ b/Tests/ECDHTests.swift @@ -0,0 +1,227 @@ +// +// ECDHTests.swift +// Tests +// +// Created by Mikael Rucinsky on 07.12.20. +// + +import XCTest +@testable import JOSESwift + +// swiftlint:disable force_unwrapping + +class ECDHTests: 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 testEcdhP521() { + + let staticJwkData = """ + { + "crv": "P-521", + "d": "AUbbQiwCrudeMaY4yO-epS8Z733v_6iekDE7Pg6lAhT2L_7n6MA3TmDbFYzTJXgWyVLhsgZhXBqYn8xTyXM4Htai", + "kty": "EC", + "x": "AKyzPrkMEWef9WsWohYs-Z18SoPmgQE53fk6CUmJV9QEvZWhXDSrptZeOrro8oXM1D4hQoSVlH_48QyxXQn27wqa", + "y": "AOWWwHV2nAFrOMGQfrh_TLj0bTHB8OVWfenbjVemgl2WdDhHFvvbkyYlAJid9X9FoazoHULmdo-zPoj-eVem4VCF" + } + """.data(using: .utf8) + + let ephermeralJwkData = """ + { + "crv": "P-521", + "d": "ASNphnyfafd_DTnTANzCX-HHmuttns5r3OlUkA5KVZcWhrGbnon23UfLgDFZqlD6m2tSLf1eOmGtX3RrJoF2Z-KJ", + "kty": "EC", + "x": "AXfHyCQgbohr3CRm-zNX0zUYriba2MHyduzaxOup5yzDO1hS-PhU0LqbaH2FECctCOYgktUKCDcyDAdkY1ZRszLb", + "y": "AHf_b9y-pbudkqws0rPcYGb9uVoIxotxdV-AQs0jcO0QCbAnm-QPNeF9-9mOCj-fAElOoi8UOfT5QlgxVdp67MQW" + } + """.data(using: .utf8) + + let expectedData = Data([0, 224, 82, 100, 184, 24, 23, 138, 155, 73, 143, 68, 142, 226, 142, 110, 120, 220, 105, 106, 220, 85, 251, 114, 5, 204, 117, 169, 140, 138, 219, 35, 86, 248, 83, 154, 231, 135, 207, 180, 80, 37, 122, 50, 47, 105, 227, 145, 69, 175, 167, 180, 171, 178, 219, 130, 56, 120, 3, 241, 93, 136, 176, 18, 188, 168]) + + let staticEcKey = try! ECPrivateKey(data: staticJwkData!) + let ephermeralEcKey = try! ECPrivateKey(data: ephermeralJwkData!) + let bobData = try! ecdhDeriveBits(for: ephermeralEcKey.getPrivate(), publicKey: staticEcKey.getPublic()) + let aliceData = try! ecdhDeriveBits(for: staticEcKey.getPrivate(), publicKey: ephermeralEcKey.getPublic()) + XCTAssertEqual(bobData, expectedData) + XCTAssertEqual(aliceData, bobData) + } + + func testEcdhP384() { + let staticJwkData = """ + { + "crv": "P-384", + "d": "k_g-sn31X_dik5nb50L_a0YCB1no_mcjsuX0bZwP6VQv_skCoUng0VUn5h_eNVIs", + "kty": "EC", + "x": "OfppBnaeX-TT6Xn_h4snNNwkdN29H_zL1A47ta-xvJ-Nq8LUaVT-klKkhOWqCBJo", + "y": "XPik7pebt5XmFIWxO9DWFLe16JIupLefjpuexAlJ3i0GlTGpzKRkOtGG-EElfj5c" + } + """.data(using: .utf8) + + let ephermeralJwkData = """ + { + "crv": "P-384", + "d": "Wjv7bhechF4t7Ujqs9YrM3rOpQYoJ1E0ZjVZaC3lAqE21nr8YRt8YDWB8jr5w66Q", + "kty": "EC", + "x": "lK4yQQb3kGyJ96JvBNJ7i7HPEermvIq7ZtyVFTsolPYIzEGvt-blROOQTHiNt69m", + "y": "gbYJSaZ6ekVI1r3pbpDGIM7o4OP3k36RVMxKCbP6HuOvq3mAHO4Lr6nyTy3bogPc" + } + """.data(using: .utf8) + + let expectedData = Data([230, 105, 144, 136, 251, 105, 192, 113, 56, 83, 18, 77, 253, 246, 16, 220, 184, 123, 192, 83, 77, 64, 255, 78, 114, 215, 36, 153, 91, 35, 78, 172, 23, 100, 143, 14, 243, 240, 74, 114, 216, 253, 94, 254, 97, 149, 115, 196]) + + let staticEcKey = try! ECPrivateKey(data: staticJwkData!) + let ephermeralEcKey = try! ECPrivateKey(data: ephermeralJwkData!) + let bobData = try! ecdhDeriveBits(for: ephermeralEcKey.getPrivate(), publicKey: staticEcKey.getPublic()) + let aliceData = try! ecdhDeriveBits(for: staticEcKey.getPrivate(), publicKey: ephermeralEcKey.getPublic()) + XCTAssertEqual(bobData, expectedData) + XCTAssertEqual(aliceData, bobData) + } + + func testEcdhP256() { + let staticJwkData = """ + { + "crv": "P-256", + "d": "AbffgK370mWXIZrN6Z9fkbTtrTR7tEezt2Xrei4MBv4", + "kty": "EC", + "x": "DmcvVfpUDcEA1qEdqoYWin33fFeWE0gmJWZUINGb_9I", + "y": "_Jt9LSkX3u-Vc3DfDq1svbfpkXCQN6Zx2QhygiHlghg" + } + """.data(using: .utf8) + + let ephermeralJwkData = """ + { + "crv": "P-256", + "d": "iRyhvwq_12htMLqxD7WxGxplPnM7qERKJ-Y9RQcLUi0", + "kty": "EC", + "x": "XPWOBbmFX4KqM--QywDwck0NNL2gheuvDgHK2r0sj6E", + "y": "KvlPaC91qExYOUJcp8C_Ml4Tv43BtRBlTEZmLTpzGU4" + } + """.data(using: .utf8) + + let expectedData = Data([73, 222, 123, 31, 188, 213, 243, 252, 244, 226, 35, 24, 228, 238, 70, 152, 31, 249, 163, 201, 233, 219, 202, 33, 245, 140, 21, 169, 252, 199, 110, 177]) + + let staticEcKey = try! ECPrivateKey(data: staticJwkData!) + let ephermeralEcKey = try! ECPrivateKey(data: ephermeralJwkData!) + let bobData = try! ecdhDeriveBits(for: ephermeralEcKey.getPrivate(), publicKey: staticEcKey.getPublic()) + let aliceData = try! ecdhDeriveBits(for: staticEcKey.getPrivate(), publicKey: ephermeralEcKey.getPublic()) + XCTAssertEqual(bobData, expectedData) + XCTAssertEqual(aliceData, bobData) + } + + func testDeriveKeyDataShoudTheSameP256() { + let staticKey = try! ECKeyPair.generateWith(ECCurveType.P256) + let ephermeralKey = try! ECKeyPair.generateWith(ECCurveType.P256) + let data1 = try! ecdhDeriveBits(for: staticKey.getPrivate(), publicKey: ephermeralKey.getPublic()) + let data2 = try! ecdhDeriveBits(for: ephermeralKey.getPrivate(), publicKey: staticKey.getPublic()) + XCTAssertEqual(data1, data2) + } + + func testDeriveKeyDataShoudTheSameP384() { + let staticKey = try! ECKeyPair.generateWith(ECCurveType.P384) + let ephermeralKey = try! ECKeyPair.generateWith(ECCurveType.P384) + let data1 = try! ecdhDeriveBits(for: staticKey.getPrivate(), publicKey: ephermeralKey.getPublic()) + let data2 = try! ecdhDeriveBits(for: ephermeralKey.getPrivate(), publicKey: staticKey.getPublic()) + XCTAssertEqual(data1, data2) + } + + func testDeriveKeyDataShoudTheSameP521() { + let staticKey = try! ECKeyPair.generateWith(ECCurveType.P521) + let ephermeralKey = try! ECKeyPair.generateWith(ECCurveType.P521) + let data1 = try! ecdhDeriveBits(for: staticKey.getPrivate(), publicKey: ephermeralKey.getPublic()) + let data2 = try! ecdhDeriveBits(for: ephermeralKey.getPrivate(), publicKey: staticKey.getPublic()) + XCTAssertEqual(data1, data2) + } + +// Concat KDF see https://tools.ietf.org/html/rfc7518#section-4.6.2 + func testConcatKDF() { + let z = Data([ + 158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132, + 38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121, + 140, 254, 144, 196]) + + let algID = Data([0, 0, 0, 7, 65, 49, 50, 56, 71, 67, 77]) + + let ptyUInfo = Data([0, 0, 0, 5, 65, 108, 105, 99, 101]) + let ptyVInfo = Data([0, 0, 0, 3, 66, 111, 98]) + + let supPubInfo = Data([0, 0, 0, 128]) + let supPrivInfo = Data() + + let expected = Data([86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26]) + + let concat = try! concatKDF(hash: Hash.SHA256, + z: z, + keyDataLen: 128, + algorithmID: algID, + partyUInfo: ptyUInfo, + partyVInfo: ptyVInfo, + suppPubInfo: supPubInfo, + suppPrivInfo: supPrivInfo) + + XCTAssertEqual(String(data: expected, encoding: .utf16)!, String(data: concat, encoding: .utf16)!) + } + + func testEcdhKeyAgreementCompute() { + let aliceKey = try! ECPrivateKey(data: """ + { + "crv": "P-256", + "d": "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo", + "kty": "EC", + "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps" + } + """.data(using: .utf8)!) + + let bobKey = try! ECPrivateKey(data: """ + { + "crv": "P-256", + "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw", + "kty": "EC", + "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck" + } + """.data(using: .utf8)!) + + let alg = KeyManagementAlgorithm.ECDH_ES + let enc = ContentEncryptionAlgorithm.A256CBCHS512 + let apuData = "Alice".data(using: .utf8)! + let apvData = "Bob".data(using: .utf8)! + let expected = Data([57, 134, 170, 121, 246, 57, 100, 32, 229, 128, 229, 211, 137, 15, 98, 63, 238, 93, 69, 34, 48, 121, 41, 235, 153, 238, 52, 37, 160, 1, 236, 193, 117, 177, 117, 78, 63, 182, 68, 206, 130, 80, 52, 181, 98, 82, 62, 154, 136, 6, 188, 168, 215, 106, 250, 134, 30, 155, 121, 81, 88, 3, 34, 93]) + let data = try! keyAgreementCompute(with: alg, + encryption: enc, + privateKey: bobKey.getPrivate(), + publicKey: aliceKey.getPublic(), + apu: apuData, + apv: apvData) + XCTAssertEqual(data, expected) + } + + func testPerformanceConcatKDF() { + // This is an example of a performance test case. + measure { + let z = Data([ + 158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132, + 38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121, + 140, 254, 144, 196]) + + let algID = Data([0, 0, 0, 7, 65, 49, 50, 56, 71, 67, 77]) + + let ptyUInfo = Data([0, 0, 0, 5, 65, 108, 105, 99, 101]) + let ptyVInfo = Data([0, 0, 0, 3, 66, 111, 98]) + + let supPubInfo = Data([0, 0, 0, 128]) + let supPrivInfo = Data() + do { + let a = try concatKDF(hash: Hash.SHA256, z: z, keyDataLen: 128, algorithmID: algID, partyUInfo: ptyUInfo, partyVInfo: ptyVInfo, suppPubInfo: supPubInfo, suppPrivInfo: supPrivInfo) + print(a) + } catch {} + } + } +} +// swiftlint:enable force_unwrapping diff --git a/Tests/EncrypterDecrypterInitializationTests.swift b/Tests/EncrypterDecrypterInitializationTests.swift index 5f05b9fc..12b6a77b 100644 --- a/Tests/EncrypterDecrypterInitializationTests.swift +++ b/Tests/EncrypterDecrypterInitializationTests.swift @@ -33,6 +33,7 @@ private func ~= (array: [T], value: T) -> Bool { class EncrypterDecrypterInitializationTests: RSACryptoTestCase { let rsaKeyManagementModeAlgorithms: [KeyManagementAlgorithm] = [.RSA1_5, .RSAOAEP, .RSAOAEP256] let aesKeyManagementModeAlgorithms: [KeyManagementAlgorithm] = [.A128KW, .A192KW, .A256KW] + let ecdhKeyManagementModeAlgorithms: [KeyManagementAlgorithm] = [.ECDH_ES, .ECDH_ES_A128KW, .ECDH_ES_A192KW, .ECDH_ES_A256KW] @available(*, deprecated) func testEncrypterDeprecated1Initialization() { @@ -87,6 +88,27 @@ class EncrypterDecrypterInitializationTests: RSACryptoTestCase { encryptionKey: Data() ) ) + case ecdhKeyManagementModeAlgorithms: + let pubJwk = """ + { + "crv": "P-256", + "kty": "EC", + "x": "CQJxA68WhgU3hztigbedfLtJitDhScq3XSnXgO0FV5o", + "y": "WFg6s36izURa733WqeoJ8zXMd7ho5OSwdWnMsEPgTEI" + } + """.data(using: .utf8) + + guard let publicJWK = pubJwk, let publicKey = try? ECPublicKey(data: publicJWK) else { + return XCTAssertThrowsError("publicKey is nil") + } + + XCTAssertNotNil( + Encrypter( + keyManagementAlgorithm: algorithm, + contentEncryptionAlgorithm: .A256CBCHS512, + encryptionKey: publicKey + ) + ) default: XCTFail() } @@ -120,6 +142,14 @@ class EncrypterDecrypterInitializationTests: RSACryptoTestCase { encryptionKey: publicKeyAlice2048! ) ) + case ecdhKeyManagementModeAlgorithms: + XCTAssertNil( + Encrypter( + keyManagementAlgorithm: algorithm, + contentEncryptionAlgorithm: .A256CBCHS512, + encryptionKey: Data() + ) + ) default: XCTFail() } @@ -179,6 +209,28 @@ class EncrypterDecrypterInitializationTests: RSACryptoTestCase { decryptionKey: Data() ) ) + case ecdhKeyManagementModeAlgorithms: + let privJwk = """ + { + "crv": "P-256", + "d": "920OCD0fW97YXbQNN-JaOtaDgbuNyVxXgKwjfXPPqv4", + "kty": "EC", + "x": "CQJxA68WhgU3hztigbedfLtJitDhScq3XSnXgO0FV5o", + "y": "WFg6s36izURa733WqeoJ8zXMd7ho5OSwdWnMsEPgTEI" + } + """.data(using: .utf8) + + guard let privateJWK = privJwk, let privateKey = try? ECPrivateKey(data: privateJWK) else { + return XCTAssertThrowsError("privateKey is nil") + } + + XCTAssertNotNil( + Decrypter( + keyManagementAlgorithm: algorithm, + contentEncryptionAlgorithm: .A256CBCHS512, + decryptionKey: privateKey + ) + ) default: XCTFail() } @@ -212,6 +264,14 @@ class EncrypterDecrypterInitializationTests: RSACryptoTestCase { decryptionKey: publicKeyAlice2048! ) ) + case ecdhKeyManagementModeAlgorithms: + XCTAssertNil( + Decrypter( + keyManagementAlgorithm: algorithm, + contentEncryptionAlgorithm: .A128CBCHS256, + decryptionKey: Data() + ) + ) default: XCTFail() } diff --git a/Tests/JWEECTests.swift b/Tests/JWEECTests.swift new file mode 100644 index 00000000..61def1a7 --- /dev/null +++ b/Tests/JWEECTests.swift @@ -0,0 +1,65 @@ +// +// JWEECTests.swift +// Tests +// +// Created by Mikael Rucinsky on 07.12.20. +// + +import XCTest +@testable import JOSESwift + +class JWEECTests: ECCryptoTestCase { + + let pubJwk = """ + { + "crv": "P-256", + "kty": "EC", + "x": "CQJxA68WhgU3hztigbedfLtJitDhScq3XSnXgO0FV5o", + "y": "WFg6s36izURa733WqeoJ8zXMd7ho5OSwdWnMsEPgTEI" + } + """.data(using: .utf8) + + let privJwk = """ + { + "crv": "P-256", + "d": "920OCD0fW97YXbQNN-JaOtaDgbuNyVxXgKwjfXPPqv4", + "kty": "EC", + "x": "CQJxA68WhgU3hztigbedfLtJitDhScq3XSnXgO0FV5o", + "y": "WFg6s36izURa733WqeoJ8zXMd7ho5OSwdWnMsEPgTEI" + } + """.data(using: .utf8) + + let plaintext = "Lorem Ipsum" + + func test() { + + guard let publicJWK = pubJwk, let publicKey = try? ECPublicKey(data: publicJWK) else { + return XCTAssertThrowsError("publicKey is nil") + } + + guard let privateJWK = privJwk, let privateKey = try? ECPrivateKey(data: privateJWK) else { + return XCTAssertThrowsError("privateKey is nil") + } + + guard let input = plaintext.data(using: .utf8), + let encrypter = Encrypter(keyManagementAlgorithm: .ECDH_ES_A128KW, + contentEncryptionAlgorithm: .A256CBCHS512, + encryptionKey: publicKey), + let decrypter = Decrypter(keyManagementAlgorithm: .ECDH_ES_A128KW, + contentEncryptionAlgorithm: .A256CBCHS512, + decryptionKey: privateKey) else { + return XCTAssertThrowsError("wrong inputs") + } + + let payload = Payload(input) + let jwe = try! JWE(header: JWEHeader(keyManagementAlgorithm: .ECDH_ES_A128KW, contentEncryptionAlgorithm: .A256CBCHS512), + payload: payload, + encrypter: encrypter) + let serialization = jwe.compactSerializedString + + let deserialization = try! JWE(compactSerialization: serialization) + let decrypted = try! deserialization.decrypt(using: decrypter) + XCTAssertEqual(input, decrypted.data()) + } + +} diff --git a/Tests/JWEHeaderTests.swift b/Tests/JWEHeaderTests.swift index 19bb8cbf..af4a747f 100644 --- a/Tests/JWEHeaderTests.swift +++ b/Tests/JWEHeaderTests.swift @@ -37,6 +37,42 @@ class JWEHeaderTests: XCTestCase { let parameterDictDirect = ["alg": "dir", "enc": "A256CBC-HS512"] let parameterDataDirect = try! JSONSerialization.data(withJSONObject: ["alg": "dir", "enc": "A256CBC-HS512"], options: [.sortedKeys]) + let parameterDictECDHES = ["alg": "ECDH-ES", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9i", "epk": + ["kty": "EC", "crv": "P-256", "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps"]] as [String: Any] + let parameterDataECDHES = try! JSONSerialization.data(withJSONObject: ["alg": "ECDH-ES", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9i", "epk": + ["kty": "EC", "crv": "P-256", "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps"]], options: []) + + let ecPublicKey = ECPublicKey(crv: .P256, + x: "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + y: "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps") + + func testInitECDHWithParameters() { + + let header = try! JWEHeader(parameters: parameterDictECDHES) + + XCTAssertEqual(header.parameters["enc"] as? String, ContentEncryptionAlgorithm.A256CBCHS512.rawValue) + XCTAssertEqual(header.parameters["alg"] as? String, KeyManagementAlgorithm.ECDH_ES.rawValue) + XCTAssertEqual(header.apu, "QWxpY2U") + XCTAssertEqual(header.apv, "Qm9i") + XCTAssertEqual(header.epk?.jsonString(), ecPublicKey.jsonString()) + XCTAssertEqual(header.data().count, try! JSONSerialization.data(withJSONObject: parameterDictECDHES, options: []).count) + } + + func testInitECDHCustomInitWithParameters() { + + let header = JWEHeader(keyManagementAlgorithm: .ECDH_ES, + contentEncryptionAlgorithm: .A256CBCHS512, + agreementPartyUInfo: "QWxpY2U", + agreementPartyVInfo: "Qm9i", + ephemeralPublicKey: ecPublicKey) + + XCTAssertEqual(header.parameters["enc"] as? String, ContentEncryptionAlgorithm.A256CBCHS512.rawValue) + XCTAssertEqual(header.parameters["alg"] as? String, KeyManagementAlgorithm.ECDH_ES.rawValue) + XCTAssertEqual(header.apu, "QWxpY2U") + XCTAssertEqual(header.apv, "Qm9i") + XCTAssertEqual(header.epk?.jsonString(), ecPublicKey.jsonString()) + XCTAssertEqual(header.data().count, try! JSONSerialization.data(withJSONObject: parameterDictECDHES, options: []).count) + } func testInitRSA1WithParameters() { let header = try! JWEHeader(parameters: parameterDictRSA) @@ -62,6 +98,14 @@ class JWEHeaderTests: XCTestCase { XCTAssertEqual(header.data().count, try! JSONSerialization.data(withJSONObject: parameterDictRSAOAEP256, options: []).count) } + func testInitECDHESWithParameters() { + let header = try! JWEHeader(parameters: parameterDictECDHES) + + XCTAssertEqual(header.parameters["enc"] as? String, ContentEncryptionAlgorithm.A256CBCHS512.rawValue) + XCTAssertEqual(header.parameters["alg"] as? String, KeyManagementAlgorithm.ECDH_ES.rawValue) + XCTAssertEqual(header.data().count, try! JSONSerialization.data(withJSONObject: parameterDictECDHES, options: []).count) + } + func testInitRSA1WithData() { let data = try! JSONSerialization.data(withJSONObject: parameterDictRSA, options: []) let header = JWEHeader(data)! @@ -97,6 +141,15 @@ class JWEHeaderTests: XCTestCase { XCTAssertEqual(header.data().count, try! JSONSerialization.data(withJSONObject: parameterDictDirect, options: []).count) } + func testInitECDHESWithData() { + let data = try! JSONSerialization.data(withJSONObject: parameterDictECDHES, options: []) + let header = JWEHeader(data)! + + XCTAssertEqual(header.parameters["enc"] as? String, ContentEncryptionAlgorithm.A256CBCHS512.rawValue) + XCTAssertEqual(header.parameters["alg"] as? String, KeyManagementAlgorithm.ECDH_ES.rawValue) + XCTAssertEqual(header.data(), data) + } + func testInitDirectWithData() { let data = try! JSONSerialization.data(withJSONObject: parameterDictDirect, options: []) let header = JWEHeader(data)! @@ -145,6 +198,18 @@ class JWEHeaderTests: XCTestCase { XCTAssertEqual(header.contentEncryptionAlgorithm!, .A256CBCHS512) } + func testInitWithAlgAndEncECDHES() { + let header = JWEHeader(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256CBCHS512) + + XCTAssertEqual(header.parameters["alg"] as? String, KeyManagementAlgorithm.ECDH_ES.rawValue) + XCTAssertEqual(header.parameters["enc"] as? String, ContentEncryptionAlgorithm.A256CBCHS512.rawValue) + + XCTAssertNotNil(header.keyManagementAlgorithm) + XCTAssertNotNil(header.contentEncryptionAlgorithm) + XCTAssertEqual(header.keyManagementAlgorithm!, .ECDH_ES) + XCTAssertEqual(header.contentEncryptionAlgorithm!, .A256CBCHS512) + } + func testInitWithMissingRequiredEncParameter() { do { _ = try JWEHeader(parameters: ["alg": "RSA-OAEP"], headerData: try! JSONSerialization.data(withJSONObject: ["alg": "RSA1_5"], options: []))