Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jwe): add chacha20-poly1305 support encryption for ecdhes and ecdh1pu #10

Merged
merged 1 commit into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:|

</td></tr> </table>

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -44,13 +48,15 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable
case .a128CBCHS256: return 256
case .a192CBCHS384: return 384
case .a256CBCHS512: return 512
case .c20PKW: return 256
}
}

/// Returns the initialization vector size in bits suitable for the encryption algorithm.
/// - 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
}
Expand All @@ -72,6 +78,8 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable
return AES192GCM()
case .a256GCM:
return AES256GCM()
case .c20PKW:
return C20PKW()
}
}

Expand All @@ -91,6 +99,8 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable
return AES192GCM()
case .a256GCM:
return AES256GCM()
case .c20PKW:
return C20PKW()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ struct ECDH1PUJWEDecryptor: JWEDecryptor {
.a256GCM,
.a128CBCHS256,
.a192CBCHS384,
.a256CBCHS512
.a256CBCHS512,
.c20PKW
]

func decrypt<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ struct ECDHJWEDecryptor: JWEDecryptor {
.a256GCM,
.a128CBCHS256,
.a192CBCHS384,
.a256CBCHS512
.a256CBCHS512,
.c20PKW
]

func decrypt<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ struct ECDH1PUJWEEncryptor: JWEEncryptor {
.a256GCM,
.a128CBCHS256,
.a192CBCHS384,
.a256CBCHS512
.a256CBCHS512,
.c20PKW
]

init(masterEphemeralKey: Bool = false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ struct ECDHJWEEncryptor: JWEEncryptor {
.a256GCM,
.a128CBCHS256,
.a192CBCHS384,
.a256CBCHS512
.a256CBCHS512,
.c20PKW
]

init(masterEphemeralKey: Bool = false) {
Expand Down
44 changes: 44 additions & 0 deletions Tests/JWATests/XC20PTests.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}
36 changes: 36 additions & 0 deletions Tests/JWETests/ECDH1PUTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
36 changes: 36 additions & 0 deletions Tests/JWETests/ECDHESTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading