Browse Source

Add XChaCha20 and XChaCha20-Poly1305 (IETF draft-irtf-cfrg-xchacha)

Zsombor Szabo 2 years ago
parent
commit
31f38545e4

+ 11 - 4
Sources/CryptoSwift/AEAD/AEADChaCha20Poly1305.swift

@@ -1,5 +1,5 @@
 //
-//  ChaCha20Poly1305.swift
+//  AEADChaCha20Poly1305.swift
 //  CryptoSwift
 //
 //  Copyright (C) 2014-2022 Marcin Krzyżanowski <marcin@krzyzanowskim.com>
@@ -24,7 +24,10 @@ public final class AEADChaCha20Poly1305: AEAD {
   /// Authenticated encryption
   public static func encrypt(_ plainText: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>, authenticationHeader: Array<UInt8>) throws -> (cipherText: Array<UInt8>, authenticationTag: Array<UInt8>) {
     let cipher = try ChaCha20(key: key, iv: iv)
+    return try self.encrypt(cipher: cipher, plainText, key: key, iv: iv, authenticationHeader: authenticationHeader)
+  }
 
+  public static func encrypt(cipher: Cipher, _ plainText: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>, authenticationHeader: Array<UInt8>) throws -> (cipherText: Array<UInt8>, authenticationTag: Array<UInt8>) {
     var polykey = Array<UInt8>(repeating: 0, count: kLen)
     var toEncrypt = polykey
     polykey = try cipher.encrypt(polykey)
@@ -40,9 +43,13 @@ public final class AEADChaCha20Poly1305: AEAD {
 
   /// Authenticated decryption
   public static func decrypt(_ cipherText: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>, authenticationHeader: Array<UInt8>, authenticationTag: Array<UInt8>) throws -> (plainText: Array<UInt8>, success: Bool) {
-    let chacha = try ChaCha20(key: key, iv: iv)
+    let cipher = try ChaCha20(key: key, iv: iv)
+    return try self.decrypt(cipher: cipher, cipherText: cipherText, key: key, iv: iv, authenticationHeader: authenticationHeader, authenticationTag: authenticationTag)
+  }
+
+  static func decrypt(cipher: Cipher, cipherText: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>, authenticationHeader: Array<UInt8>, authenticationTag: Array<UInt8>) throws -> (plainText: Array<UInt8>, success: Bool) {
 
-    let polykey = try chacha.encrypt(Array<UInt8>(repeating: 0, count: self.kLen))
+    let polykey = try cipher.encrypt(Array<UInt8>(repeating: 0, count: self.kLen))
     let mac = try calculateAuthenticationTag(authenticator: Poly1305(key: polykey), cipherText: cipherText, authenticationHeader: authenticationHeader)
     guard mac == authenticationTag else {
       return (cipherText, false)
@@ -52,7 +59,7 @@ public final class AEADChaCha20Poly1305: AEAD {
     toDecrypt += polykey
     toDecrypt += polykey
     toDecrypt += cipherText
-    let fullPlainText = try chacha.decrypt(toDecrypt)
+    let fullPlainText = try cipher.decrypt(toDecrypt)
     let plainText = Array(fullPlainText.dropFirst(64))
     return (plainText, true)
   }

+ 83 - 0
Sources/CryptoSwift/AEAD/AEADXChaCha20Poly1305.swift

@@ -0,0 +1,83 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+
+import Foundation
+
+/// This class implements the XChaCha20-Poly1305 Authenticated Encryption with
+/// Associated Data (AEAD_XCHACHA20_POLY1305) construction, providing both encryption and authentication.
+///
+/// For more information about the XChaCha20-Poly1305 algorithm, refer to the IETF draft: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
+public final class AEADXChaCha20Poly1305: AEAD {
+
+  /// The key length (in bytes) required for the XChaCha20 cipher (32 bytes).
+  public static let kLen = 32 // key length
+
+  /// The valid range of initialization vector lengths for the XChaCha20 cipher (12 bytes).
+  public static var ivRange = Range<Int>(12...12)
+
+  /// Encrypts the given plaintext using the XChaCha20 cipher and generates an authentication
+  /// tag using the Poly1305 MAC.
+  ///
+  /// - Parameters:
+  ///   - plainText: The plaintext to be encrypted.
+  ///   - key: The encryption key.
+  ///   - iv: The initialization vector.
+  ///   - authenticationHeader: The authentication header.
+  /// - Returns: A tuple containing the ciphertext and authentication tag.
+  /// - Throws: An error if encryption fails.
+  public static func encrypt(
+    _ plainText: Array<UInt8>,
+    key: Array<UInt8>,
+    iv: Array<UInt8>,
+    authenticationHeader: Array<UInt8>
+  ) throws -> (cipherText: Array<UInt8>, authenticationTag: Array<UInt8>) {
+    try AEADChaCha20Poly1305.encrypt(
+      cipher: XChaCha20(key: key, iv: iv),
+      plainText,
+      key: key,
+      iv: iv,
+      authenticationHeader: authenticationHeader
+    )
+  }
+
+  /// Decrypts the given ciphertext using the XChaCha20 cipher and verifies the authentication
+  /// tag using the Poly1305 MAC.
+  ///
+  /// - Parameters:
+  ///   - cipherText: The ciphertext to be decrypted.
+  ///   - key: The decryption key.
+  ///   - iv: The initialization vector.
+  ///   - authenticationHeader: The authentication header.
+  ///   - authenticationTag: The authentication tag.
+  /// - Returns: A tuple containing the decrypted plaintext and a boolean value indicating
+  ///   the success of the decryption and authentication process.
+  /// - Throws: An error if decryption fails.
+  public static func decrypt(
+    _ cipherText: Array<UInt8>,
+    key: Array<UInt8>,
+    iv: Array<UInt8>,
+    authenticationHeader: Array<UInt8>,
+    authenticationTag: Array<UInt8>
+  ) throws -> (plainText: Array<UInt8>, success: Bool) {
+    try AEADChaCha20Poly1305.decrypt(
+      cipher: XChaCha20(key: key, iv: iv),
+      cipherText: cipherText,
+      key: key,
+      iv: iv,
+      authenticationHeader: authenticationHeader,
+      authenticationTag: authenticationTag
+    )
+  }
+}

+ 7 - 3
Sources/CryptoSwift/ChaCha20.swift

@@ -28,7 +28,11 @@ public final class ChaCha20: BlockCipher {
   fileprivate let key: Key
   fileprivate var counter: Array<UInt8>
 
-  public init(key: Array<UInt8>, iv nonce: Array<UInt8>) throws {
+  public convenience init(key: Array<UInt8>, iv nonce: Array<UInt8>) throws {
+    try self.init(key: key, iv: nonce, blockCounter: 0)
+  }
+
+  init(key: Array<UInt8>, iv nonce: Array<UInt8>, blockCounter: UInt32 = 0) throws {
     precondition(nonce.count == 12 || nonce.count == 8)
 
     if key.count != 32 {
@@ -39,9 +43,9 @@ public final class ChaCha20: BlockCipher {
     self.keySize = self.key.count
 
     if nonce.count == 8 {
-      self.counter = [0, 0, 0, 0, 0, 0, 0, 0] + nonce
+      self.counter = blockCounter.bigEndian.bytes() + [0, 0, 0, 0] + nonce
     } else {
-      self.counter = [0, 0, 0, 0] + nonce
+      self.counter = blockCounter.bigEndian.bytes() + nonce
     }
 
     assert(self.counter.count == 16)

+ 29 - 0
Sources/CryptoSwift/Foundation/XChaCha20+Foundation.swift

@@ -0,0 +1,29 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+
+import Foundation
+
+extension XChaCha20 {
+  /// Convenience initializer that creates an XChaCha20 instance with the given key and IV
+  /// represented as hex-encoded strings.
+  ///
+  /// - Parameters:
+  ///   - key: The encryption/decryption key as a hex-encoded string.
+  ///   - iv: The initialization vector as a hex-encoded string.
+  /// - Throws: An error if the provided key or IV are of invalid length, format, or not hex-encoded.
+  public convenience init(key: String, iv: String) throws {
+    try self.init(key: key.bytes, iv: iv.bytes)
+  }
+}

+ 232 - 0
Sources/CryptoSwift/XChaCha20.swift

@@ -0,0 +1,232 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+
+/// XChaCha20 is a Swift implementation of the XChaCha20 stream cipher, which is an extension of the ChaCha20 cipher
+/// that uses a 192-bit nonce instead of the original 64-bit nonce. XChaCha20 provides a higher security level by
+/// allowing a larger number of safe random nonces, reducing the risk of nonce reuse.
+///
+/// For more information about the XChaCha20 algorithm, refer to the IETF draft: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
+public final class XChaCha20: BlockCipher, BlockMode {
+
+  public enum Error: Swift.Error {
+    case invalidKeyOrInitializationVector
+    case notSupported
+  }
+
+  fileprivate var chacha20: ChaCha20
+
+  // MARK: BlockCipher
+
+  public static let blockSize = 64 // 512 / 8
+
+  // MARK: Cipher
+
+  public let keySize: Int
+
+  /// Initializes a new instance of XChaCha20 with the provided key, nonce, and optional block counter.
+  /// - Parameters:
+  ///   - key: A 256-bit (32-byte) key for the XChaCha20 cipher.
+  ///   - nonce: A 192-bit (24-byte) nonce for the XChaCha20 cipher.
+  ///   - blockCounter: An optional initial block counter value, defaulting to 0.
+  /// - Throws: Error.invalidKeyOrInitializationVector if the key or nonce lengths are not valid.
+  public init(key: Array<UInt8>, iv nonce: Array<UInt8>, blockCounter: UInt32 = 0) throws {
+    guard key.count == 32 && nonce.count == 24 else {
+      throw Error.invalidKeyOrInitializationVector
+    }
+
+    self.keySize = key.count
+
+    // From https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.3
+    //  XChaCha20 can be constructed from an existing ChaCha20 implementation
+    //   and HChaCha20.  All one needs to do is:
+    //
+    //   1.  Pass the key and the first 16 bytes of the 24-byte nonce to
+    //       HChaCha20 to obtain the subkey.
+    //
+    //   2.  Use the subkey and remaining 8 byte nonce with ChaCha20 as normal
+    //       (prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte
+    //       nonce).
+    self.chacha20 = try .init(
+      key: XChaCha20.hChaCha20(key: key, nonce: Array(nonce[0..<16])),
+      iv: [0, 0, 0, 0] + Array(nonce[16..<24]),
+      blockCounter: blockCounter
+    )
+  }
+
+  // MARK: BlockMode
+
+  /// Options specific to the block mode.
+  public let options: BlockModeOption = [.none]
+  /// The custom block size for the block mode, if any. XChaCha20 does not have a custom block size.
+  public let customBlockSize: Int? = nil
+
+  public func worker(blockSize: Int, cipherOperation: @escaping CipherOperationOnBlock, encryptionOperation: @escaping CipherOperationOnBlock) throws -> CipherModeWorker {
+    return XChaCha20Worker(
+      blockSize: blockSize,
+      cipherOperation: cipherOperation,
+      xChaCha20: self
+    )
+  }
+
+  /// Computes the HChaCha20 function on the provided key and nonce.
+  ///
+  /// See: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2
+  ///
+  /// - Parameters:
+  ///   - key: A 256-bit (32-byte) key.
+  ///   - nonce: A 128-bit (16-byte) nonce.
+  /// - Returns: A 256-bit (32-byte) derived key.
+  static func hChaCha20(key: [UInt8], nonce: [UInt8]) -> [UInt8] {
+    precondition(key.count == 32)
+    precondition(nonce.count == 16)
+
+    // HChaCha20 is initialized the same way as the ChaCha cipher, except
+    // that HChaCha20 uses a 128-bit nonce and has no counter.  Instead, the
+    // block counter is replaced by the first 32 bits of the nonce.
+
+    var state = Array<UInt32>(repeating: 0, count: 16)
+
+    state[0] = 0x61707865
+    state[1] = 0x3320646e
+    state[2] = 0x79622d32
+    state[3] = 0x6b206574
+    for i in 0..<8 {
+      state[4 + i] = UInt32(bytes: key[i * 4..<(i + 1) * 4]).bigEndian
+    }
+    for i in 0..<4 {
+      state[12 + i] = UInt32(bytes: nonce[i * 4..<(i + 1) * 4]).bigEndian
+    }
+
+    // After initialization, proceed through the ChaCha rounds as usual.
+
+    for _ in 1...10 {
+      self.innerBlock(&state)
+    }
+
+    // Once the 20 ChaCha rounds have been completed, the first 128 bits and
+    // last 128 bits of the ChaCha state (both little-endian) are
+    // concatenated, and this 256-bit subkey is returned.
+
+    var output = Array<UInt8>()
+    for i in 0..<4 {
+      output += state[i].bigEndian.bytes()
+    }
+    for i in 0..<4 {
+      output += state[12 + i].bigEndian.bytes()
+    }
+
+    return output
+  }
+
+  /// Performs the "quarter round" operation on the provided state at the specified indices.
+  /// - Parameters:
+  ///   - state: The state on which to perform the operation.
+  ///   - a: The index of the first element in the state.
+  ///   - b: The index of the second element in the state.
+  ///   - c: The index of the third element in the state.
+  ///   - d: The index of the fourth element in the state.
+  static func qRound(_ state: inout [UInt32], _ a: Int, _ b: Int, _ c: Int, _ d: Int) {
+    state[a] = state[a] &+ state[b]
+    state[d] ^= state[a]
+    state[d] = (state[d] << 16) | (state[d] >> 16)
+    state[c] = state[c] &+ state[d]
+    state[b] ^= state[c]
+    state[b] = (state[b] << 12) | (state[b] >> 20)
+    state[a] = state[a] &+ state[b]
+    state[d] ^= state[a]
+    state[d] = (state[d] << 8) | (state[d] >> 24)
+    state[c] = state[c] &+ state[d]
+    state[b] ^= state[c]
+    state[b] = (state[b] << 7) | (state[b] >> 25)
+  }
+
+  /// Performs the inner block operation on the provided state.
+  /// - Parameter state: The state on which to perform the operation.
+  static func innerBlock(_ state: inout [UInt32]) {
+    self.qRound(&state, 0, 4, 8, 12)
+    self.qRound(&state, 1, 5, 9, 13)
+    self.qRound(&state, 2, 6, 10, 14)
+    self.qRound(&state, 3, 7, 11, 15)
+    self.qRound(&state, 0, 5, 10, 15)
+    self.qRound(&state, 1, 6, 11, 12)
+    self.qRound(&state, 2, 7, 8, 13)
+    self.qRound(&state, 3, 4, 9, 14)
+  }
+}
+
+// MARK: Cipher
+
+extension XChaCha20: Cipher {
+  public func encrypt(_ bytes: ArraySlice<UInt8>) throws -> Array<UInt8> {
+    try self.chacha20.encrypt(bytes)
+  }
+
+  public func decrypt(_ bytes: ArraySlice<UInt8>) throws -> Array<UInt8> {
+    try self.encrypt(bytes)
+  }
+}
+
+// MARK: Cryptors
+
+extension XChaCha20: Cryptors {
+
+  public func makeEncryptor() throws -> Cryptor & Updatable {
+    return try BlockEncryptor(
+      blockSize: XChaCha20.blockSize,
+      padding: .noPadding,
+      self.worker(
+        blockSize: XChaCha20.blockSize,
+        cipherOperation: { _ in nil },
+        encryptionOperation: { _ in nil }
+      )
+    )
+  }
+
+  public func makeDecryptor() throws -> Cryptor & Updatable {
+    return try BlockDecryptor(
+      blockSize: XChaCha20.blockSize,
+      padding: .noPadding,
+      self.worker(
+        blockSize: XChaCha20.blockSize,
+        cipherOperation: { _ in nil },
+        encryptionOperation: { _ in nil }
+      )
+    )
+  }
+}
+
+class XChaCha20Worker: CipherModeWorker {
+  let blockSize: Int
+  let cipherOperation: CipherOperationOnBlock
+  let xChaCha20: XChaCha20
+
+  init(blockSize: Int, cipherOperation: @escaping CipherOperationOnBlock, xChaCha20: XChaCha20) {
+    self.blockSize = blockSize
+    self.cipherOperation = cipherOperation
+    self.xChaCha20 = xChaCha20
+  }
+
+  var additionalBufferSize: Int {
+    return 0
+  }
+
+  func encrypt(block plaintext: ArraySlice<UInt8>) -> Array<UInt8> {
+    return (try? self.xChaCha20.encrypt(plaintext)) ?? .init()
+  }
+
+  func decrypt(block ciphertext: ArraySlice<UInt8>) -> Array<UInt8> {
+    return (try? self.xChaCha20.decrypt(ciphertext)) ?? .init()
+  }
+}

+ 78 - 0
Tests/CryptoSwiftTests/XChaCha20Poly1305Tests.swift

@@ -0,0 +1,78 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+
+import XCTest
+@testable import CryptoSwift
+
+class XChaCha20Poly1305Tests: XCTestCase {
+
+  /// See: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.3.1
+  func testRoundTrip() {
+    do {
+      let plaintext: Array<UInt8> = .init(
+        hex: """
+        4c616469657320616e642047656e746c656d656e206f662074686520636c6173
+        73206f66202739393a204966204920636f756c64206f6666657220796f75206f
+        6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73
+        637265656e20776f756c642062652069742e
+        """.replacingOccurrences(of: "\n", with: "")
+      )
+      let aad: Array<UInt8> = .init(
+        hex: "50515253c0c1c2c3c4c5c6c7")
+      let key: Array<UInt8> = .init(
+        hex: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
+      let iv: Array<UInt8> = .init(
+        hex: "404142434445464748494a4b4c4d4e4f5051525354555657")
+
+      let encryptResult = try AEADXChaCha20Poly1305.encrypt(
+        plaintext,
+        key: key,
+        iv: iv,
+        authenticationHeader: aad
+      )
+
+      XCTAssertEqual(
+        encryptResult.cipherText.toHexString(),
+        """
+        bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb
+        731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452
+        2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9
+        21f9664c97637da9768812f615c68b13b52e
+        """.replacingOccurrences(of: "\n", with: "")
+      )
+
+      XCTAssertEqual(
+        encryptResult.authenticationTag.toHexString(),
+        "c0875924c1c7987947deafd8780acf49"
+      )
+
+      let decryptResult = try AEADXChaCha20Poly1305.decrypt(
+        encryptResult.cipherText,
+        key: key,
+        iv: iv,
+        authenticationHeader: aad,
+        authenticationTag: encryptResult.authenticationTag
+      )
+
+      XCTAssertEqual(decryptResult.plainText, plaintext)
+    } catch {
+      XCTFail(error.localizedDescription)
+    }
+  }
+
+  static let allTests = [
+    ("Test Vector for AEAD_XCHACHA20_POLY1305", testRoundTrip)
+  ]
+}

+ 134 - 0
Tests/CryptoSwiftTests/XChaCha20Tests.swift

@@ -0,0 +1,134 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+
+import XCTest
+@testable import CryptoSwift
+
+final class XChaCha20Tests: XCTestCase {
+
+  /// See: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2.1
+  func testHChaCha20BlockFunction() {
+    let key: Array<UInt8> = .init(
+      hex: "00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:14:15:16:17:18:19:1a:1b:1c:1d:1e:1f".replacingOccurrences(of: ":", with: ""))
+    let counter: Array<UInt8> = .init(
+      hex: "00:00:00:09:00:00:00:4a:00:00:00:00:31:41:59:27".replacingOccurrences(of: ":", with: ""))
+    XCTAssertEqual(
+      XChaCha20.hChaCha20(key: key, nonce: counter).toHexString(),
+      """
+      82413b42 27b27bfe d30e4250 8a877d73
+      a0f9e4d5 8a74a853 c12ec413 26d3ecdc
+      """.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "\n", with: "")
+    )
+  }
+
+  // See: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.3.2.1
+
+  let plaintext: Array<UInt8> = .init(
+    hex: """
+    5468652064686f6c65202870726f6e6f756e6365642022646f6c652229206973
+    20616c736f206b6e6f776e2061732074686520417369617469632077696c6420
+    646f672c2072656420646f672c20616e642077686973746c696e6720646f672e
+    2049742069732061626f7574207468652073697a65206f662061204765726d61
+    6e20736865706865726420627574206c6f6f6b73206d6f7265206c696b652061
+    206c6f6e672d6c656767656420666f782e205468697320686967686c7920656c
+    757369766520616e6420736b696c6c6564206a756d70657220697320636c6173
+    736966696564207769746820776f6c7665732c20636f796f7465732c206a6163
+    6b616c732c20616e6420666f78657320696e20746865207461786f6e6f6d6963
+    2066616d696c792043616e696461652e
+    """.replacingOccurrences(of: "\n", with: ""))
+  let key: Array<UInt8> = .init(hex: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
+  let iv: Array<UInt8> = .init(hex: "404142434445464748494a4b4c4d4e4f5051525354555658")
+  let expectedResult0 = """
+  4559abba4e48c16102e8bb2c05e6947f50a786de162f9b0b7e592a9b53d0d4e9
+  8d8d6410d540a1a6375b26d80dace4fab52384c731acbf16a5923c0c48d3575d
+  4d0d2c673b666faa731061277701093a6bf7a158a8864292a41c48e3a9b4c0da
+  ece0f8d98d0d7e05b37a307bbb66333164ec9e1b24ea0d6c3ffddcec4f68e744
+  3056193a03c810e11344ca06d8ed8a2bfb1e8d48cfa6bc0eb4e2464b74814240
+  7c9f431aee769960e15ba8b96890466ef2457599852385c661f752ce20f9da0c
+  09ab6b19df74e76a95967446f8d0fd415e7bee2a12a114c20eb5292ae7a349ae
+  577820d5520a1f3fb62a17ce6a7e68fa7c79111d8860920bc048ef43fe84486c
+  cb87c25f0ae045f0cce1e7989a9aa220a28bdd4827e751a24a6d5c62d790a663
+  93b93111c1a55dd7421a10184974c7c5
+  """.replacingOccurrences(of: "\n", with: "")
+
+  func testXChaCha20() {
+    do {
+      let actualResult0 = try XChaCha20(key: key, iv: iv, blockCounter: 0).encrypt(self.plaintext).toHexString()
+      XCTAssertEqual(actualResult0, self.expectedResult0)
+
+      // See: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.3.2.2
+      let actualResult1 = try XChaCha20(key: key, iv: iv, blockCounter: 1).encrypt(self.plaintext).toHexString()
+      XCTAssertEqual(
+        actualResult1,
+        """
+        7d0a2e6b7f7c65a236542630294e063b7ab9b555a5d5149aa21e4ae1e4fbce87
+        ecc8e08a8b5e350abe622b2ffa617b202cfad72032a3037e76ffdcdc4376ee05
+        3a190d7e46ca1de04144850381b9cb29f051915386b8a710b8ac4d027b8b050f
+        7cba5854e028d564e453b8a968824173fc16488b8970cac828f11ae53cabd201
+        12f87107df24ee6183d2274fe4c8b1485534ef2c5fbc1ec24bfc3663efaa08bc
+        047d29d25043532db8391a8a3d776bf4372a6955827ccb0cdd4af403a7ce4c63
+        d595c75a43e045f0cce1f29c8b93bd65afc5974922f214a40b7c402cdb91ae73
+        c0b63615cdad0480680f16515a7ace9d39236464328a37743ffc28f4ddb324f4
+        d0f5bbdc270c65b1749a6efff1fbaa09536175ccd29fb9e6057b307320d31683
+        8a9c71f70b5b5907a66f7ea49aadc409
+        """.replacingOccurrences(of: "\n", with: "")
+      )
+    } catch {
+      XCTFail(error.localizedDescription)
+    }
+  }
+
+  func testXChaCha20PartialEncryption() {
+    do {
+      let cipher = try XChaCha20(key: key, iv: iv)
+      var ciphertext = Array<UInt8>()
+      var encryptor = try cipher.makeEncryptor()
+      try self.plaintext.batched(by: 8).forEach { chunk in
+        ciphertext += try encryptor.update(withBytes: chunk)
+      }
+      ciphertext += try encryptor.finish()
+      XCTAssertEqual(ciphertext.toHexString(), self.expectedResult0)
+    } catch {
+      XCTFail(error.localizedDescription)
+    }
+  }
+
+  func testXChaCha20PartialDecryption() {
+    do {
+      let cipher = try XChaCha20(key: key, iv: iv)
+      var plaintext = Array<UInt8>()
+      var decryptor = try cipher.makeDecryptor()
+      let ciphertext = Array<UInt8>(hex: expectedResult0)
+      try ciphertext.batched(by: 8).forEach { chunk in
+        plaintext += try decryptor.update(withBytes: chunk)
+      }
+      plaintext += try decryptor.finish()
+      XCTAssertEqual(plaintext, self.plaintext)
+    } catch {
+      XCTFail(error.localizedDescription)
+    }
+  }
+
+  static func allTests() -> [(String, (XChaCha20Tests) -> () -> Void)] {
+    let tests = [
+      ("Test Vector for the HChaCha20 Block Function", testHChaCha20BlockFunction),
+      ("Test Vectors for XChaCha20", testXChaCha20),
+      ("XChaCha20 partial encryption", testXChaCha20PartialEncryption),
+      ("XChaCha20 partial decryption", testXChaCha20PartialDecryption),
+    ]
+
+    return tests
+  }
+}