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 XC20PKW encryption #26

Merged
merged 1 commit into from
Jun 6, 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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ This library provides comprehensive support for the Jose suite of standards, inc
| A192GCMKW |:white_check_mark:|
| A256GCMKW |:white_check_mark:|
| C20PKW |:white_check_mark:|
| XC20PKW |:white_check_mark:|

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

Expand Down Expand Up @@ -336,7 +337,8 @@ Please check our documentation for more on [JWE Encryption](https://beatt83.gith
- 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)
- XC20PKW (XChaCha20-Poly1305)
- Note: ChaChaPoly20-Poly1305 and XChaChaPoly20-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,93 @@
import CryptoSwift
import Foundation

/// `XC20PKW` provides methods to encrypt and decrypt data using the XChaCha20-Poly1305 algorithm.
public struct XC20PKW: ContentEncryptor, ContentDecryptor {
/// The content encryption algorithm used, represented as a string.
public let contentEncryptionAlgorithm: String = ContentEncryptionAlgorithm.xC20PKW.rawValue
/// The size of the initialization vector in bits.
public let initializationVectorSizeInBits: Int = ContentEncryptionAlgorithm.xC20PKW.initializationVectorSizeInBits
/// The size of the content encryption key (CEK) in bits.
public let cekKeySize: Int = ContentEncryptionAlgorithm.xC20PKW.keySizeInBits

/// Generates a random initialization vector.
/// - Throws: An error if the random data generation fails.
/// - Returns: A data object containing the initialization vector.
public func generateInitializationVector() throws -> Data {
try SecureRandom.secureRandomData(count: initializationVectorSizeInBits / 8)
}

/// Generates a random content encryption key (CEK).
/// - Throws: An error if the random data generation fails.
/// - Returns: A data object containing the CEK.
public func generateCEK() throws -> Data {
try SecureRandom.secureRandomData(count: cekKeySize / 8)
}

/// Encrypts the payload using the XChaCha20-Poly1305 algorithm.
/// - Parameters:
/// - payload: The data to be encrypted.
/// - key: The encryption key.
/// - arguments: Additional encryption arguments, such as initialization vector and additional authenticated data.
/// - Throws: An error if the encryption fails or if required arguments are missing or of incorrect size.
/// - Returns: A `ContentEncryptionResult` containing the cipher text and authentication tag.
public 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 CryptoSwift.AEADXChaCha20Poly1305.encrypt(
payload.bytes,
key: key.bytes,
iv: iv.bytes,
authenticationHeader: aad.bytes
)

return .init(cipher: Data(aead.cipherText), authenticationData: Data(aead.authenticationTag))
}

/// Decrypts the cipher text using the XChaCha20-Poly1305 algorithm.
/// - Parameters:
/// - cipher: The data to be decrypted.
/// - key: The decryption key.
/// - arguments: Additional decryption arguments, such as initialization vector and authentication tag.
/// - Throws: An error if the decryption fails or if required arguments are missing.
/// - Returns: The decrypted data.
public 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 Data(AEADXChaCha20Poly1305.decrypt(
cipher.bytes,
key: key.bytes,
iv: iv.bytes,
authenticationHeader: aad.bytes,
authenticationTag: tag.bytes
).plainText)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable
/// 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"

/// XChaCha20-Poly1305 with a 256-bit key and 192 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 xC20PKW = "XC20PKW"



/// Returns the key size in bits used by the encryption algorithm.
/// - Returns: The size of the key in bits.
Expand All @@ -48,7 +54,7 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable
case .a128CBCHS256: return 256
case .a192CBCHS384: return 384
case .a256CBCHS512: return 512
case .c20PKW: return 256
case .c20PKW, .xC20PKW: return 256
}
}

Expand All @@ -57,6 +63,7 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable
public var initializationVectorSizeInBits: Int {
switch self {
case .c20PKW: return 96
case .xC20PKW: return 192
case .a128CBCHS256, .a192CBCHS384, .a256CBCHS512: return 128
case .a128GCM, .a192GCM, .a256GCM: return 96
}
Expand All @@ -80,6 +87,8 @@ public enum ContentEncryptionAlgorithm: String, Codable, Equatable, CaseIterable
return AES256GCM()
case .c20PKW:
return C20PKW()
case .xC20PKW:
return XC20PKW()
}
}

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

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

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

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

init(masterEphemeralKey: Bool = false) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/jose-swift/jose-swift.docc/Articles/JWEEncryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ The **jose-swift** library supports a wide range of cryptographic algorithms for
- **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)
- **XC20PKW**: XChaCha20-Poly1305
- Note: ChaChaPoly20-Poly1305 and XChaChaPoly20-Poly1305 is specified in [draft-amringer-jose-chacha-02](https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02)

### Compression Algorithms
- **DEFLATE**: (zip)
Expand Down
44 changes: 44 additions & 0 deletions Tests/JWATests/C20PTests.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())
}
}
8 changes: 4 additions & 4 deletions Tests/JWATests/XC20PTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
import JSONWebKey
import XCTest

final class C20PTests: XCTestCase {
final class XC20PTests: XCTestCase {

func testC20PCycle() throws {
func testXC20PCycle() throws {
let payload = "Test".data(using: .utf8)!
let encryptor = ContentEncryptionAlgorithm.c20PKW.encryptor
let decryptor = ContentEncryptionAlgorithm.c20PKW.decryptor
let encryptor = ContentEncryptionAlgorithm.xC20PKW.encryptor
let decryptor = ContentEncryptionAlgorithm.xC20PKW.decryptor
let key = try encryptor.generateCEK()
let iv = try encryptor.generateInitializationVector()
let aad = Data()
Expand Down
36 changes: 36 additions & 0 deletions Tests/JWETests/ECDH1PUTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,40 @@ final class ECDH1PUTests: XCTestCase {

XCTAssertEqual(payload.toHexString(), decrypted.toHexString())
}

func testECDH1PUA256KW_XC20PKWCycle() throws {
let payload = try "Test".tryToData()
let aliceKey = JWK.testingCurve25519KPair
let bobKey = JWK.testingCurve25519KPair

let keyAlg = KeyManagementAlgorithm.ecdh1PUA256KW
let encAlg = ContentEncryptionAlgorithm.xC20PKW

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 @@ -128,4 +128,40 @@ final class ECDHESTests: XCTestCase {

XCTAssertEqual(payload, decrypted)
}

func testECDHESA256KW_XC20PKWCycle() throws {
let payload = try "Test".tryToData()
let aliceKey = JWK.testingES256Pair
let bobKey = JWK.testingES256Pair

let keyAlg = KeyManagementAlgorithm.ecdhESA128KW
let encAlg = ContentEncryptionAlgorithm.xC20PKW

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