diff --git a/Sources/CryptoExtras/Key Derivation/Argon2/Argon2.swift b/Sources/CryptoExtras/Key Derivation/Argon2/Argon2.swift new file mode 100644 index 000000000..142710594 --- /dev/null +++ b/Sources/CryptoExtras/Key Derivation/Argon2/Argon2.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Crypto +import Foundation + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension KDF { + /// An implementation of the Argon2id key derivation function as defined in RFC 9106. + public enum Argon2id: Sendable { + /// Derives a symmetric key using the Argon2id algorithm. + /// + /// - Parameters: + /// - password: The passphrase used as a basis for the key. + /// - salt: The salt to use for key derivation (recommended at least 16 bytes). + /// - outputByteCount: The length in bytes of the resulting symmetric key. + /// - iterations: The number of passes over memory (time cost). + /// - memoryByteCount: The memory cost in bytes. + /// - parallelism: The number of independent lanes. + /// - secret: Optional secret data (key) to be hashed. + /// - associatedData: Optional additional associated data to be hashed. + /// - Returns: The derived symmetric key. + public static func deriveKey( + from password: Passphrase, + salt: Salt, + outputByteCount: Int, + iterations: Int, + memoryByteCount: Int, + parallelism: Int, + secret: Data? = nil, + associatedData: Data? = nil + ) throws -> SymmetricKey { + let hash = try Argon2NativeImplementation.hash( + password: password, + salt: salt, + iterations: iterations, + memoryBytes: memoryByteCount, + parallelism: parallelism, + outputLength: outputByteCount, + variant: .id, + secret: secret, + associatedData: associatedData + ) + return SymmetricKey(data: hash) + } + } +} diff --git a/Sources/CryptoExtras/Key Derivation/Argon2/Native/Argon2id+Native.swift b/Sources/CryptoExtras/Key Derivation/Argon2/Native/Argon2id+Native.swift new file mode 100644 index 000000000..2bd11d50f --- /dev/null +++ b/Sources/CryptoExtras/Key Derivation/Argon2/Native/Argon2id+Native.swift @@ -0,0 +1,207 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Crypto + +internal enum Argon2NativeImplementation { + enum Variant: Int { case d = 0, i = 1, id = 2 } + + struct Block { + var v: [UInt64] + init() { self.v = [UInt64](repeating: 0, count: 128) } + mutating func xor(with other: Block) { for i in 0..<128 { v[i] ^= other.v[i] } } + } + + /// Pure Swift implementation of Argon2id as defined in RFC 9106. + /// See: https://www.rfc-editor.org/rfc/rfc9106.html + static func hash( + password: P, salt: S, iterations: Int, memoryBytes: Int, parallelism: Int, outputLength: Int, + variant: Variant, secret: Data? = nil, associatedData: Data? = nil + ) throws -> Data { + let m = memoryBytes / 1024 + let p = parallelism; let t = iterations + let m_prime = 4 * p * (m / (4 * p)); let q = m_prime / p + + var h0Input = Data() + h0Input.append(contentsOf: withUnsafeBytes(of: UInt32(p).littleEndian) { Data($0) }) + h0Input.append(contentsOf: withUnsafeBytes(of: UInt32(outputLength).littleEndian) { Data($0) }) + h0Input.append(contentsOf: withUnsafeBytes(of: UInt32(m).littleEndian) { Data($0) }) + h0Input.append(contentsOf: withUnsafeBytes(of: UInt32(t).littleEndian) { Data($0) }) + h0Input.append(contentsOf: withUnsafeBytes(of: UInt32(0x13).littleEndian) { Data($0) }) + h0Input.append(contentsOf: withUnsafeBytes(of: UInt32(variant.rawValue).littleEndian) { Data($0) }) + + func appendData(_ d: D) { + h0Input.append(contentsOf: withUnsafeBytes(of: UInt32(d.count).littleEndian) { Data($0) }) + h0Input.append(contentsOf: d) + } + + appendData(password); appendData(salt) + appendData(secret ?? Data()); appendData(associatedData ?? Data()) + + let h0 = Blake2b.hash(data: h0Input, outLength: 64) + var blocks = [Block](repeating: Block(), count: p * q) + + for lane in 0..

Block { + var r = Block() + for i in 0..<128 { r.v[i] = x.v[i] ^ y.v[i] } + let originalR = r + + for i in 0..<8 { + applyPRound(&r.v, (0..<8).map { 8 * i + $0 }) + } + for i in 0..<8 { + applyPRound(&r.v, (0..<8).map { i + 8 * $0 }) + } + + for i in 0..<128 { r.v[i] ^= originalR.v[i] } + return r + } + + private static func applyPRound(_ v: inout [UInt64], _ indices: [Int]) { + var s = [UInt64](repeating: 0, count: 16) + for i in 0..<8 { s[2*i] = v[2*indices[i]]; s[2*i+1] = v[2*indices[i]+1] } + + func callGB(_ i0: Int, _ i1: Int, _ i2: Int, _ i3: Int) { + var a = s[i0], b = s[i1], c = s[i2], d = s[i3] + gb(&a, &b, &c, &d) + s[i0] = a; s[i1] = b; s[i2] = c; s[i3] = d + } + + callGB(0, 4, 8, 12) + callGB(1, 5, 9, 13) + callGB(2, 6, 10, 14) + callGB(3, 7, 11, 15) + + callGB(0, 5, 10, 15) + callGB(1, 6, 11, 12) + callGB(2, 7, 8, 13) + callGB(3, 4, 9, 14) + + for i in 0..<8 { v[2*indices[i]] = s[2*i]; v[2*indices[i]+1] = s[2*i+1] } + } + + private static func gb(_ a: inout UInt64, _ b: inout UInt64, _ c: inout UInt64, _ d: inout UInt64) { + func f(_ x: UInt64, _ y: UInt64) -> UInt64 { + let x32 = x & 0xFFFFFFFF; let y32 = y & 0xFFFFFFFF + return x &+ y &+ (2 &* x32 &* y32) + } + a = f(a, b); d = rotateRight(d ^ a, by: 32) + c = f(c, d); b = rotateRight(b ^ c, by: 24) + a = f(a, b); d = rotateRight(d ^ a, by: 16) + c = f(c, d); b = rotateRight(b ^ c, by: 63) + } + + private static func rotateRight(_ value: UInt64, by: Int) -> UInt64 { return (value >> by) | (value << (64 - by)) } + + private static func hPrime(data: Data, length: Int) -> Data { + if length <= 64 { + return Blake2b.hash(data: withUnsafeBytes(of: UInt32(length).littleEndian) { Data($0) } + data, outLength: length) + } + let r = (length + 31) / 32 - 2 + var result = Data() + var v = Blake2b.hash(data: withUnsafeBytes(of: UInt32(length).littleEndian) { Data($0) } + data, outLength: 64) + result.append(v.prefix(32)) + for _ in 0.. (Int, Int) { + var j1: UInt32 = 0; var j2: UInt32 = 0 + if generator != nil { (j1, j2) = generator!.nextPair() } + else { + let j = slice * sliceLen + col; let prevCol = (j - 1 + q) % q + let v0 = blocks[lane * q + prevCol].v[0] + j1 = UInt32(v0 & 0xFFFFFFFF); j2 = UInt32(v0 >> 32) + } + let l = (pass == 0 && slice == 0) ? lane : Int(j2 % UInt32(p)) + var refSize: Int + if pass == 0 { refSize = (l == lane) ? (slice * sliceLen + col - 1) : (slice * sliceLen - (col == 0 ? 1 : 0)) } + else { refSize = (l == lane) ? (q - sliceLen + col - 1) : (q - sliceLen - (col == 0 ? 1 : 0)) } + if refSize < 1 { refSize = 1 } + let z = (UInt64(refSize) * ((UInt64(j1) * UInt64(j1)) >> 32)) >> 32 + let relPos = refSize - 1 - Int(z) + let absZ = (pass == 0) ? relPos : ((slice + 1) * sliceLen + relPos) % q + return (l, absZ) + } + + private struct IndexGenerator { + var blocks: [UInt64]; var index: Int + init(pass: Int, lane: Int, slice: Int, m_prime: Int, iterations: Int, variant: Variant) { + var input = Block() + input.v[0] = UInt64(pass); input.v[1] = UInt64(lane); input.v[2] = UInt64(slice) + input.v[3] = UInt64(m_prime); input.v[4] = UInt64(iterations); input.v[5] = UInt64(variant.rawValue) + self.blocks = []; self.index = 0; let zero = Block() + for i in 1...100 { input.v[6] = UInt64(i); self.blocks.append(contentsOf: g(x: zero, y: g(x: zero, y: input)).v) } + } + mutating func nextPair() -> (UInt32, UInt32) { + let j1 = UInt32(blocks[index] & 0xFFFFFFFF); let j2 = UInt32(blocks[index] >> 32) + index += 1; return (j1, j2) + } + } + + private static func dataToBlock(_ data: Data) -> Block { + var block = Block(); let bytes = [UInt8](data) + for i in 0..<128 { + let offset = i * 8; var val: UInt64 = 0 + for k in 0..<8 { val |= UInt64(bytes[offset + k]) << (k * 8) } + block.v[i] = val + } + return block + } + + private static func blockToData(_ block: Block) -> Data { + var data = Data() + for i in 0..<128 { withUnsafeBytes(of: block.v[i].littleEndian) { data.append(contentsOf: $0) } } + return data + } +} diff --git a/Sources/CryptoExtras/Key Derivation/Argon2/Native/Blake2b.swift b/Sources/CryptoExtras/Key Derivation/Argon2/Native/Blake2b.swift new file mode 100644 index 000000000..10d337f71 --- /dev/null +++ b/Sources/CryptoExtras/Key Derivation/Argon2/Native/Blake2b.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Crypto + +internal enum Blake2b { + static let blockBytes = 128 + static let outBytes = 64 + static let iv: [UInt64] = [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179] + static let sigma: [[Int]] = [ + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ], [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 ], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 ], [ 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 ], + [ 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 ], [ 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 ], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 ], [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 ], + [ 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 ], [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 ], + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ], [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 ] + ] + + static func hash(data: D, outLength: Int) -> Data { + var h = iv; h[0] ^= UInt64(0x01010000) ^ UInt64(outLength); let buffer = [UInt8](data) + let blockCount = buffer.count == 0 ? 1 : (buffer.count + blockBytes - 1) / blockBytes + var t: UInt64 = 0 + for i in 0..>= 8 } + } + return result + } + + private static func compress(h: inout [UInt64], m: [UInt64], t: UInt64, f: Bool) { + var v = [UInt64](repeating: 0, count: 16); for i in 0..<8 { v[i] = h[i]; v[i + 8] = iv[i] } + v[12] ^= t; if f { v[14] ^= 0xffffffffffffffff } + for i in 0..<12 { + let row = sigma[i] + mixStep(v: &v, a: 0, b: 4, c: 8, d: 12, x: m[row[0]], y: m[row[1]]) + mixStep(v: &v, a: 1, b: 5, c: 9, d: 13, x: m[row[2]], y: m[row[3]]) + mixStep(v: &v, a: 2, b: 6, c: 10, d: 14, x: m[row[4]], y: m[row[5]]) + mixStep(v: &v, a: 3, b: 7, c: 11, d: 15, x: m[row[6]], y: m[row[7]]) + mixStep(v: &v, a: 0, b: 5, c: 10, d: 15, x: m[row[8]], y: m[row[9]]) + mixStep(v: &v, a: 1, b: 6, c: 11, d: 12, x: m[row[10]], y: m[row[11]]) + mixStep(v: &v, a: 2, b: 7, c: 8, d: 13, x: m[row[12]], y: m[row[13]]) + mixStep(v: &v, a: 3, b: 4, c: 9, d: 14, x: m[row[14]], y: m[row[15]]) + } + for i in 0..<8 { h[i] ^= v[i] ^ v[i + 8] } + } + + private static func mixStep(v: inout [UInt64], a: Int, b: Int, c: Int, d: Int, x: UInt64, y: UInt64) { + v[a] = v[a] &+ v[b] &+ x; v[d] = rotateRight(v[d] ^ v[a], by: 32); v[c] = v[c] &+ v[d]; v[b] = rotateRight(v[b] ^ v[c], by: 24) + v[a] = v[a] &+ v[b] &+ y; v[d] = rotateRight(v[d] ^ v[a], by: 16); v[c] = v[c] &+ v[d]; v[b] = rotateRight(v[b] ^ v[c], by: 63) + } + + private static func rotateRight(_ value: UInt64, by: Int) -> UInt64 { return (value >> by) | (value << (64 - by)) } +} diff --git a/Tests/CryptoExtrasTests/Argon2Tests.swift b/Tests/CryptoExtrasTests/Argon2Tests.swift new file mode 100644 index 000000000..680f02957 --- /dev/null +++ b/Tests/CryptoExtrasTests/Argon2Tests.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import XCTest +import Crypto +import CryptoExtras + +final class Argon2Tests: XCTestCase { + /// Verified against official RFC 9106 test vectors. + /// See: https://www.rfc-editor.org/rfc/rfc9106.html#section-5.3 + func testRFC9106Argon2idTestVector() throws { + // From RFC 9106, Section 5.3 + let password = Data(repeating: 0x01, count: 32) + let salt = Data(repeating: 0x02, count: 16) + let secret = Data(repeating: 0x03, count: 8) + let ad = Data(repeating: 0x04, count: 12) + + // Settings: Argon2id, v=13, m=32, t=3, p=4 + let key = try KDF.Argon2id.deriveKey( + from: password, + salt: salt, + outputByteCount: 32, + iterations: 3, + memoryByteCount: 32 * 1024, // 32 KiB + parallelism: 4, + secret: secret, + associatedData: ad + ) + + let expectedHash = Data([ + 0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, + 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9, + 0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e, + 0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01, 0xe6, 0x59 + ]) + + key.withUnsafeBytes { + XCTAssertEqual(Data($0), expectedHash, "Hash should match RFC 9106 test vector (Section 5.3)") + } + } +}