Argon2 Hash & Verify — Swift source
Hash passwords with Argon2id — the winner of the Password Hashing Competition. Configure memory, iterations, and parallelism. WASM-powered, client-side.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// argon2 — Argon2id password hashing (PHC string format).
//
// Language: Swift 5.9+ (Foundation)
// Ported from src/lib/argon2.ts
// display source — part of CosmoDev's polyglot tool pages
//
// The TS build drives the reference C library compiled to WASM
// (argon2-browser). Swift's system crypto (CryptoKit, CommonCrypto) has no
// Argon2, so — like the Ruby and Kotlin siblings — this port keeps the PHC
// parser, validator, Base64 codec, and verifier pure Swift and delegates the
// KDF itself to a native engine. The default engine drives the same reference
// C library through its CLI (`argon2`, github.com/P-H-C/phc-winner-argon2)
// via Foundation.Process; inject any other binding through the `Argon2Kdf`
// alias (e.g. a wrapper around a Swift Argon2 package).
//
// The CLI reads the password from stdin but takes the salt as a command-line
// *string*, and argv must be valid UTF-8 — raw random bytes are not. The
// default engine therefore salts with the 32-character lowercase hex
// rendering of a 16-byte random value: ASCII round-trips through argv, still
// carries 128 bits of entropy, and the PHC string records exactly those
// bytes. Bind a custom KDF for raw-byte salts.
//
// PHC string format (what `encoded` holds - the string you store in a DB):
// $argon2id$v=19$m=65536,t=3,p=1$<b64 salt>$<b64 digest>
// Salt and digest are unpadded standard Base64.
import Foundation
// MARK: - Constants
/// Defaults follow the OWASP-recommended Argon2id profile (64 MiB, 3 passes).
public enum Argon2Defaults {
public static let memory = 65_536 // KiB
public static let iterations = 3
public static let parallelism = 1
public static let hashLength = 32 // bytes
}
/// Random salt size in bytes (128 bits - the PHC recommendation). The default
/// engine hashes the hex rendering of these bytes (see the header note).
public let SALT_BYTES = 16
/// Argon2 variants, named and encoded as the C library does.
public enum Argon2Type: String, CaseIterable {
case argon2d
case argon2i
case argon2id
/// The reference CLI's type flag.
var cliFlag: String {
switch self {
case .argon2d: return "-d"
case .argon2i: return "-i"
case .argon2id: return "-id"
}
}
}
// MARK: - Types
/// Hashing parameters; every field optional, defaults come from Argon2Defaults.
public struct Argon2Options {
/// Memory cost in KiB (default 65536 = 64 MiB). Must be >= 1024.
public var memory: Int?
/// Time cost - passes over memory (default 3). Must be >= 1.
public var iterations: Int?
/// Parallelism - lanes (default 1). Must be >= 1.
public var parallelism: Int?
/// Digest length in bytes (default 32). Must be 16...64.
public var hashLength: Int?
/// Explicit salt bytes; a random 16-byte salt is generated when nil.
public var salt: [UInt8]?
public init(
memory: Int? = nil, iterations: Int? = nil, parallelism: Int? = nil,
hashLength: Int? = nil, salt: [UInt8]? = nil
) {
self.memory = memory
self.iterations = iterations
self.parallelism = parallelism
self.hashLength = hashLength
self.salt = salt
}
}
/// What `argon2Hash` returns.
public struct Argon2Result: Equatable {
/// Raw digest, lowercase hex (`hashLength` bytes).
public let hash: String
/// Self-contained PHC string - store this, verify against it.
public let encoded: String
/// Salt used, lowercase hex.
public let salt: String
}
/// Parameters extracted from a PHC string (`parseArgon2`'s return type).
public struct Argon2Params: Equatable {
public let type: Argon2Type
public let version: Int
public let memory: Int
public let iterations: Int
public let parallelism: Int
/// Salt, decoded from the embedded Base64 into lowercase hex.
public let salt: String
/// Digest, decoded from the embedded Base64 into lowercase hex ('' if absent).
public let hash: String
}
/// Thrown where the TS reference throws: malformed PHC strings, bad
/// parameters, or a failing KDF engine. `message` mirrors the TS text.
public struct Argon2Error: Error, CustomStringConvertible {
public let message: String
public var description: String { message }
}
// MARK: - Codec helpers
/// Lowercase hex of a byte array.
public func bytesToHex(_ bytes: [UInt8]) -> String {
bytes.map { String(format: "%02x", $0) }.joined()
}
/// Hex string -> bytes. Throws on odd length or non-hex characters.
func hexToBytes(_ hex: String) throws -> [UInt8] {
let chars = Array(hex)
if chars.count % 2 != 0 {
throw Argon2Error(message: "Invalid hex string: odd length")
}
var out: [UInt8] = []
out.reserveCapacity(chars.count / 2)
var i = 0
while i < chars.count {
guard let hi = chars[i].hexDigitValue, let lo = chars[i + 1].hexDigitValue else {
throw Argon2Error(message: "Invalid hex string: non-hex characters")
}
out.append(UInt8(hi << 4 | lo))
i += 2
}
return out
}
/// Unpadded standard Base64 (the PHC encoding) -> bytes. Throws on any
/// non-alphabet character or an impossible length (1 mod 4).
func phcBase64ToBytes(_ b64: String) throws -> [UInt8] {
if b64.isEmpty {
throw Argon2Error(message: "Invalid Argon2 string: empty Base64 field")
}
if b64.range(of: "^[A-Za-z0-9+/]+$", options: .regularExpression) == nil {
throw Argon2Error(message: "Invalid Argon2 string: non-Base64 characters")
}
if b64.count % 4 == 1 {
throw Argon2Error(message: "Invalid Argon2 string: impossible Base64 length")
}
// Re-pad to a multiple of 4 and let Foundation do the 6-bit arithmetic;
// the length checks above already mirror the TS byte counting.
let padded = b64 + String(repeating: "=", count: (4 - b64.count % 4) % 4)
guard let data = Data(base64Encoded: padded) else {
throw Argon2Error(message: "Invalid Argon2 string: non-Base64 characters")
}
return [UInt8](data)
}
/// Bytes -> unpadded standard Base64 (the PHC encoding).
func phcBase64FromBytes(_ bytes: [UInt8]) -> String {
let b64 = Data(bytes).base64EncodedString()
return String(b64.drop { $0 == "=" }.reversed().drop { $0 == "=" }.reversed())
}
// MARK: - Parsing
/**
Parse a PHC-format Argon2 string (`$argon2id$v=19$m=65536,t=3,p=1$salt$hash`)
into its typed parameters. Accepts argon2d / argon2i / argon2id. The digest
segment is optional (some encoders omit it); salt and hash are returned as
lowercase hex. Throws on any malformed input.
*/
public func parseArgon2(_ encoded: String) throws -> Argon2Params {
let pattern =
"^\\$(argon2(?:d|i|id))\\$v=(\\d+)\\$m=(\\d+),t=(\\d+),p=(\\d+)\\$([A-Za-z0-9+/]+)(?:\\$([A-Za-z0-9+/]+))?$"
guard let m = encoded.trimmingCharacters(in: .whitespacesAndNewlines)
.range(of: pattern, options: .regularExpression)
else {
throw Argon2Error(
message: "Invalid Argon2 string: expected $argon2id$v=19$m=…,t=…,p=…$salt$hash")
}
// Capture the groups out of the matched range.
let ns = encoded.trimmingCharacters(in: .whitespacesAndNewlines) as NSString
let regex = try NSRegularExpression(pattern: pattern)
let match = regex.firstMatch(
in: encoded.trimmingCharacters(in: .whitespacesAndNewlines), range: NSRange(location: 0, length: ns.length))!
var groups: [String] = []
for i in 1..<match.numberOfRanges {
if let r = Range(match.range(at: i), range: m, in: ns) {}
groups.append("")
}
// Simpler: re-run with NSTextCheckingResult capture extraction.
var caps: [String?] = []
for i in 1..<match.numberOfRanges {
let r = match.range(at: i)
caps.append(r.location == NSNotFound ? nil : ns.substring(with: r))
}
guard let typeRaw = caps[0], let type = Argon2Type(rawValue: typeRaw),
let version = Int(caps[1] ?? ""), let memory = Int(caps[2] ?? ""),
let iterations = Int(caps[3] ?? ""), let parallelism = Int(caps[4] ?? ""),
let saltB64 = caps[5]
else {
throw Argon2Error(message: "Invalid Argon2 string: expected $argon2id$v=19$m=…,t=…,p=…$salt$hash")
}
return Argon2Params(
type: type,
version: version,
memory: memory,
iterations: iterations,
parallelism: parallelism,
salt: bytesToHex(try phcBase64ToBytes(saltB64)),
hash: caps[6].map { bytesToHex(try phcBase64ToBytes($0)) } ?? ""
)
}
// MARK: - Options
/// Validate + normalise hashing parameters, throwing with a clear message.
func normalizeOptions(_ options: Argon2Options?) throws -> (
memory: Int, iterations: Int, parallelism: Int, hashLength: Int
) {
let memory = options?.memory ?? Argon2Defaults.memory
let iterations = options?.iterations ?? Argon2Defaults.iterations
let parallelism = options?.parallelism ?? Argon2Defaults.parallelism
let hashLength = options?.hashLength ?? Argon2Defaults.hashLength
if memory < 1024 { throw Argon2Error(message: "Memory must be at least 1024 KiB") }
if iterations < 1 { throw Argon2Error(message: "Iterations must be at least 1") }
if parallelism < 1 { throw Argon2Error(message: "Parallelism must be at least 1") }
if hashLength < 16 || hashLength > 64 {
throw Argon2Error(message: "Hash length must be between 16 and 64 bytes")
}
return (memory, iterations, parallelism, hashLength)
}
// MARK: - KDF engine
/// Computes a raw Argon2 digest. The default implementation drives the
/// reference `argon2` CLI; tests and alternate engines inject their own.
public typealias Argon2Kdf = (
_ password: [UInt8], _ salt: [UInt8],
_ type: Argon2Type, _ memoryKiB: Int, _ iterations: Int,
_ parallelism: Int, _ hashLength: Int
) throws -> [UInt8]
/**
The default KDF engine: run the reference `argon2` CLI.
- Password goes in on stdin (`-r` prints the raw digest bytes).
- The salt crosses as an argv string, so callers pass text-safe bytes —
`argon2Hash` passes the hex rendering of a 16-byte random value.
- `-k` takes memory in KiB directly (no log2 rounding needed).
*/
public func processArgon2Kdf(
password: [UInt8], salt: [UInt8], type: Argon2Type,
memoryKiB: Int, iterations: Int, parallelism: Int, hashLength: Int
) throws -> [UInt8] {
let saltString = String(decoding: salt, as: UTF8.self)
let out = try runProcess(
executable: "argon2",
arguments: [
saltString, type.cliFlag, "-v", "13", "-r",
"-k", String(memoryKiB), "-t", String(iterations),
"-p", String(parallelism), "-l", String(hashLength),
],
stdin: Data(password))
return [UInt8](out)
}
/// Spawn `executable`, feed `stdin`, wait, and return stdout bytes.
/// Non-zero exit throws with stderr's trimmed text.
private func runProcess(executable: String, arguments: [String], stdin: Data?) throws -> Data {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = [executable] + arguments
let inPipe = Pipe()
let outPipe = Pipe()
let errPipe = Pipe()
process.standardInput = inPipe
process.standardOutput = outPipe
process.standardError = errPipe
try process.run()
if let stdin = stdin {
inPipe.fileHandleForWriting.write(stdin)
}
inPipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
let out = outPipe.fileHandleForReading.readDataToEndOfFile()
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
let err = String(decoding: errData, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard process.terminationStatus == 0 else {
throw Argon2Error(message: "argon2 failed: \(err.isEmpty ? "exit \(process.terminationStatus)" : err)")
}
return out
}
// MARK: - Hash & verify
/**
Hash a password with Argon2id (hybrid of Argon2i's side-channel resistance
and Argon2d's GPU resistance - the Password Hashing Competition winner and
the recommended mode for password storage). Returns the digest (hex), the
salt used (hex), and the self-contained PHC string. A fresh random salt is
generated per call unless `options.salt` is given.
The TS reference is async only so it can await WASM instantiation; the CLI
call here is synchronous, and callers dispatch it to a background queue.
*/
public func argon2Hash(
_ password: String, options: Argon2Options? = nil,
kdf: @escaping Argon2Kdf = processArgon2Kdf
) throws -> Argon2Result {
let opts = try normalizeOptions(options)
// The default engine needs an argv-safe salt string, so the random salt
// is the hex rendering of SALT_BYTES random bytes (128 bits of entropy,
// 32 ASCII bytes in the PHC field). Explicit salts are used as given.
let saltBytes: [UInt8]
if let explicit = options?.salt {
saltBytes = explicit
} else {
var entropy = [UInt8](repeating: 0, count: SALT_BYTES)
for i in 0..<SALT_BYTES { entropy[i] = UInt8.random(in: .min ... .max) }
saltBytes = Array(bytesToHex(entropy).utf8)
}
let digest = try kdf(
Array(password.utf8), saltBytes, .argon2id,
opts.memory, opts.iterations, opts.parallelism, opts.hashLength)
let encoded =
"$argon2id$v=19$m=\(opts.memory),t=\(opts.iterations),p=\(opts.parallelism)"
+ "$\(phcBase64FromBytes(saltBytes))$\(phcBase64FromBytes(digest))"
return Argon2Result(hash: bytesToHex(digest), encoded: encoded, salt: bytesToHex(saltBytes))
}
/**
Verify a password against a PHC-format encoded hash (as produced by
`argon2Hash`). Returns true on match, false on mismatch; throws only on a
malformed encoded string or a KDF failure. Any Argon2 type (d/i/id) is
accepted - the type is read from the string itself, exactly like the C
library's argon2_verify, which this recompute-and-compare mirrors.
*/
public func argon2Verify(
_ encoded: String, _ password: String,
kdf: Argon2Kdf = processArgon2Kdf
) throws -> Bool {
let params = try parseArgon2(encoded) // validate format up front
guard !params.hash.isEmpty else {
// No digest segment: nothing to compare a recomputation against.
return false
}
let digest = try kdf(
Array(password.utf8), try hexToBytes(params.salt), params.type,
params.memory, params.iterations, params.parallelism, params.hash.count / 2)
return bytesToHex(digest) == params.hash
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →