Skip to content

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 →