| 1 | // https://datatracker.ietf.org/doc/rfc9106 |
| 2 | // https://github.com/golang/crypto/tree/master/argon2 |
| 3 | // https://github.com/P-H-C/phc-winner-argon2 |
| 4 | |
| 5 | const builtin = @import("builtin"); |
| 6 | |
| 7 | const std = @import("std"); |
| 8 | const blake2 = crypto.hash.blake2; |
| 9 | const crypto = std.crypto; |
| 10 | const Io = std.Io; |
| 11 | const math = std.math; |
| 12 | const mem = std.mem; |
| 13 | const phc_format = pwhash.phc_format; |
| 14 | const pwhash = crypto.pwhash; |
| 15 | const Blake2b512 = blake2.Blake2b512; |
| 16 | const Blocks = std.array_list.AlignedManaged([block_length]u64, .@"16"); |
| 17 | const H0 = [Blake2b512.digest_length + 8]u8; |
| 18 | |
| 19 | const EncodingError = crypto.errors.EncodingError; |
| 20 | const KdfError = pwhash.KdfError; |
| 21 | const HasherError = pwhash.HasherError; |
| 22 | const Error = pwhash.Error; |
| 23 | |
| 24 | const version = 0x13; |
| 25 | const block_length = 128; |
| 26 | const sync_points = 4; |
| 27 | const max_int = 0xffff_ffff; |
| 28 | |
| 29 | const default_salt_len = 32; |
| 30 | const default_hash_len = 32; |
| 31 | const max_salt_len = 64; |
| 32 | const max_hash_len = 64; |
| 33 | |
| 34 | /// Argon2 type |
| 35 | pub const Mode = enum { |
| 36 | /// Argon2d is faster and uses data-depending memory access, which makes it highly resistant |
| 37 | /// against GPU cracking attacks and suitable for applications with no threats from side-channel |
| 38 | /// timing attacks (eg. cryptocurrencies). |
| 39 | argon2d, |
| 40 | |
| 41 | /// Argon2i instead uses data-independent memory access, which is preferred for password |
| 42 | /// hashing and password-based key derivation, but it is slower as it makes more passes over |
| 43 | /// the memory to protect from tradeoff attacks. |
| 44 | argon2i, |
| 45 | |
| 46 | /// Argon2id is a hybrid of Argon2i and Argon2d, using a combination of data-depending and |
| 47 | /// data-independent memory accesses, which gives some of Argon2i's resistance to side-channel |
| 48 | /// cache timing attacks and much of Argon2d's resistance to GPU cracking attacks. |
| 49 | argon2id, |
| 50 | }; |
| 51 | |
| 52 | /// Argon2 parameters |
| 53 | pub const Params = struct { |
| 54 | const Self = @This(); |
| 55 | |
| 56 | /// Time cost, which defines the amount of computation realized and therefore the execution |
| 57 | /// time, given in number of iterations. |
| 58 | t: u32, |
| 59 | |
| 60 | /// Memory cost, which defines the memory usage, given in kibibytes. |
| 61 | m: u32, |
| 62 | |
| 63 | /// Parallelism degree, which defines the number of independent tasks, |
| 64 | /// to be multiplexed onto threads when possible. |
| 65 | p: u24, |
| 66 | |
| 67 | /// The secret parameter, which is used for keyed hashing. This allows a secret key to be input |
| 68 | /// at hashing time (from some external location) and be folded into the value of the hash. This |
| 69 | /// means that even if your salts and hashes are compromised, an attacker cannot brute-force to |
| 70 | /// find the password without the key. |
| 71 | secret: ?[]const u8 = null, |
| 72 | |
| 73 | /// The ad parameter, which is used to fold any additional data into the hash value. Functionally, |
| 74 | /// this behaves almost exactly like the secret or salt parameters; the ad parameter is folding |
| 75 | /// into the value of the hash. However, this parameter is used for different data. The salt |
| 76 | /// should be a random string stored alongside your password. The secret should be a random key |
| 77 | /// only usable at hashing time. The ad is for any other data. |
| 78 | ad: ?[]const u8 = null, |
| 79 | |
| 80 | /// Baseline parameters for interactive logins using argon2i type |
| 81 | pub const interactive_2i = Self.fromLimits(4, 33554432); |
| 82 | /// Baseline parameters for normal usage using argon2i type |
| 83 | pub const moderate_2i = Self.fromLimits(6, 134217728); |
| 84 | /// Baseline parameters for offline usage using argon2i type |
| 85 | pub const sensitive_2i = Self.fromLimits(8, 536870912); |
| 86 | |
| 87 | /// Baseline parameters for interactive logins using argon2id type |
| 88 | pub const interactive_2id = Self.fromLimits(2, 67108864); |
| 89 | /// Baseline parameters for normal usage using argon2id type |
| 90 | pub const moderate_2id = Self.fromLimits(3, 268435456); |
| 91 | /// Baseline parameters for offline usage using argon2id type |
| 92 | pub const sensitive_2id = Self.fromLimits(4, 1073741824); |
| 93 | |
| 94 | /// Recommended parameters for argon2id type according to the |
| 95 | /// [OWASP cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). |
| 96 | pub const owasp_2id = Self{ .t = 2, .m = 19 * 1024, .p = 1 }; |
| 97 | |
| 98 | /// Create parameters from ops and mem limits, where mem_limit given in bytes |
| 99 | pub fn fromLimits(ops_limit: u32, mem_limit: usize) Self { |
| 100 | const m = mem_limit / 1024; |
| 101 | std.debug.assert(m <= max_int); |
| 102 | return .{ .t = ops_limit, .m = @as(u32, @intCast(m)), .p = 1 }; |
| 103 | } |
| 104 | }; |
| 105 | |
| 106 | fn initHash( |
| 107 | password: []const u8, |
| 108 | salt: []const u8, |
| 109 | params: Params, |
| 110 | dk_len: usize, |
| 111 | mode: Mode, |
| 112 | ) H0 { |
| 113 | var h0: H0 = undefined; |
| 114 | var parameters: [24]u8 = undefined; |
| 115 | var tmp: [4]u8 = undefined; |
| 116 | var b2 = Blake2b512.init(.{}); |
| 117 | mem.writeInt(u32, parameters[0..4], params.p, .little); |
| 118 | mem.writeInt(u32, parameters[4..8], @as(u32, @intCast(dk_len)), .little); |
| 119 | mem.writeInt(u32, parameters[8..12], params.m, .little); |
| 120 | mem.writeInt(u32, parameters[12..16], params.t, .little); |
| 121 | mem.writeInt(u32, parameters[16..20], version, .little); |
| 122 | mem.writeInt(u32, parameters[20..24], @backingInt(mode), .little); |
| 123 | b2.update(&parameters); |
| 124 | mem.writeInt(u32, &tmp, @as(u32, @intCast(password.len)), .little); |
| 125 | b2.update(&tmp); |
| 126 | b2.update(password); |
| 127 | mem.writeInt(u32, &tmp, @as(u32, @intCast(salt.len)), .little); |
| 128 | b2.update(&tmp); |
| 129 | b2.update(salt); |
| 130 | const secret = params.secret orelse ""; |
| 131 | std.debug.assert(secret.len <= max_int); |
| 132 | mem.writeInt(u32, &tmp, @as(u32, @intCast(secret.len)), .little); |
| 133 | b2.update(&tmp); |
| 134 | b2.update(secret); |
| 135 | const ad = params.ad orelse ""; |
| 136 | std.debug.assert(ad.len <= max_int); |
| 137 | mem.writeInt(u32, &tmp, @as(u32, @intCast(ad.len)), .little); |
| 138 | b2.update(&tmp); |
| 139 | b2.update(ad); |
| 140 | b2.final(h0[0..Blake2b512.digest_length]); |
| 141 | return h0; |
| 142 | } |
| 143 | |
| 144 | fn blake2bLong(out: []u8, in: []const u8) void { |
| 145 | const H = Blake2b512; |
| 146 | var outlen_bytes: [4]u8 = undefined; |
| 147 | mem.writeInt(u32, &outlen_bytes, @as(u32, @intCast(out.len)), .little); |
| 148 | |
| 149 | var out_buf: [H.digest_length]u8 = undefined; |
| 150 | |
| 151 | if (out.len <= H.digest_length) { |
| 152 | var h = H.init(.{ .expected_out_bits = out.len * 8 }); |
| 153 | h.update(&outlen_bytes); |
| 154 | h.update(in); |
| 155 | h.final(&out_buf); |
| 156 | @memcpy(out, out_buf[0..out.len]); |
| 157 | return; |
| 158 | } |
| 159 | |
| 160 | var h = H.init(.{}); |
| 161 | h.update(&outlen_bytes); |
| 162 | h.update(in); |
| 163 | h.final(&out_buf); |
| 164 | var out_slice = out; |
| 165 | out_slice[0 .. H.digest_length / 2].* = out_buf[0 .. H.digest_length / 2].*; |
| 166 | out_slice = out_slice[H.digest_length / 2 ..]; |
| 167 | |
| 168 | var in_buf: [H.digest_length]u8 = undefined; |
| 169 | while (out_slice.len > H.digest_length) { |
| 170 | in_buf = out_buf; |
| 171 | H.hash(&in_buf, &out_buf, .{}); |
| 172 | out_slice[0 .. H.digest_length / 2].* = out_buf[0 .. H.digest_length / 2].*; |
| 173 | out_slice = out_slice[H.digest_length / 2 ..]; |
| 174 | } |
| 175 | in_buf = out_buf; |
| 176 | H.hash(&in_buf, &out_buf, .{ .expected_out_bits = out_slice.len * 8 }); |
| 177 | @memcpy(out_slice, out_buf[0..out_slice.len]); |
| 178 | } |
| 179 | |
| 180 | fn initBlocks( |
| 181 | blocks: *Blocks, |
| 182 | h0: *H0, |
| 183 | memory: u32, |
| 184 | threads: u24, |
| 185 | ) void { |
| 186 | var block0: [1024]u8 = undefined; |
| 187 | var lane: u24 = 0; |
| 188 | while (lane < threads) : (lane += 1) { |
| 189 | const j = lane * (memory / threads); |
| 190 | mem.writeInt(u32, h0[Blake2b512.digest_length + 4 ..][0..4], lane, .little); |
| 191 | |
| 192 | mem.writeInt(u32, h0[Blake2b512.digest_length..][0..4], 0, .little); |
| 193 | blake2bLong(&block0, h0); |
| 194 | for (&blocks.items[j + 0], 0..) |*v, i| { |
| 195 | v.* = mem.readInt(u64, block0[i * 8 ..][0..8], .little); |
| 196 | } |
| 197 | |
| 198 | mem.writeInt(u32, h0[Blake2b512.digest_length..][0..4], 1, .little); |
| 199 | blake2bLong(&block0, h0); |
| 200 | for (&blocks.items[j + 1], 0..) |*v, i| { |
| 201 | v.* = mem.readInt(u64, block0[i * 8 ..][0..8], .little); |
| 202 | } |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | fn processBlocks( |
| 207 | blocks: *Blocks, |
| 208 | time: u32, |
| 209 | memory: u32, |
| 210 | threads: u24, |
| 211 | mode: Mode, |
| 212 | io: Io, |
| 213 | ) Io.Cancelable!void { |
| 214 | const lanes = memory / threads; |
| 215 | const segments = lanes / sync_points; |
| 216 | |
| 217 | if (builtin.single_threaded or threads == 1) { |
| 218 | processBlocksSync(blocks, time, memory, threads, mode, lanes, segments); |
| 219 | } else { |
| 220 | try processBlocksAsync(blocks, time, memory, threads, mode, lanes, segments, io); |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | fn processBlocksSync( |
| 225 | blocks: *Blocks, |
| 226 | time: u32, |
| 227 | memory: u32, |
| 228 | threads: u24, |
| 229 | mode: Mode, |
| 230 | lanes: u32, |
| 231 | segments: u32, |
| 232 | ) void { |
| 233 | var n: u32 = 0; |
| 234 | while (n < time) : (n += 1) { |
| 235 | var slice: u32 = 0; |
| 236 | while (slice < sync_points) : (slice += 1) { |
| 237 | var lane: u24 = 0; |
| 238 | while (lane < threads) : (lane += 1) { |
| 239 | processSegment(blocks, time, memory, threads, mode, lanes, segments, n, slice, lane); |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | fn processBlocksAsync( |
| 246 | blocks: *Blocks, |
| 247 | time: u32, |
| 248 | memory: u32, |
| 249 | threads: u24, |
| 250 | mode: Mode, |
| 251 | lanes: u32, |
| 252 | segments: u32, |
| 253 | io: Io, |
| 254 | ) Io.Cancelable!void { |
| 255 | var n: u32 = 0; |
| 256 | while (n < time) : (n += 1) { |
| 257 | var slice: u32 = 0; |
| 258 | while (slice < sync_points) : (slice += 1) { |
| 259 | var group: Io.Group = .init; |
| 260 | defer group.cancel(io); |
| 261 | var lane: u24 = 0; |
| 262 | while (lane < threads) : (lane += 1) { |
| 263 | group.async(io, processSegment, .{ |
| 264 | blocks, time, memory, threads, mode, lanes, segments, n, slice, lane, |
| 265 | }); |
| 266 | } |
| 267 | try group.await(io); |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | fn processSegment( |
| 273 | blocks: *Blocks, |
| 274 | passes: u32, |
| 275 | memory: u32, |
| 276 | threads: u24, |
| 277 | mode: Mode, |
| 278 | lanes: u32, |
| 279 | segments: u32, |
| 280 | n: u32, |
| 281 | slice: u32, |
| 282 | lane: u24, |
| 283 | ) void { |
| 284 | var addresses: [block_length]u64 align(16) = @splat(0); |
| 285 | var in: [block_length]u64 align(16) = @splat(0); |
| 286 | const zero: [block_length]u64 align(16) = @splat(0); |
| 287 | if (mode == .argon2i or (mode == .argon2id and n == 0 and slice < sync_points / 2)) { |
| 288 | in[0] = n; |
| 289 | in[1] = lane; |
| 290 | in[2] = slice; |
| 291 | in[3] = memory; |
| 292 | in[4] = passes; |
| 293 | in[5] = @backingInt(mode); |
| 294 | } |
| 295 | var index: u32 = 0; |
| 296 | if (n == 0 and slice == 0) { |
| 297 | index = 2; |
| 298 | if (mode == .argon2i or mode == .argon2id) { |
| 299 | in[6] += 1; |
| 300 | processBlock(&addresses, &in, &zero); |
| 301 | processBlock(&addresses, &addresses, &zero); |
| 302 | } |
| 303 | } |
| 304 | var offset = lane * lanes + slice * segments + index; |
| 305 | var random: u64 = 0; |
| 306 | while (index < segments) : ({ |
| 307 | index += 1; |
| 308 | offset += 1; |
| 309 | }) { |
| 310 | var prev = offset -% 1; |
| 311 | if (index == 0 and slice == 0) { |
| 312 | prev +%= lanes; |
| 313 | } |
| 314 | if (mode == .argon2i or (mode == .argon2id and n == 0 and slice < sync_points / 2)) { |
| 315 | if (index % block_length == 0) { |
| 316 | in[6] += 1; |
| 317 | processBlock(&addresses, &in, &zero); |
| 318 | processBlock(&addresses, &addresses, &zero); |
| 319 | } |
| 320 | random = addresses[index % block_length]; |
| 321 | } else { |
| 322 | random = blocks.items[prev][0]; |
| 323 | } |
| 324 | const new_offset = indexAlpha(random, lanes, segments, threads, n, slice, lane, index); |
| 325 | processBlockXor(&blocks.items[offset], &blocks.items[prev], &blocks.items[new_offset]); |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | fn processBlock( |
| 330 | out: *align(16) [block_length]u64, |
| 331 | in1: *align(16) const [block_length]u64, |
| 332 | in2: *align(16) const [block_length]u64, |
| 333 | ) void { |
| 334 | processBlockGeneric(out, in1, in2, false); |
| 335 | } |
| 336 | |
| 337 | fn processBlockXor( |
| 338 | out: *[block_length]u64, |
| 339 | in1: *const [block_length]u64, |
| 340 | in2: *const [block_length]u64, |
| 341 | ) void { |
| 342 | processBlockGeneric(out, in1, in2, true); |
| 343 | } |
| 344 | |
| 345 | fn processBlockGeneric( |
| 346 | out: *[block_length]u64, |
| 347 | in1: *const [block_length]u64, |
| 348 | in2: *const [block_length]u64, |
| 349 | comptime xor: bool, |
| 350 | ) void { |
| 351 | var t: [block_length]u64 = undefined; |
| 352 | for (&t, 0..) |*v, i| { |
| 353 | v.* = in1[i] ^ in2[i]; |
| 354 | } |
| 355 | var i: usize = 0; |
| 356 | while (i < block_length) : (i += 16) { |
| 357 | blamkaGeneric(t[i..][0..16]); |
| 358 | } |
| 359 | i = 0; |
| 360 | var buffer: [16]u64 = undefined; |
| 361 | while (i < block_length / 8) : (i += 2) { |
| 362 | var j: usize = 0; |
| 363 | while (j < block_length / 8) : (j += 2) { |
| 364 | buffer[j] = t[j * 8 + i]; |
| 365 | buffer[j + 1] = t[j * 8 + i + 1]; |
| 366 | } |
| 367 | blamkaGeneric(&buffer); |
| 368 | j = 0; |
| 369 | while (j < block_length / 8) : (j += 2) { |
| 370 | t[j * 8 + i] = buffer[j]; |
| 371 | t[j * 8 + i + 1] = buffer[j + 1]; |
| 372 | } |
| 373 | } |
| 374 | if (xor) { |
| 375 | for (t, 0..) |v, j| { |
| 376 | out[j] ^= in1[j] ^ in2[j] ^ v; |
| 377 | } |
| 378 | } else { |
| 379 | for (t, 0..) |v, j| { |
| 380 | out[j] = in1[j] ^ in2[j] ^ v; |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | const QuarterRound = struct { a: usize, b: usize, c: usize, d: usize }; |
| 386 | |
| 387 | fn Rp(a: usize, b: usize, c: usize, d: usize) QuarterRound { |
| 388 | return .{ .a = a, .b = b, .c = c, .d = d }; |
| 389 | } |
| 390 | |
| 391 | fn fBlaMka(x: u64, y: u64) u64 { |
| 392 | const xy = @as(u64, @as(u32, @truncate(x))) * @as(u64, @as(u32, @truncate(y))); |
| 393 | return x +% y +% 2 *% xy; |
| 394 | } |
| 395 | |
| 396 | fn blamkaGeneric(x: *[16]u64) void { |
| 397 | const rounds = comptime [_]QuarterRound{ |
| 398 | Rp(0, 4, 8, 12), |
| 399 | Rp(1, 5, 9, 13), |
| 400 | Rp(2, 6, 10, 14), |
| 401 | Rp(3, 7, 11, 15), |
| 402 | Rp(0, 5, 10, 15), |
| 403 | Rp(1, 6, 11, 12), |
| 404 | Rp(2, 7, 8, 13), |
| 405 | Rp(3, 4, 9, 14), |
| 406 | }; |
| 407 | inline for (rounds) |r| { |
| 408 | x[r.a] = fBlaMka(x[r.a], x[r.b]); |
| 409 | x[r.d] = math.rotr(u64, x[r.d] ^ x[r.a], 32); |
| 410 | x[r.c] = fBlaMka(x[r.c], x[r.d]); |
| 411 | x[r.b] = math.rotr(u64, x[r.b] ^ x[r.c], 24); |
| 412 | x[r.a] = fBlaMka(x[r.a], x[r.b]); |
| 413 | x[r.d] = math.rotr(u64, x[r.d] ^ x[r.a], 16); |
| 414 | x[r.c] = fBlaMka(x[r.c], x[r.d]); |
| 415 | x[r.b] = math.rotr(u64, x[r.b] ^ x[r.c], 63); |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | fn finalize( |
| 420 | blocks: *Blocks, |
| 421 | memory: u32, |
| 422 | threads: u24, |
| 423 | out: []u8, |
| 424 | ) void { |
| 425 | const lanes = memory / threads; |
| 426 | var lane: u24 = 0; |
| 427 | while (lane < threads - 1) : (lane += 1) { |
| 428 | for (blocks.items[(lane * lanes) + lanes - 1], 0..) |v, i| { |
| 429 | blocks.items[memory - 1][i] ^= v; |
| 430 | } |
| 431 | } |
| 432 | var block: [1024]u8 = undefined; |
| 433 | for (blocks.items[memory - 1], 0..) |v, i| { |
| 434 | mem.writeInt(u64, block[i * 8 ..][0..8], v, .little); |
| 435 | } |
| 436 | blake2bLong(out, &block); |
| 437 | } |
| 438 | |
| 439 | fn indexAlpha( |
| 440 | rand: u64, |
| 441 | lanes: u32, |
| 442 | segments: u32, |
| 443 | threads: u24, |
| 444 | n: u32, |
| 445 | slice: u32, |
| 446 | lane: u24, |
| 447 | index: u32, |
| 448 | ) u32 { |
| 449 | var ref_lane = @as(u32, @intCast(rand >> 32)) % threads; |
| 450 | if (n == 0 and slice == 0) { |
| 451 | ref_lane = lane; |
| 452 | } |
| 453 | var m = 3 * segments; |
| 454 | var s = ((slice + 1) % sync_points) * segments; |
| 455 | if (lane == ref_lane) { |
| 456 | m += index; |
| 457 | } |
| 458 | if (n == 0) { |
| 459 | m = slice * segments; |
| 460 | s = 0; |
| 461 | if (slice == 0 or lane == ref_lane) { |
| 462 | m += index; |
| 463 | } |
| 464 | } |
| 465 | if (index == 0 or lane == ref_lane) { |
| 466 | m -= 1; |
| 467 | } |
| 468 | var p = @as(u64, @as(u32, @truncate(rand))); |
| 469 | p = (p * p) >> 32; |
| 470 | p = (p * m) >> 32; |
| 471 | return ref_lane * lanes + @as(u32, @intCast(((s + m - (p + 1)) % lanes))); |
| 472 | } |
| 473 | |
| 474 | fn blockCount(params: Params) u32 { |
| 475 | return @max( |
| 476 | params.m / (sync_points * params.p) * (sync_points * params.p), |
| 477 | 2 * sync_points * params.p, |
| 478 | ); |
| 479 | } |
| 480 | |
| 481 | /// Compute the number of bytes of scratch memory `kdf` needs for `params`. |
| 482 | pub fn calcSize(params: Params) usize { |
| 483 | return blockCount(params) * @sizeOf([block_length]u64); |
| 484 | } |
| 485 | |
| 486 | /// Derives a key from the password, salt, and argon2 parameters. |
| 487 | /// |
| 488 | /// Derived key has to be at least 4 bytes length. |
| 489 | /// |
| 490 | /// Salt has to be at least 8 bytes length. |
| 491 | pub fn kdf( |
| 492 | allocator: mem.Allocator, |
| 493 | derived_key: []u8, |
| 494 | password: []const u8, |
| 495 | salt: []const u8, |
| 496 | params: Params, |
| 497 | mode: Mode, |
| 498 | io: Io, |
| 499 | ) KdfError!void { |
| 500 | if (derived_key.len < 4) return KdfError.WeakParameters; |
| 501 | if (derived_key.len > max_int) return KdfError.OutputTooLong; |
| 502 | |
| 503 | if (password.len > max_int) return KdfError.WeakParameters; |
| 504 | if (salt.len < 8 or salt.len > max_int) return KdfError.WeakParameters; |
| 505 | if (params.t < 1 or params.p < 1) return KdfError.WeakParameters; |
| 506 | if (params.m / 8 < params.p) return KdfError.WeakParameters; |
| 507 | |
| 508 | var h0 = initHash(password, salt, params, derived_key.len, mode); |
| 509 | const memory = blockCount(params); |
| 510 | |
| 511 | var blocks = try Blocks.initCapacity(allocator, memory); |
| 512 | defer blocks.deinit(); |
| 513 | |
| 514 | blocks.appendNTimesAssumeCapacity(@splat(0), memory); |
| 515 | |
| 516 | initBlocks(&blocks, &h0, memory, params.p); |
| 517 | try processBlocks(&blocks, params.t, memory, params.p, mode, io); |
| 518 | finalize(&blocks, memory, params.p, derived_key); |
| 519 | } |
| 520 | |
| 521 | const PhcFormatHasher = struct { |
| 522 | const BinValue = phc_format.BinValue; |
| 523 | |
| 524 | const HashResult = struct { |
| 525 | alg_id: []const u8, |
| 526 | alg_version: ?u32, |
| 527 | m: u32, |
| 528 | t: u32, |
| 529 | p: u24, |
| 530 | salt: BinValue(max_salt_len), |
| 531 | hash: BinValue(max_hash_len), |
| 532 | }; |
| 533 | |
| 534 | pub fn create( |
| 535 | allocator: mem.Allocator, |
| 536 | password: []const u8, |
| 537 | params: Params, |
| 538 | mode: Mode, |
| 539 | buf: []u8, |
| 540 | io: Io, |
| 541 | ) HasherError![]const u8 { |
| 542 | if (params.secret != null or params.ad != null) return HasherError.InvalidEncoding; |
| 543 | |
| 544 | var salt: [default_salt_len]u8 = undefined; |
| 545 | io.random(&salt); |
| 546 | |
| 547 | var hash: [default_hash_len]u8 = undefined; |
| 548 | try kdf(allocator, &hash, password, &salt, params, mode, io); |
| 549 | |
| 550 | return phc_format.serialize(HashResult{ |
| 551 | .alg_id = @tagName(mode), |
| 552 | .alg_version = version, |
| 553 | .m = params.m, |
| 554 | .t = params.t, |
| 555 | .p = params.p, |
| 556 | .salt = try BinValue(max_salt_len).fromSlice(&salt), |
| 557 | .hash = try BinValue(max_hash_len).fromSlice(&hash), |
| 558 | }, buf); |
| 559 | } |
| 560 | |
| 561 | pub fn verify( |
| 562 | allocator: mem.Allocator, |
| 563 | str: []const u8, |
| 564 | password: []const u8, |
| 565 | io: Io, |
| 566 | ) HasherError!void { |
| 567 | const hash_result = try phc_format.deserialize(HashResult, str); |
| 568 | |
| 569 | const mode = std.meta.stringToEnum(Mode, hash_result.alg_id) orelse |
| 570 | return HasherError.PasswordVerificationFailed; |
| 571 | if (hash_result.alg_version) |v| { |
| 572 | if (v != version) return HasherError.InvalidEncoding; |
| 573 | } |
| 574 | const params = Params{ .t = hash_result.t, .m = hash_result.m, .p = hash_result.p }; |
| 575 | |
| 576 | const expected_hash = hash_result.hash.constSlice(); |
| 577 | var hash_buf: [max_hash_len]u8 = undefined; |
| 578 | if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; |
| 579 | const hash = hash_buf[0..expected_hash.len]; |
| 580 | |
| 581 | try kdf(allocator, hash, password, hash_result.salt.constSlice(), params, mode, io); |
| 582 | if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; |
| 583 | } |
| 584 | }; |
| 585 | |
| 586 | /// Options for hashing a password. |
| 587 | /// |
| 588 | /// Allocator is required for argon2. |
| 589 | /// |
| 590 | /// Only phc encoding is supported. |
| 591 | pub const HashOptions = struct { |
| 592 | allocator: ?mem.Allocator, |
| 593 | params: Params, |
| 594 | mode: Mode = .argon2id, |
| 595 | encoding: pwhash.Encoding = .phc, |
| 596 | }; |
| 597 | |
| 598 | /// Compute a hash of a password using the argon2 key derivation function. |
| 599 | /// The function returns a string that includes all the parameters required for verification. |
| 600 | pub fn strHash( |
| 601 | password: []const u8, |
| 602 | options: HashOptions, |
| 603 | out: []u8, |
| 604 | io: Io, |
| 605 | ) Error![]const u8 { |
| 606 | const allocator = options.allocator orelse return Error.AllocatorRequired; |
| 607 | switch (options.encoding) { |
| 608 | .phc => return PhcFormatHasher.create( |
| 609 | allocator, |
| 610 | password, |
| 611 | options.params, |
| 612 | options.mode, |
| 613 | out, |
| 614 | io, |
| 615 | ), |
| 616 | .crypt => return Error.InvalidEncoding, |
| 617 | } |
| 618 | } |
| 619 | |
| 620 | /// Options for hash verification. |
| 621 | pub const VerifyOptions = struct { |
| 622 | allocator: mem.Allocator, |
| 623 | }; |
| 624 | |
| 625 | /// Verify that a previously computed hash is valid for a given password. |
| 626 | pub fn strVerify( |
| 627 | str: []const u8, |
| 628 | password: []const u8, |
| 629 | options: VerifyOptions, |
| 630 | io: Io, |
| 631 | ) Error!void { |
| 632 | return PhcFormatHasher.verify(options.allocator, str, password, io); |
| 633 | } |
| 634 | |
| 635 | test "argon2d" { |
| 636 | if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30074 |
| 637 | |
| 638 | const password: [32]u8 = @splat(0x01); |
| 639 | const salt: [16]u8 = @splat(0x02); |
| 640 | const secret: [8]u8 = @splat(0x03); |
| 641 | const ad: [12]u8 = @splat(0x04); |
| 642 | |
| 643 | var dk: [32]u8 = undefined; |
| 644 | try kdf( |
| 645 | std.testing.allocator, |
| 646 | &dk, |
| 647 | &password, |
| 648 | &salt, |
| 649 | .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad }, |
| 650 | .argon2d, |
| 651 | std.testing.io, |
| 652 | ); |
| 653 | |
| 654 | const want = [_]u8{ |
| 655 | 0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97, |
| 656 | 0x53, 0x71, 0xd3, 0x09, 0x19, 0x73, 0x42, 0x94, |
| 657 | 0xf8, 0x68, 0xe3, 0xbe, 0x39, 0x84, 0xf3, 0xc1, |
| 658 | 0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe, 0x4a, 0xcb, |
| 659 | }; |
| 660 | try std.testing.expectEqualSlices(u8, &dk, &want); |
| 661 | } |
| 662 | |
| 663 | test "argon2i" { |
| 664 | const password: [32]u8 = @splat(0x01); |
| 665 | const salt: [16]u8 = @splat(0x02); |
| 666 | const secret: [8]u8 = @splat(0x03); |
| 667 | const ad: [12]u8 = @splat(0x04); |
| 668 | |
| 669 | var dk: [32]u8 = undefined; |
| 670 | try kdf( |
| 671 | std.testing.allocator, |
| 672 | &dk, |
| 673 | &password, |
| 674 | &salt, |
| 675 | .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad }, |
| 676 | .argon2i, |
| 677 | std.testing.io, |
| 678 | ); |
| 679 | |
| 680 | const want = [_]u8{ |
| 681 | 0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa, |
| 682 | 0x13, 0xf0, 0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1, |
| 683 | 0xc8, 0xde, 0x6b, 0x01, 0x6d, 0xd3, 0x88, 0xd2, |
| 684 | 0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b, 0x6c, 0xe8, |
| 685 | }; |
| 686 | try std.testing.expectEqualSlices(u8, &dk, &want); |
| 687 | } |
| 688 | |
| 689 | test "argon2id" { |
| 690 | const password: [32]u8 = @splat(0x01); |
| 691 | const salt: [16]u8 = @splat(0x02); |
| 692 | const secret: [8]u8 = @splat(0x03); |
| 693 | const ad: [12]u8 = @splat(0x04); |
| 694 | |
| 695 | var dk: [32]u8 = undefined; |
| 696 | try kdf( |
| 697 | std.testing.allocator, |
| 698 | &dk, |
| 699 | &password, |
| 700 | &salt, |
| 701 | .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad }, |
| 702 | .argon2id, |
| 703 | std.testing.io, |
| 704 | ); |
| 705 | |
| 706 | const want = [_]u8{ |
| 707 | 0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, |
| 708 | 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9, |
| 709 | 0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e, |
| 710 | 0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01, 0xe6, 0x59, |
| 711 | }; |
| 712 | try std.testing.expectEqualSlices(u8, &dk, &want); |
| 713 | } |
| 714 | |
| 715 | test "kdf" { |
| 716 | if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31402 |
| 717 | |
| 718 | const password = "password"; |
| 719 | const salt = "somesalt"; |
| 720 | |
| 721 | const TestVector = struct { |
| 722 | mode: Mode, |
| 723 | time: u32, |
| 724 | memory: u32, |
| 725 | threads: u8, |
| 726 | hash: []const u8, |
| 727 | }; |
| 728 | const test_vectors = [_]TestVector{ |
| 729 | .{ |
| 730 | .mode = .argon2i, |
| 731 | .time = 1, |
| 732 | .memory = 64, |
| 733 | .threads = 1, |
| 734 | .hash = "b9c401d1844a67d50eae3967dc28870b22e508092e861a37", |
| 735 | }, |
| 736 | .{ |
| 737 | .mode = .argon2d, |
| 738 | .time = 1, |
| 739 | .memory = 64, |
| 740 | .threads = 1, |
| 741 | .hash = "8727405fd07c32c78d64f547f24150d3f2e703a89f981a19", |
| 742 | }, |
| 743 | .{ |
| 744 | .mode = .argon2id, |
| 745 | .time = 1, |
| 746 | .memory = 64, |
| 747 | .threads = 1, |
| 748 | .hash = "655ad15eac652dc59f7170a7332bf49b8469be1fdb9c28bb", |
| 749 | }, |
| 750 | .{ |
| 751 | .mode = .argon2i, |
| 752 | .time = 2, |
| 753 | .memory = 64, |
| 754 | .threads = 1, |
| 755 | .hash = "8cf3d8f76a6617afe35fac48eb0b7433a9a670ca4a07ed64", |
| 756 | }, |
| 757 | .{ |
| 758 | .mode = .argon2d, |
| 759 | .time = 2, |
| 760 | .memory = 64, |
| 761 | .threads = 1, |
| 762 | .hash = "3be9ec79a69b75d3752acb59a1fbb8b295a46529c48fbb75", |
| 763 | }, |
| 764 | .{ |
| 765 | .mode = .argon2id, |
| 766 | .time = 2, |
| 767 | .memory = 64, |
| 768 | .threads = 1, |
| 769 | .hash = "068d62b26455936aa6ebe60060b0a65870dbfa3ddf8d41f7", |
| 770 | }, |
| 771 | .{ |
| 772 | .mode = .argon2i, |
| 773 | .time = 2, |
| 774 | .memory = 64, |
| 775 | .threads = 2, |
| 776 | .hash = "2089f3e78a799720f80af806553128f29b132cafe40d059f", |
| 777 | }, |
| 778 | .{ |
| 779 | .mode = .argon2d, |
| 780 | .time = 2, |
| 781 | .memory = 64, |
| 782 | .threads = 2, |
| 783 | .hash = "68e2462c98b8bc6bb60ec68db418ae2c9ed24fc6748a40e9", |
| 784 | }, |
| 785 | .{ |
| 786 | .mode = .argon2id, |
| 787 | .time = 2, |
| 788 | .memory = 64, |
| 789 | .threads = 2, |
| 790 | .hash = "350ac37222f436ccb5c0972f1ebd3bf6b958bf2071841362", |
| 791 | }, |
| 792 | .{ |
| 793 | .mode = .argon2i, |
| 794 | .time = 3, |
| 795 | .memory = 256, |
| 796 | .threads = 2, |
| 797 | .hash = "f5bbf5d4c3836af13193053155b73ec7476a6a2eb93fd5e6", |
| 798 | }, |
| 799 | .{ |
| 800 | .mode = .argon2d, |
| 801 | .time = 3, |
| 802 | .memory = 256, |
| 803 | .threads = 2, |
| 804 | .hash = "f4f0669218eaf3641f39cc97efb915721102f4b128211ef2", |
| 805 | }, |
| 806 | .{ |
| 807 | .mode = .argon2id, |
| 808 | .time = 3, |
| 809 | .memory = 256, |
| 810 | .threads = 2, |
| 811 | .hash = "4668d30ac4187e6878eedeacf0fd83c5a0a30db2cc16ef0b", |
| 812 | }, |
| 813 | .{ |
| 814 | .mode = .argon2i, |
| 815 | .time = 4, |
| 816 | .memory = 256, |
| 817 | .threads = 4, |
| 818 | .hash = "f7dbbacbf16999e3700817a7e06f65a8db2e9fa9504ede4c", |
| 819 | }, |
| 820 | .{ |
| 821 | .mode = .argon2d, |
| 822 | .time = 4, |
| 823 | .memory = 256, |
| 824 | .threads = 4, |
| 825 | .hash = "ea2970501cf49faa5ba1d2e6370204e9b57ca90a8fea937b", |
| 826 | }, |
| 827 | .{ |
| 828 | .mode = .argon2id, |
| 829 | .time = 4, |
| 830 | .memory = 256, |
| 831 | .threads = 4, |
| 832 | .hash = "fbd40d5a8cb92f88c20bda4b3cdb1f9d5af1efa937032410", |
| 833 | }, |
| 834 | .{ |
| 835 | .mode = .argon2i, |
| 836 | .time = 4, |
| 837 | .memory = 256, |
| 838 | .threads = 8, |
| 839 | .hash = "15d3c398364e53f68fd12d19baf3f21432d964254fe27467", |
| 840 | }, |
| 841 | .{ |
| 842 | .mode = .argon2d, |
| 843 | .time = 4, |
| 844 | .memory = 256, |
| 845 | .threads = 8, |
| 846 | .hash = "23c9adc06f06e21e4612c1466a1be02627690932b02c0df0", |
| 847 | }, |
| 848 | .{ |
| 849 | .mode = .argon2id, |
| 850 | .time = 4, |
| 851 | .memory = 256, |
| 852 | .threads = 8, |
| 853 | .hash = "f22802f8ca47be93f9954e4ce20c1e944e938fbd4a125d9d", |
| 854 | }, |
| 855 | .{ |
| 856 | .mode = .argon2i, |
| 857 | .time = 2, |
| 858 | .memory = 64, |
| 859 | .threads = 3, |
| 860 | .hash = "5cab452fe6b8479c8661def8cd703b611a3905a6d5477fe6", |
| 861 | }, |
| 862 | .{ |
| 863 | .mode = .argon2d, |
| 864 | .time = 2, |
| 865 | .memory = 64, |
| 866 | .threads = 3, |
| 867 | .hash = "22474a423bda2ccd36ec9afd5119e5c8949798cadf659f51", |
| 868 | }, |
| 869 | .{ |
| 870 | .mode = .argon2id, |
| 871 | .time = 2, |
| 872 | .memory = 64, |
| 873 | .threads = 3, |
| 874 | .hash = "4a15b31aec7c2590b87d1f520be7d96f56658172deaa3079", |
| 875 | }, |
| 876 | .{ |
| 877 | .mode = .argon2i, |
| 878 | .time = 3, |
| 879 | .memory = 256, |
| 880 | .threads = 6, |
| 881 | .hash = "ebc8f91964abd8ceab49a12963b0a9e57d635bfa2aad2884", |
| 882 | }, |
| 883 | .{ |
| 884 | .mode = .argon2d, |
| 885 | .time = 3, |
| 886 | .memory = 256, |
| 887 | .threads = 6, |
| 888 | .hash = "1dd7202fd68da6675f769f4034b7a1db30d8785331954117", |
| 889 | }, |
| 890 | .{ |
| 891 | .mode = .argon2id, |
| 892 | .time = 3, |
| 893 | .memory = 256, |
| 894 | .threads = 6, |
| 895 | .hash = "424436b6ee22a66b04b9d0cf78f190305c5c166bae8baa09", |
| 896 | }, |
| 897 | }; |
| 898 | for (test_vectors) |v| { |
| 899 | var want: [24]u8 = undefined; |
| 900 | _ = try std.fmt.hexToBytes(&want, v.hash); |
| 901 | |
| 902 | var dk: [24]u8 = undefined; |
| 903 | try kdf( |
| 904 | std.testing.allocator, |
| 905 | &dk, |
| 906 | password, |
| 907 | salt, |
| 908 | .{ .t = v.time, .m = v.memory, .p = v.threads }, |
| 909 | v.mode, |
| 910 | std.testing.io, |
| 911 | ); |
| 912 | |
| 913 | try std.testing.expectEqualSlices(u8, &dk, &want); |
| 914 | } |
| 915 | } |
| 916 | |
| 917 | test "phc format hasher" { |
| 918 | const allocator = std.testing.allocator; |
| 919 | const password = "testpass"; |
| 920 | const io = std.testing.io; |
| 921 | |
| 922 | var buf: [128]u8 = undefined; |
| 923 | const hash = try PhcFormatHasher.create( |
| 924 | allocator, |
| 925 | password, |
| 926 | .{ .t = 3, .m = 32, .p = 4 }, |
| 927 | .argon2id, |
| 928 | &buf, |
| 929 | io, |
| 930 | ); |
| 931 | try PhcFormatHasher.verify(allocator, hash, password, io); |
| 932 | } |
| 933 | |
| 934 | test "password hash and password verify" { |
| 935 | const allocator = std.testing.allocator; |
| 936 | const password = "testpass"; |
| 937 | const io = std.testing.io; |
| 938 | |
| 939 | var buf: [128]u8 = undefined; |
| 940 | const hash = try strHash( |
| 941 | password, |
| 942 | .{ .allocator = allocator, .params = .{ .t = 3, .m = 32, .p = 4 } }, |
| 943 | &buf, |
| 944 | io, |
| 945 | ); |
| 946 | try strVerify(hash, password, .{ .allocator = allocator }, io); |
| 947 | } |
| 948 | |
| 949 | test "kdf derived key length" { |
| 950 | if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31504 |
| 951 | |
| 952 | const allocator = std.testing.allocator; |
| 953 | const io = std.testing.io; |
| 954 | |
| 955 | const password = "testpass"; |
| 956 | const salt = "saltsalt"; |
| 957 | const params = Params{ .t = 3, .m = 32, .p = 4 }; |
| 958 | const mode = Mode.argon2id; |
| 959 | |
| 960 | var dk1: [11]u8 = undefined; |
| 961 | try kdf(allocator, &dk1, password, salt, params, mode, io); |
| 962 | |
| 963 | var dk2: [77]u8 = undefined; |
| 964 | try kdf(allocator, &dk2, password, salt, params, mode, io); |
| 965 | |
| 966 | var dk3: [111]u8 = undefined; |
| 967 | try kdf(allocator, &dk3, password, salt, params, mode, io); |
| 968 | } |