Skip to content
Open
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
58 changes: 58 additions & 0 deletions Sources/CryptoExtras/Key Derivation/Argon2/Argon2.swift
Original file line number Diff line number Diff line change
@@ -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<Passphrase: DataProtocol, Salt: DataProtocol>(
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<P: DataProtocol, S: DataProtocol>(
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: DataProtocol>(_ 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..<p {
for j in 0..<2 {
var input = Data(h0)
input.append(contentsOf: withUnsafeBytes(of: UInt32(j).littleEndian) { Data($0) })
input.append(contentsOf: withUnsafeBytes(of: UInt32(lane).littleEndian) { Data($0) })
blocks[lane * q + j] = dataToBlock(hPrime(data: input, length: 1024))
}
}

for pass in 0..<t {
for slice in 0..<4 {
let sliceLen = q / 4
var generators: [IndexGenerator?] = (0..<p).map { lane in
if variant == .i || (variant == .id && pass == 0 && slice < 2) {
return IndexGenerator(pass: pass, lane: lane, slice: slice, m_prime: m_prime, iterations: t, variant: variant)
}
return nil
}
for col in 0..<sliceLen {
let j = slice * sliceLen + col
if pass == 0 && j < 2 { continue }
for lane in 0..<p {
let (l, z) = computeIndices(pass: pass, lane: lane, slice: slice, col: col, sliceLen: sliceLen, p: p, q: q, variant: variant, blocks: blocks, generator: &generators[lane])
let prevCol = (j == 0 ? q - 1 : j - 1)
let nextBlock = g(x: blocks[lane * q + prevCol], y: blocks[l * q + z])
if pass == 0 { blocks[lane * q + j] = nextBlock }
else { blocks[lane * q + j].xor(with: nextBlock) }
}
}
}
}

var finalBlock = blocks[0 * q + (q - 1)]
for i in 1..<p { finalBlock.xor(with: blocks[i * q + (q - 1)]) }
return hPrime(data: blockToData(finalBlock), length: outputLength)
}

private static func g(x: Block, y: Block) -> 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..<r-1 { v = Blake2b.hash(data: v, outLength: 64); result.append(v.prefix(32)) }
v = Blake2b.hash(data: v, outLength: length - 32 * r); result.append(v)
return result
}

private static func computeIndices(pass: Int, lane: Int, slice: Int, col: Int, sliceLen: Int, p: Int, q: Int, variant: Variant, blocks: [Block], generator: inout IndexGenerator?) -> (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
}
}
75 changes: 75 additions & 0 deletions Sources/CryptoExtras/Key Derivation/Argon2/Native/Blake2b.swift
Original file line number Diff line number Diff line change
@@ -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<D: DataProtocol>(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..<blockCount {
let isLast = i == blockCount - 1; var block = [UInt64](repeating: 0, count: 16); let start = i * blockBytes
let end = min(start + blockBytes, buffer.count); let len = end - start; t += UInt64(len)
for j in 0..<16 {
let offset = j * 8; var val: UInt64 = 0
for k in 0..<8 { let idx = start + offset + k; if idx < buffer.count { val |= UInt64(buffer[idx]) << (k * 8) } }
block[j] = val
}
compress(h: &h, m: block, t: t, f: isLast)
}
var result = Data()
for i in 0..<((outLength + 7) / 8) {
var val = h[i]; for _ in 0..<min(8, outLength - i * 8) { result.append(UInt8(val & 0xff)); val >>= 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)) }
}
52 changes: 52 additions & 0 deletions Tests/CryptoExtrasTests/Argon2Tests.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
}