diff --git a/README.md b/README.md index dbc8ee2..9328b17 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ This library provides comprehensive support for the Jose suite of standards, inc | A128GCMKW |:white_check_mark:| | A192GCMKW |:white_check_mark:| | A256GCMKW |:white_check_mark:| +| C20PKW |:white_check_mark:| @@ -268,7 +269,17 @@ let jws = try JWS(payload: payload, key: keyJWK) let jwsString = jws.compactSerialization try JWS(jwsString: jwsString).verify(key: keyJWK) +``` + +If you want to add additional headers beyond the default to the JWS: +```swift +let rsaKeyId = "Hello-keyId" +var header = DefaultJWSHeaderImpl() +header.keyID = rsaKeyId +header.algorithm = .rsa512 +let keyJWK = JWK(keyType: .rsa, algorithm: "RSA512", keyID: rsaKeyId, e: rsaKeyExponent, n: rsaKeyModulus) +let jwe = try JWS(payload: payload, protectedHeader: header, key: jwk) ``` ### JWE (JSON Web Encryption) @@ -307,6 +318,8 @@ JWE represents encrypted content using JSON-based data structures, following the - A128GCM (AES GCM using 128-bit key) - A192GCM (AES GCM using 192-bit key) - A256GCM (AES GCM using 256-bit key) + - C20PKW (ChaCha20-Poly1305) + - Note: ChaChaPoly20-Poly1305 is specified in [draft-amringer-jose-chacha-02](https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02) 3. **Compression Algorithms**: - DEFLATE (zip) diff --git a/Sources/JSONWebAlgorithms/ContentEncryption/AES/XC20P+ContentEncryption.swift b/Sources/JSONWebAlgorithms/ContentEncryption/AES/XC20P+ContentEncryption.swift new file mode 100644 index 0000000..348fc5f --- /dev/null +++ b/Sources/JSONWebAlgorithms/ContentEncryption/AES/XC20P+ContentEncryption.swift @@ -0,0 +1,60 @@ +import CryptoKit +import Foundation + +struct C20PKW: ContentEncryptor, ContentDecryptor { + + let contentEncryptionAlgorithm: String = ContentEncryptionAlgorithm.c20PKW.rawValue + let initializationVectorSizeInBits: Int = ContentEncryptionAlgorithm.c20PKW.initializationVectorSizeInBits + let cekKeySize: Int = ContentEncryptionAlgorithm.c20PKW.keySizeInBits + + func generateInitializationVector() throws -> Data { + try SecureRandom.secureRandomData(count: initializationVectorSizeInBits / 8) + } + + func generateCEK() throws -> Data { + try SecureRandom.secureRandomData(count: cekKeySize / 8) + } + + func encrypt(payload: Data, using key: Data, arguments: [ContentEncryptionArguments]) throws -> ContentEncryptionResult { + guard let iv = arguments.initializationVector else { + throw CryptoError.missingInitializationVector + } + + guard iv.count * 8 == initializationVectorSizeInBits else { + throw CryptoError.initializationVectorWrongSize(sizeInBits: initializationVectorSizeInBits) + } + + guard let aad = arguments.additionalAuthenticationData else { + throw CryptoError.missingAdditionalAuthenticatingData + } + + let aead = try ChaChaPoly.seal( + payload, + using: .init(data: key), + nonce: .init(data: iv), + authenticating: aad + ) + + return .init(cipher: aead.ciphertext, authenticationData: aead.tag) + } + + func decrypt(cipher: Data, using key: Data, arguments: [ContentEncryptionArguments]) throws -> Data { + guard let iv = arguments.initializationVector else { + throw CryptoError.missingInitializationVector + } + + guard let tag = arguments.authenticationTag else { + throw CryptoError.missingAuthenticationTag + } + + guard let aad = arguments.additionalAuthenticationData else { + throw CryptoError.missingAdditionalAuthenticatingData + } + + return try ChaChaPoly.open(.init( + nonce: .init(data: iv), + ciphertext: cipher, + tag: tag + ), using: .init(data: key), authenticating: aad) + } +} diff --git a/Sources/JSONWebAlgorithms/ContentEncryption/ContentEncryptionAlgorithm.swift b/Sources/JSONWebAlgorithms/ContentEncryption/ContentEncryptionAlgorithm.swift index 8b5b088..61fcc3c 100644 --- a/Sources/JSONWebAlgorithms/ContentEncryption/ContentEncryptionAlgorithm.swift +++ b/Sources/JSONWebAlgorithms/ContentEncryption/ContentEncryptionAlgorithm.swift @@ -33,6 +33,10 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable /// AES encryption in GCM mode with a 256-bit key. /// This algorithm provides robust security and is widely used in various security protocols and systems. case a256GCM = "A256GCM" + + /// ChaCha20-Poly1305 with a 256-bit key and 96 bit IV. + /// This algorithm provides robust security and is widely used in various security protocols and systems, it is faster than AES in mobile devices. + case c20PKW = "C20PKW" /// Returns the key size in bits used by the encryption algorithm. /// - Returns: The size of the key in bits. @@ -44,6 +48,7 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable case .a128CBCHS256: return 256 case .a192CBCHS384: return 384 case .a256CBCHS512: return 512 + case .c20PKW: return 256 } } @@ -51,6 +56,7 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable /// - Returns: The size of the initialization vector in bits. public var initializationVectorSizeInBits: Int { switch self { + case .c20PKW: return 96 case .a128CBCHS256, .a192CBCHS384, .a256CBCHS512: return 128 case .a128GCM, .a192GCM, .a256GCM: return 96 } @@ -72,6 +78,8 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable return AES192GCM() case .a256GCM: return AES256GCM() + case .c20PKW: + return C20PKW() } } @@ -91,6 +99,8 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable return AES192GCM() case .a256GCM: return AES256GCM() + case .c20PKW: + return C20PKW() } } } diff --git a/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDH1PUDecryptor.swift b/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDH1PUDecryptor.swift index a93ad2d..e9e7f6f 100644 --- a/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDH1PUDecryptor.swift +++ b/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDH1PUDecryptor.swift @@ -34,7 +34,8 @@ struct ECDH1PUJWEDecryptor: JWEDecryptor { .a256GCM, .a128CBCHS256, .a192CBCHS384, - .a256CBCHS512 + .a256CBCHS512, + .c20PKW ] func decrypt< diff --git a/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDHDecryptor.swift b/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDHDecryptor.swift index 4b7eadf..322ad1f 100644 --- a/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDHDecryptor.swift +++ b/Sources/JSONWebEncryption/EncryptionModule/Decryptors/ECDHDecryptor.swift @@ -34,7 +34,8 @@ struct ECDHJWEDecryptor: JWEDecryptor { .a256GCM, .a128CBCHS256, .a192CBCHS384, - .a256CBCHS512 + .a256CBCHS512, + .c20PKW ] func decrypt< diff --git a/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDH1PUEncrypter.swift b/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDH1PUEncrypter.swift index 115c515..869e24d 100644 --- a/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDH1PUEncrypter.swift +++ b/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDH1PUEncrypter.swift @@ -35,7 +35,8 @@ struct ECDH1PUJWEEncryptor: JWEEncryptor { .a256GCM, .a128CBCHS256, .a192CBCHS384, - .a256CBCHS512 + .a256CBCHS512, + .c20PKW ] init(masterEphemeralKey: Bool = false) { diff --git a/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDHEncrypter.swift b/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDHEncrypter.swift index c87a8c3..c35f9ca 100644 --- a/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDHEncrypter.swift +++ b/Sources/JSONWebEncryption/EncryptionModule/Encryptors/ECDHEncrypter.swift @@ -34,7 +34,8 @@ struct ECDHJWEEncryptor: JWEEncryptor { .a256GCM, .a128CBCHS256, .a192CBCHS384, - .a256CBCHS512 + .a256CBCHS512, + .c20PKW ] init(masterEphemeralKey: Bool = false) { diff --git a/Tests/JWATests/XC20PTests.swift b/Tests/JWATests/XC20PTests.swift new file mode 100644 index 0000000..7b9b51b --- /dev/null +++ b/Tests/JWATests/XC20PTests.swift @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Gonçalo Frade + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@testable import JSONWebAlgorithms +import JSONWebKey +import XCTest + +final class C20PTests: XCTestCase { + + func testC20PCycle() throws { + let payload = "Test".data(using: .utf8)! + let encryptor = ContentEncryptionAlgorithm.c20PKW.encryptor + let decryptor = ContentEncryptionAlgorithm.c20PKW.decryptor + let key = try encryptor.generateCEK() + let iv = try encryptor.generateInitializationVector() + let aad = Data() + + let encryption = try encryptor.encrypt(payload: payload, using: key, arguments: [ + .initializationVector(iv), + .additionalAuthenticationData(aad) + ]) + + let decryption = try decryptor.decrypt(cipher: encryption.cipher, using: key, arguments: [ + .initializationVector(iv), + .authenticationTag(encryption.authenticationData), + .additionalAuthenticationData(aad) + ]) + + XCTAssertEqual(try! payload.tryToString(), try! decryption.tryToString()) + } +} diff --git a/Tests/JWETests/ECDH1PUTests.swift b/Tests/JWETests/ECDH1PUTests.swift index e361b1d..64834cb 100644 --- a/Tests/JWETests/ECDH1PUTests.swift +++ b/Tests/JWETests/ECDH1PUTests.swift @@ -201,4 +201,40 @@ final class ECDH1PUTests: XCTestCase { XCTAssertEqual(payload.toHexString(), decrypted.toHexString()) } + + func testECDH1PUA256KW_C20PKWCycle() throws { + let payload = try "Test".tryToData() + let aliceKey = JWK.testingCurve25519KPair + let bobKey = JWK.testingCurve25519KPair + + let keyAlg = KeyManagementAlgorithm.ecdh1PUA256KW + let encAlg = ContentEncryptionAlgorithm.c20PKW + + let header = try DefaultJWEHeaderImpl( + keyManagementAlgorithm: keyAlg, + encodingAlgorithm: encAlg, + agreementPartyUInfo: Base64URL.encode("Alice".tryToData()).tryToData(), + agreementPartyVInfo: Base64URL.encode("Bob".tryToData()).tryToData() + ) + + let jwe = try ECDH1PUJWEEncryptor().encrypt( + payload: payload, + senderKey: aliceKey, + recipientKey: bobKey, + protectedHeader: header + ) + + let decrypted = try ECDH1PUJWEDecryptor().decrypt( + protectedHeader: jwe.protectedHeader!, + cipher: jwe.cipherText, + encryptedKey: jwe.encryptedKey, + initializationVector: jwe.initializationVector, + authenticationTag: jwe.authenticationTag, + additionalAuthenticationData: jwe.additionalAuthenticationData, + senderKey: aliceKey, + recipientKey: bobKey + ) + + XCTAssertEqual(payload.toHexString(), decrypted.toHexString()) + } } diff --git a/Tests/JWETests/ECDHESTests.swift b/Tests/JWETests/ECDHESTests.swift index 81b8fc3..e96a980 100644 --- a/Tests/JWETests/ECDHESTests.swift +++ b/Tests/JWETests/ECDHESTests.swift @@ -92,4 +92,40 @@ final class ECDHESTests: XCTestCase { XCTAssertEqual(payload, decrypted) } + + func testECDHESA256KW_C20PKWCycle() throws { + let payload = try "Test".tryToData() + let aliceKey = JWK.testingES256Pair + let bobKey = JWK.testingES256Pair + + let keyAlg = KeyManagementAlgorithm.ecdhESA128KW + let encAlg = ContentEncryptionAlgorithm.c20PKW + + let header = try DefaultJWEHeaderImpl( + keyManagementAlgorithm: keyAlg, + encodingAlgorithm: encAlg, + agreementPartyUInfo: Base64URL.encode("Alice".tryToData()).tryToData(), + agreementPartyVInfo: Base64URL.encode("Bob".tryToData()).tryToData() + ) + + let jwe = try ECDHJWEEncryptor().encrypt( + payload: payload, + senderKey: aliceKey, + recipientKey: bobKey, + protectedHeader: header + ) + + let decrypted = try ECDHJWEDecryptor().decrypt( + protectedHeader: jwe.protectedHeader!, + cipher: jwe.cipherText, + encryptedKey: jwe.encryptedKey, + initializationVector: jwe.initializationVector, + authenticationTag: jwe.authenticationTag, + additionalAuthenticationData: jwe.additionalAuthenticationData, + senderKey: aliceKey, + recipientKey: bobKey + ) + + XCTAssertEqual(payload, decrypted) + } }