Skip to content

Commit

Permalink
feat(jwe): add chacha20-poly1305 support encryption for ecdhes and ec…
Browse files Browse the repository at this point in the history
…dh1pu (#10)
  • Loading branch information
beatt83 authored Mar 28, 2024
1 parent 071aa57 commit 3d48a51
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 4 deletions.
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)
}
}

0 comments on commit 3d48a51

Please sign in to comment.