| 1 | // https://tools.ietf.org/html/rfc7914 |
| 2 | // https://github.com/golang/crypto/blob/master/scrypt/scrypt.go |
| 3 | // https://github.com/Tarsnap/scrypt |
| 4 | |
| 5 | const std = @import("std"); |
| 6 | const crypto = std.crypto; |
| 7 | const fmt = std.fmt; |
| 8 | const math = std.math; |
| 9 | const mem = std.mem; |
| 10 | const meta = std.meta; |
| 11 | const pwhash = crypto.pwhash; |
| 12 | |
| 13 | const phc_format = @import("phc_encoding.zig"); |
| 14 | |
| 15 | const HmacSha256 = crypto.auth.hmac.sha2.HmacSha256; |
| 16 | const KdfError = pwhash.KdfError; |
| 17 | const HasherError = pwhash.HasherError; |
| 18 | const EncodingError = phc_format.Error; |
| 19 | const Error = pwhash.Error; |
| 20 | |
| 21 | const max_size = math.maxInt(usize); |
| 22 | const max_int = max_size >> 1; |
| 23 | pub const default_salt_len = 32; |
| 24 | const default_hash_len = 32; |
| 25 | const max_salt_len = 64; |
| 26 | const max_hash_len = 64; |
| 27 | |
| 28 | fn blockCopy(dst: []align(16) u32, src: []align(16) const u32, n: usize) void { |
| 29 | @memcpy(dst[0 .. n * 16], src[0 .. n * 16]); |
| 30 | } |
| 31 | |
| 32 | fn blockXor(dst: []align(16) u32, src: []align(16) const u32, n: usize) void { |
| 33 | for (src[0 .. n * 16], 0..) |v, i| { |
| 34 | dst[i] ^= v; |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | const QuarterRound = struct { a: usize, b: usize, c: usize, d: u6 }; |
| 39 | |
| 40 | fn Rp(a: usize, b: usize, c: usize, d: u6) QuarterRound { |
| 41 | return QuarterRound{ .a = a, .b = b, .c = c, .d = d }; |
| 42 | } |
| 43 | |
| 44 | fn salsa8core(b: *align(16) [16]u32) void { |
| 45 | const arx_steps = comptime [_]QuarterRound{ |
| 46 | Rp(4, 0, 12, 7), Rp(8, 4, 0, 9), Rp(12, 8, 4, 13), Rp(0, 12, 8, 18), |
| 47 | Rp(9, 5, 1, 7), Rp(13, 9, 5, 9), Rp(1, 13, 9, 13), Rp(5, 1, 13, 18), |
| 48 | Rp(14, 10, 6, 7), Rp(2, 14, 10, 9), Rp(6, 2, 14, 13), Rp(10, 6, 2, 18), |
| 49 | Rp(3, 15, 11, 7), Rp(7, 3, 15, 9), Rp(11, 7, 3, 13), Rp(15, 11, 7, 18), |
| 50 | Rp(1, 0, 3, 7), Rp(2, 1, 0, 9), Rp(3, 2, 1, 13), Rp(0, 3, 2, 18), |
| 51 | Rp(6, 5, 4, 7), Rp(7, 6, 5, 9), Rp(4, 7, 6, 13), Rp(5, 4, 7, 18), |
| 52 | Rp(11, 10, 9, 7), Rp(8, 11, 10, 9), Rp(9, 8, 11, 13), Rp(10, 9, 8, 18), |
| 53 | Rp(12, 15, 14, 7), Rp(13, 12, 15, 9), Rp(14, 13, 12, 13), Rp(15, 14, 13, 18), |
| 54 | }; |
| 55 | var x = b.*; |
| 56 | var j: usize = 0; |
| 57 | while (j < 8) : (j += 2) { |
| 58 | inline for (arx_steps) |r| { |
| 59 | x[r.a] ^= math.rotl(u32, x[r.b] +% x[r.c], r.d); |
| 60 | } |
| 61 | } |
| 62 | j = 0; |
| 63 | while (j < 16) : (j += 1) { |
| 64 | b[j] +%= x[j]; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | fn salsaXor(tmp: *align(16) [16]u32, in: []align(16) const u32, out: []align(16) u32) void { |
| 69 | blockXor(tmp, in, 1); |
| 70 | salsa8core(tmp); |
| 71 | blockCopy(out, tmp, 1); |
| 72 | } |
| 73 | |
| 74 | fn blockMix(tmp: *align(16) [16]u32, in: []align(16) const u32, out: []align(16) u32, r: u30) void { |
| 75 | blockCopy(tmp, @alignCast(in[(2 * r - 1) * 16 ..]), 1); |
| 76 | var i: usize = 0; |
| 77 | while (i < 2 * r) : (i += 2) { |
| 78 | salsaXor(tmp, @alignCast(in[i * 16 ..]), @alignCast(out[i * 8 ..])); |
| 79 | salsaXor(tmp, @alignCast(in[i * 16 + 16 ..]), @alignCast(out[i * 8 + r * 16 ..])); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | fn integerify(b: []align(16) const u32, r: u30) u64 { |
| 84 | const j = (2 * r - 1) * 16; |
| 85 | return @as(u64, b[j]) | @as(u64, b[j + 1]) << 32; |
| 86 | } |
| 87 | |
| 88 | fn smix(b: []align(16) u8, r: u30, n: usize, v: []align(16) u32, xy: []align(16) u32) void { |
| 89 | const x: []align(16) u32 = @alignCast(xy[0 .. 32 * r]); |
| 90 | const y: []align(16) u32 = @alignCast(xy[32 * r ..]); |
| 91 | |
| 92 | for (x, 0..) |*v1, j| { |
| 93 | v1.* = mem.readInt(u32, b[4 * j ..][0..4], .little); |
| 94 | } |
| 95 | |
| 96 | var tmp: [16]u32 align(16) = undefined; |
| 97 | var i: usize = 0; |
| 98 | while (i < n) : (i += 2) { |
| 99 | blockCopy(@alignCast(v[i * (32 * r) ..]), x, 2 * r); |
| 100 | blockMix(&tmp, x, y, r); |
| 101 | |
| 102 | blockCopy(@alignCast(v[(i + 1) * (32 * r) ..]), y, 2 * r); |
| 103 | blockMix(&tmp, y, x, r); |
| 104 | } |
| 105 | |
| 106 | i = 0; |
| 107 | while (i < n) : (i += 2) { |
| 108 | var j = @as(usize, @intCast(integerify(x, r) & (n - 1))); |
| 109 | blockXor(x, @alignCast(v[j * (32 * r) ..]), 2 * r); |
| 110 | blockMix(&tmp, x, y, r); |
| 111 | |
| 112 | j = @as(usize, @intCast(integerify(y, r) & (n - 1))); |
| 113 | blockXor(y, @alignCast(v[j * (32 * r) ..]), 2 * r); |
| 114 | blockMix(&tmp, y, x, r); |
| 115 | } |
| 116 | |
| 117 | for (x, 0..) |v1, j| { |
| 118 | mem.writeInt(u32, b[4 * j ..][0..4], v1, .little); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | /// Scrypt parameters |
| 123 | pub const Params = struct { |
| 124 | const Self = @This(); |
| 125 | |
| 126 | /// The CPU/Memory cost parameter [ln] is log2(N). |
| 127 | ln: u6, |
| 128 | |
| 129 | /// The [r]esource usage parameter specifies the block size. |
| 130 | r: u30, |
| 131 | |
| 132 | /// The [p]arallelization parameter. |
| 133 | /// A large value of [p] can be used to increase the computational cost of scrypt without |
| 134 | /// increasing the memory usage. |
| 135 | p: u30, |
| 136 | |
| 137 | /// Baseline parameters for interactive logins |
| 138 | pub const interactive = Self.fromLimits(524288, 16777216); |
| 139 | |
| 140 | /// Baseline parameters for offline usage |
| 141 | pub const sensitive = Self.fromLimits(33554432, 1073741824); |
| 142 | |
| 143 | /// Recommended parameters according to the |
| 144 | /// [OWASP cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). |
| 145 | pub const owasp = Self{ .ln = 17, .r = 8, .p = 1 }; |
| 146 | |
| 147 | /// Create parameters from ops and mem limits, where mem_limit given in bytes |
| 148 | pub fn fromLimits(ops_limit: u64, mem_limit: usize) Self { |
| 149 | const ops = @max(32768, ops_limit); |
| 150 | const r: u30 = 8; |
| 151 | if (ops < mem_limit / 32) { |
| 152 | const max_n = ops / (r * 4); |
| 153 | return Self{ .r = r, .p = 1, .ln = @as(u6, @intCast(math.log2(max_n))) }; |
| 154 | } else { |
| 155 | const max_n = mem_limit / (@as(usize, @intCast(r)) * 128); |
| 156 | const ln = @as(u6, @intCast(math.log2(max_n))); |
| 157 | const max_rp = @min(0x3fffffff, (ops / 4) / (@as(u64, 1) << ln)); |
| 158 | return Self{ .r = r, .p = @as(u30, @intCast(max_rp / @as(u64, r))), .ln = ln }; |
| 159 | } |
| 160 | } |
| 161 | }; |
| 162 | |
| 163 | /// Apply scrypt to generate a key from a password. |
| 164 | /// |
| 165 | /// scrypt is defined in RFC 7914. |
| 166 | /// |
| 167 | /// allocator: mem.Allocator. |
| 168 | /// |
| 169 | /// derived_key: Slice of appropriate size for generated key. Generally 16 or 32 bytes in length. |
| 170 | /// May be uninitialized. All bytes will be overwritten. |
| 171 | /// Maximum size is `derived_key.len / 32 == 0xffff_ffff`. |
| 172 | /// |
| 173 | /// password: Arbitrary sequence of bytes of any length. |
| 174 | /// |
| 175 | /// salt: Arbitrary sequence of bytes of any length. |
| 176 | /// |
| 177 | /// params: Params. |
| 178 | pub fn kdf( |
| 179 | allocator: mem.Allocator, |
| 180 | derived_key: []u8, |
| 181 | password: []const u8, |
| 182 | salt: []const u8, |
| 183 | params: Params, |
| 184 | ) KdfError!void { |
| 185 | if (derived_key.len == 0) return KdfError.WeakParameters; |
| 186 | if (derived_key.len / 32 > 0xffff_ffff) return KdfError.OutputTooLong; |
| 187 | if (params.ln == 0 or params.r == 0 or params.p == 0) return KdfError.WeakParameters; |
| 188 | |
| 189 | const n64 = @as(u64, 1) << params.ln; |
| 190 | if (n64 > max_size) return KdfError.WeakParameters; |
| 191 | const n = @as(usize, @intCast(n64)); |
| 192 | if (@as(u64, params.r) * @as(u64, params.p) >= 1 << 30 or |
| 193 | params.r > max_int / 128 / @as(u64, params.p) or |
| 194 | params.r > max_int / 256 or |
| 195 | n > max_int / 128 / @as(u64, params.r)) return KdfError.WeakParameters; |
| 196 | |
| 197 | const xy = try allocator.alignedAlloc(u32, .@"16", 64 * params.r); |
| 198 | defer allocator.free(xy); |
| 199 | const v = try allocator.alignedAlloc(u32, .@"16", 32 * n * params.r); |
| 200 | defer allocator.free(v); |
| 201 | var dk = try allocator.alignedAlloc(u8, .@"16", params.p * 128 * params.r); |
| 202 | defer allocator.free(dk); |
| 203 | |
| 204 | try pwhash.pbkdf2(dk, password, salt, 1, HmacSha256); |
| 205 | var i: u32 = 0; |
| 206 | while (i < params.p) : (i += 1) { |
| 207 | smix(@alignCast(dk[i * 128 * params.r ..]), params.r, n, v, xy); |
| 208 | } |
| 209 | try pwhash.pbkdf2(derived_key, password, dk, 1, HmacSha256); |
| 210 | } |
| 211 | |
| 212 | const crypt_format = struct { |
| 213 | /// String prefix for scrypt |
| 214 | pub const prefix = "$7$"; |
| 215 | |
| 216 | /// Standard type for a set of scrypt parameters, with the salt and hash. |
| 217 | pub fn HashResult(comptime crypt_max_hash_len: usize) type { |
| 218 | return struct { |
| 219 | ln: u6, |
| 220 | r: u30, |
| 221 | p: u30, |
| 222 | salt: []const u8, |
| 223 | hash: BinValue(crypt_max_hash_len), |
| 224 | }; |
| 225 | } |
| 226 | |
| 227 | const Codec = CustomB64Codec("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".*); |
| 228 | |
| 229 | /// A wrapped binary value whose maximum size is `max_len`. |
| 230 | /// |
| 231 | /// This type must be used whenever a binary value is encoded in a PHC-formatted string. |
| 232 | /// This includes `salt`, `hash`, and any other binary parameters such as keys. |
| 233 | /// |
| 234 | /// Once initialized, the actual value can be read with the `constSlice()` function. |
| 235 | pub fn BinValue(comptime max_len: usize) type { |
| 236 | return struct { |
| 237 | const Self = @This(); |
| 238 | const capacity = max_len; |
| 239 | const max_encoded_length = Codec.encodedLen(max_len); |
| 240 | |
| 241 | buf: [max_len]u8 = undefined, |
| 242 | len: usize = 0, |
| 243 | |
| 244 | /// Wrap an existing byte slice |
| 245 | pub fn fromSlice(slice: []const u8) EncodingError!Self { |
| 246 | if (slice.len > capacity) return EncodingError.NoSpaceLeft; |
| 247 | var bin_value: Self = undefined; |
| 248 | @memcpy(bin_value.buf[0..slice.len], slice); |
| 249 | bin_value.len = slice.len; |
| 250 | return bin_value; |
| 251 | } |
| 252 | |
| 253 | /// Return the slice containing the actual value. |
| 254 | pub fn constSlice(self: *const Self) []const u8 { |
| 255 | return self.buf[0..self.len]; |
| 256 | } |
| 257 | |
| 258 | fn fromB64(self: *Self, str: []const u8) !void { |
| 259 | const len = Codec.decodedLen(str.len); |
| 260 | if (len > self.buf.len) return EncodingError.NoSpaceLeft; |
| 261 | try Codec.decode(self.buf[0..len], str); |
| 262 | self.len = len; |
| 263 | } |
| 264 | |
| 265 | fn toB64(self: *const Self, buf: []u8) ![]const u8 { |
| 266 | const value = self.constSlice(); |
| 267 | const len = Codec.encodedLen(value.len); |
| 268 | if (len > buf.len) return EncodingError.NoSpaceLeft; |
| 269 | const encoded = buf[0..len]; |
| 270 | Codec.encode(encoded, value); |
| 271 | return encoded; |
| 272 | } |
| 273 | }; |
| 274 | } |
| 275 | |
| 276 | /// Expand binary data into a salt for the modular crypt format. |
| 277 | pub fn saltFromBin(comptime len: usize, salt: [len]u8) [Codec.encodedLen(len)]u8 { |
| 278 | var buf: [Codec.encodedLen(len)]u8 = undefined; |
| 279 | Codec.encode(&buf, &salt); |
| 280 | return buf; |
| 281 | } |
| 282 | |
| 283 | /// Deserialize a string into a structure `T` (matching `HashResult`). |
| 284 | pub fn deserialize(comptime T: type, str: []const u8) EncodingError!T { |
| 285 | var out: T = undefined; |
| 286 | |
| 287 | if (str.len < 16) return EncodingError.InvalidEncoding; |
| 288 | if (!mem.eql(u8, prefix, str[0..3])) return EncodingError.InvalidEncoding; |
| 289 | out.ln = try Codec.intDecode(u6, str[3..4]); |
| 290 | out.r = try Codec.intDecode(u30, str[4..9]); |
| 291 | out.p = try Codec.intDecode(u30, str[9..14]); |
| 292 | |
| 293 | var it = mem.splitScalar(u8, str[14..], '$'); |
| 294 | |
| 295 | const salt = it.first(); |
| 296 | if (@hasField(T, "salt")) out.salt = salt; |
| 297 | |
| 298 | const hash_str = it.next() orelse return EncodingError.InvalidEncoding; |
| 299 | if (@hasField(T, "hash")) try out.hash.fromB64(hash_str); |
| 300 | |
| 301 | return out; |
| 302 | } |
| 303 | |
| 304 | /// Serialize parameters into a string in modular crypt format. |
| 305 | pub fn serialize(params: anytype, str: []u8) EncodingError![]const u8 { |
| 306 | var w: std.Io.Writer = .fixed(str); |
| 307 | serializeTo(params, &w) catch |err| switch (err) { |
| 308 | error.WriteFailed => return error.NoSpaceLeft, |
| 309 | else => |e| return e, |
| 310 | }; |
| 311 | return w.buffered(); |
| 312 | } |
| 313 | |
| 314 | /// Compute the number of bytes required to serialize `params` |
| 315 | pub fn calcSize(params: anytype) usize { |
| 316 | var trash: [128]u8 = undefined; |
| 317 | var d: std.Io.Writer.Discarding = .init(&trash); |
| 318 | serializeTo(params, &d.writer) catch unreachable; |
| 319 | return @intCast(d.fullCount()); |
| 320 | } |
| 321 | |
| 322 | fn serializeTo(params: anytype, w: *std.Io.Writer) !void { |
| 323 | var header: [14]u8 = undefined; |
| 324 | header[0..3].* = prefix.*; |
| 325 | Codec.intEncode(header[3..4], params.ln); |
| 326 | Codec.intEncode(header[4..9], params.r); |
| 327 | Codec.intEncode(header[9..14], params.p); |
| 328 | try w.writeAll(&header); |
| 329 | try w.writeAll(params.salt); |
| 330 | try w.writeAll("$"); |
| 331 | var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined; |
| 332 | const hash_str = try params.hash.toB64(&buf); |
| 333 | try w.writeAll(hash_str); |
| 334 | } |
| 335 | |
| 336 | /// Custom codec that maps 6 bits into 8 like regular Base64, but uses its own alphabet, |
| 337 | /// encodes bits in little-endian, and can also encode integers. |
| 338 | fn CustomB64Codec(comptime map: [64]u8) type { |
| 339 | return struct { |
| 340 | const map64 = map; |
| 341 | |
| 342 | fn encodedLen(len: usize) usize { |
| 343 | return (len * 4 + 2) / 3; |
| 344 | } |
| 345 | |
| 346 | fn decodedLen(len: usize) usize { |
| 347 | return len / 4 * 3 + (len % 4) * 3 / 4; |
| 348 | } |
| 349 | |
| 350 | fn intEncode(dst: []u8, src: anytype) void { |
| 351 | var n = src; |
| 352 | for (dst) |*x| { |
| 353 | x.* = map64[@as(u6, @truncate(n))]; |
| 354 | n = math.shr(@TypeOf(src), n, 6); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | fn intDecode(comptime T: type, src: *const [(@bitSizeOf(T) + 5) / 6]u8) !T { |
| 359 | var v: T = 0; |
| 360 | for (src, 0..) |x, i| { |
| 361 | const vi = mem.findScalar(u8, &map64, x) orelse return EncodingError.InvalidEncoding; |
| 362 | v |= @as(T, @intCast(vi)) << @as(math.Log2Int(T), @intCast(i * 6)); |
| 363 | } |
| 364 | return v; |
| 365 | } |
| 366 | |
| 367 | fn decode(dst: []u8, src: []const u8) !void { |
| 368 | std.debug.assert(dst.len == decodedLen(src.len)); |
| 369 | var i: usize = 0; |
| 370 | while (i < src.len / 4) : (i += 1) { |
| 371 | mem.writeInt(u24, dst[i * 3 ..][0..3], try intDecode(u24, src[i * 4 ..][0..4]), .little); |
| 372 | } |
| 373 | const leftover = src[i * 4 ..]; |
| 374 | var v: u24 = 0; |
| 375 | for (leftover, 0..) |_, j| { |
| 376 | v |= @as(u24, try intDecode(u6, leftover[j..][0..1])) << @as(u5, @intCast(j * 6)); |
| 377 | } |
| 378 | for (dst[i * 3 ..], 0..) |*x, j| { |
| 379 | x.* = @as(u8, @truncate(v >> @as(u5, @intCast(j * 8)))); |
| 380 | } |
| 381 | } |
| 382 | |
| 383 | fn encode(dst: []u8, src: []const u8) void { |
| 384 | std.debug.assert(dst.len == encodedLen(src.len)); |
| 385 | var i: usize = 0; |
| 386 | while (i < src.len / 3) : (i += 1) { |
| 387 | intEncode(dst[i * 4 ..][0..4], mem.readInt(u24, src[i * 3 ..][0..3], .little)); |
| 388 | } |
| 389 | const leftover = src[i * 3 ..]; |
| 390 | var v: u24 = 0; |
| 391 | for (leftover, 0..) |x, j| { |
| 392 | v |= @as(u24, x) << @as(u5, @intCast(j * 8)); |
| 393 | } |
| 394 | intEncode(dst[i * 4 ..], v); |
| 395 | } |
| 396 | }; |
| 397 | } |
| 398 | }; |
| 399 | |
| 400 | /// Hash and verify passwords using the PHC format. |
| 401 | const PhcFormatHasher = struct { |
| 402 | const alg_id = "scrypt"; |
| 403 | const BinValue = phc_format.BinValue; |
| 404 | |
| 405 | const HashResult = struct { |
| 406 | alg_id: []const u8, |
| 407 | ln: u6, |
| 408 | r: u30, |
| 409 | p: u30, |
| 410 | salt: BinValue(max_salt_len), |
| 411 | hash: BinValue(max_hash_len), |
| 412 | }; |
| 413 | |
| 414 | /// Return a non-deterministic hash of the password encoded as a PHC-format string |
| 415 | pub fn create( |
| 416 | allocator: mem.Allocator, |
| 417 | password: []const u8, |
| 418 | params: Params, |
| 419 | buf: []u8, |
| 420 | io: std.Io, |
| 421 | ) HasherError![]const u8 { |
| 422 | var salt: [default_salt_len]u8 = undefined; |
| 423 | io.random(&salt); |
| 424 | return createWithSalt(allocator, password, params, buf, &salt); |
| 425 | } |
| 426 | |
| 427 | /// Return a deterministic hash of the password encoded as a PHC-format string. |
| 428 | /// Uses the provided salt instead of generating one randomly. |
| 429 | pub fn createWithSalt( |
| 430 | allocator: mem.Allocator, |
| 431 | password: []const u8, |
| 432 | params: Params, |
| 433 | buf: []u8, |
| 434 | salt: *const [default_salt_len]u8, |
| 435 | ) HasherError![]const u8 { |
| 436 | var hash: [default_hash_len]u8 = undefined; |
| 437 | try kdf(allocator, &hash, password, salt, params); |
| 438 | |
| 439 | return phc_format.serialize(HashResult{ |
| 440 | .alg_id = alg_id, |
| 441 | .ln = params.ln, |
| 442 | .r = params.r, |
| 443 | .p = params.p, |
| 444 | .salt = try BinValue(max_salt_len).fromSlice(salt), |
| 445 | .hash = try BinValue(max_hash_len).fromSlice(&hash), |
| 446 | }, buf); |
| 447 | } |
| 448 | |
| 449 | /// Verify a password against a PHC-format encoded string |
| 450 | pub fn verify( |
| 451 | allocator: mem.Allocator, |
| 452 | str: []const u8, |
| 453 | password: []const u8, |
| 454 | ) HasherError!void { |
| 455 | const hash_result = try phc_format.deserialize(HashResult, str); |
| 456 | if (!mem.eql(u8, hash_result.alg_id, alg_id)) return HasherError.PasswordVerificationFailed; |
| 457 | const params = Params{ .ln = hash_result.ln, .r = hash_result.r, .p = hash_result.p }; |
| 458 | const expected_hash = hash_result.hash.constSlice(); |
| 459 | var hash_buf: [max_hash_len]u8 = undefined; |
| 460 | if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; |
| 461 | const hash = hash_buf[0..expected_hash.len]; |
| 462 | try kdf(allocator, hash, password, hash_result.salt.constSlice(), params); |
| 463 | if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; |
| 464 | } |
| 465 | }; |
| 466 | |
| 467 | /// Hash and verify passwords using the modular crypt format. |
| 468 | const CryptFormatHasher = struct { |
| 469 | const BinValue = crypt_format.BinValue; |
| 470 | const HashResult = crypt_format.HashResult(max_hash_len); |
| 471 | |
| 472 | /// Length of a string returned by the create() function |
| 473 | pub const pwhash_str_length: usize = 101; |
| 474 | |
| 475 | /// Return a non-deterministic hash of the password encoded into the modular crypt format |
| 476 | pub fn create( |
| 477 | allocator: mem.Allocator, |
| 478 | password: []const u8, |
| 479 | params: Params, |
| 480 | buf: []u8, |
| 481 | io: std.Io, |
| 482 | ) HasherError![]const u8 { |
| 483 | var salt_bin: [default_salt_len]u8 = undefined; |
| 484 | io.random(&salt_bin); |
| 485 | return createWithSalt(allocator, password, params, buf, &salt_bin); |
| 486 | } |
| 487 | |
| 488 | /// Return a deterministic hash of the password encoded into the modular crypt format. |
| 489 | /// Uses the provided salt instead of generating one randomly. |
| 490 | pub fn createWithSalt( |
| 491 | allocator: mem.Allocator, |
| 492 | password: []const u8, |
| 493 | params: Params, |
| 494 | buf: []u8, |
| 495 | salt_bin: *const [default_salt_len]u8, |
| 496 | ) HasherError![]const u8 { |
| 497 | const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin.*); |
| 498 | |
| 499 | var hash: [default_hash_len]u8 = undefined; |
| 500 | try kdf(allocator, &hash, password, &salt, params); |
| 501 | |
| 502 | return crypt_format.serialize(HashResult{ |
| 503 | .ln = params.ln, |
| 504 | .r = params.r, |
| 505 | .p = params.p, |
| 506 | .salt = &salt, |
| 507 | .hash = try BinValue(max_hash_len).fromSlice(&hash), |
| 508 | }, buf); |
| 509 | } |
| 510 | |
| 511 | /// Verify a password against a string in modular crypt format |
| 512 | pub fn verify( |
| 513 | allocator: mem.Allocator, |
| 514 | str: []const u8, |
| 515 | password: []const u8, |
| 516 | ) HasherError!void { |
| 517 | const hash_result = try crypt_format.deserialize(HashResult, str); |
| 518 | const params = Params{ .ln = hash_result.ln, .r = hash_result.r, .p = hash_result.p }; |
| 519 | const expected_hash = hash_result.hash.constSlice(); |
| 520 | var hash_buf: [max_hash_len]u8 = undefined; |
| 521 | if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; |
| 522 | const hash = hash_buf[0..expected_hash.len]; |
| 523 | try kdf(allocator, hash, password, hash_result.salt, params); |
| 524 | if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; |
| 525 | } |
| 526 | }; |
| 527 | |
| 528 | /// Options for hashing a password. |
| 529 | /// |
| 530 | /// Allocator is required for scrypt. |
| 531 | pub const HashOptions = struct { |
| 532 | allocator: ?mem.Allocator, |
| 533 | params: Params, |
| 534 | encoding: pwhash.Encoding, |
| 535 | }; |
| 536 | |
| 537 | /// Compute a hash of a password using the scrypt key derivation function. |
| 538 | /// The function returns a string that includes all the parameters required for verification. |
| 539 | pub fn strHash( |
| 540 | password: []const u8, |
| 541 | options: HashOptions, |
| 542 | out: []u8, |
| 543 | io: std.Io, |
| 544 | ) Error![]const u8 { |
| 545 | const allocator = options.allocator orelse return Error.AllocatorRequired; |
| 546 | switch (options.encoding) { |
| 547 | .phc => return PhcFormatHasher.create(allocator, password, options.params, out, io), |
| 548 | .crypt => return CryptFormatHasher.create(allocator, password, options.params, out, io), |
| 549 | } |
| 550 | } |
| 551 | |
| 552 | /// Compute a deterministic hash of a password using the scrypt key derivation function. |
| 553 | /// The function returns a string that includes all the parameters required for verification. |
| 554 | /// Uses the provided salt instead of generating one randomly. |
| 555 | pub fn strHashWithSalt( |
| 556 | password: []const u8, |
| 557 | options: HashOptions, |
| 558 | out: []u8, |
| 559 | salt: *const [default_salt_len]u8, |
| 560 | ) Error![]const u8 { |
| 561 | const allocator = options.allocator orelse return Error.AllocatorRequired; |
| 562 | switch (options.encoding) { |
| 563 | .phc => return PhcFormatHasher.createWithSalt(allocator, password, options.params, out, salt), |
| 564 | .crypt => return CryptFormatHasher.createWithSalt(allocator, password, options.params, out, salt), |
| 565 | } |
| 566 | } |
| 567 | |
| 568 | /// Options for hash verification. |
| 569 | pub const VerifyOptions = struct { |
| 570 | allocator: mem.Allocator, |
| 571 | }; |
| 572 | |
| 573 | /// Verify that a previously computed hash is valid for a given password. |
| 574 | pub fn strVerify( |
| 575 | str: []const u8, |
| 576 | password: []const u8, |
| 577 | options: VerifyOptions, |
| 578 | ) Error!void { |
| 579 | if (mem.startsWith(u8, str, crypt_format.prefix)) { |
| 580 | return CryptFormatHasher.verify(options.allocator, str, password); |
| 581 | } else { |
| 582 | return PhcFormatHasher.verify(options.allocator, str, password); |
| 583 | } |
| 584 | } |
| 585 | |
| 586 | // These tests take way too long to run, so I have disabled them. |
| 587 | const run_long_tests = false; |
| 588 | |
| 589 | test "kdf" { |
| 590 | if (!run_long_tests) return error.SkipZigTest; |
| 591 | |
| 592 | const password = "testpass"; |
| 593 | const salt = "saltsalt"; |
| 594 | |
| 595 | var dk: [32]u8 = undefined; |
| 596 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 15, .r = 8, .p = 1 }); |
| 597 | |
| 598 | const hex = "1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886de"; |
| 599 | var bytes: [hex.len / 2]u8 = undefined; |
| 600 | _ = try fmt.hexToBytes(&bytes, hex); |
| 601 | |
| 602 | try std.testing.expectEqualSlices(u8, &bytes, &dk); |
| 603 | } |
| 604 | |
| 605 | test "kdf rfc 1" { |
| 606 | if (!run_long_tests) return error.SkipZigTest; |
| 607 | |
| 608 | const password = ""; |
| 609 | const salt = ""; |
| 610 | |
| 611 | var dk: [64]u8 = undefined; |
| 612 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 4, .r = 1, .p = 1 }); |
| 613 | |
| 614 | const hex = "77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906"; |
| 615 | var bytes: [hex.len / 2]u8 = undefined; |
| 616 | _ = try fmt.hexToBytes(&bytes, hex); |
| 617 | |
| 618 | try std.testing.expectEqualSlices(u8, &bytes, &dk); |
| 619 | } |
| 620 | |
| 621 | test "kdf rfc 2" { |
| 622 | if (!run_long_tests) return error.SkipZigTest; |
| 623 | |
| 624 | const password = "password"; |
| 625 | const salt = "NaCl"; |
| 626 | |
| 627 | var dk: [64]u8 = undefined; |
| 628 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 10, .r = 8, .p = 16 }); |
| 629 | |
| 630 | const hex = "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"; |
| 631 | var bytes: [hex.len / 2]u8 = undefined; |
| 632 | _ = try fmt.hexToBytes(&bytes, hex); |
| 633 | |
| 634 | try std.testing.expectEqualSlices(u8, &bytes, &dk); |
| 635 | } |
| 636 | |
| 637 | test "kdf rfc 3" { |
| 638 | if (!run_long_tests) return error.SkipZigTest; |
| 639 | |
| 640 | const password = "pleaseletmein"; |
| 641 | const salt = "SodiumChloride"; |
| 642 | |
| 643 | var dk: [64]u8 = undefined; |
| 644 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 14, .r = 8, .p = 1 }); |
| 645 | |
| 646 | const hex = "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887"; |
| 647 | var bytes: [hex.len / 2]u8 = undefined; |
| 648 | _ = try fmt.hexToBytes(&bytes, hex); |
| 649 | |
| 650 | try std.testing.expectEqualSlices(u8, &bytes, &dk); |
| 651 | } |
| 652 | |
| 653 | test "kdf rfc 4" { |
| 654 | if (!run_long_tests) return error.SkipZigTest; |
| 655 | |
| 656 | const password = "pleaseletmein"; |
| 657 | const salt = "SodiumChloride"; |
| 658 | |
| 659 | var dk: [64]u8 = undefined; |
| 660 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 20, .r = 8, .p = 1 }); |
| 661 | |
| 662 | const hex = "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4"; |
| 663 | var bytes: [hex.len / 2]u8 = undefined; |
| 664 | _ = try fmt.hexToBytes(&bytes, hex); |
| 665 | |
| 666 | try std.testing.expectEqualSlices(u8, &bytes, &dk); |
| 667 | } |
| 668 | |
| 669 | test "password hashing (crypt format)" { |
| 670 | if (!run_long_tests) return error.SkipZigTest; |
| 671 | |
| 672 | const alloc = std.testing.allocator; |
| 673 | const io = std.testing.io; |
| 674 | |
| 675 | const str = "$7$A6....1....TrXs5Zk6s8sWHpQgWDIXTR8kUU3s6Jc3s.DtdS8M2i4$a4ik5hGDN7foMuHOW.cp.CtX01UyCeO0.JAG.AHPpx5"; |
| 676 | const password = "Y0!?iQa9M%5ekffW(`"; |
| 677 | try CryptFormatHasher.verify(alloc, str, password); |
| 678 | |
| 679 | const params = Params.interactive; |
| 680 | var buf: [CryptFormatHasher.pwhash_str_length]u8 = undefined; |
| 681 | const str2 = try CryptFormatHasher.create(alloc, password, params, &buf, io); |
| 682 | try CryptFormatHasher.verify(alloc, str2, password); |
| 683 | } |
| 684 | |
| 685 | test "strHash and strVerify" { |
| 686 | if (!run_long_tests) return error.SkipZigTest; |
| 687 | |
| 688 | const alloc = std.testing.allocator; |
| 689 | const io = std.testing.io; |
| 690 | |
| 691 | const password = "testpass"; |
| 692 | const params = Params.interactive; |
| 693 | const verify_options: VerifyOptions = .{ .allocator = alloc }; |
| 694 | var buf: [128]u8 = undefined; |
| 695 | |
| 696 | { |
| 697 | const str = try strHash( |
| 698 | password, |
| 699 | .{ .allocator = alloc, .params = params, .encoding = .crypt }, |
| 700 | &buf, |
| 701 | io, |
| 702 | ); |
| 703 | try strVerify(str, password, verify_options); |
| 704 | } |
| 705 | { |
| 706 | const str = try strHash( |
| 707 | password, |
| 708 | .{ .allocator = alloc, .params = params, .encoding = .phc }, |
| 709 | &buf, |
| 710 | io, |
| 711 | ); |
| 712 | try strVerify(str, password, verify_options); |
| 713 | } |
| 714 | } |
| 715 | |
| 716 | test "unix-scrypt" { |
| 717 | if (!run_long_tests) return error.SkipZigTest; |
| 718 | |
| 719 | const alloc = std.testing.allocator; |
| 720 | |
| 721 | // https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt |
| 722 | { |
| 723 | const str = "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"; |
| 724 | const password = "pleaseletmein"; |
| 725 | try strVerify(str, password, .{ .allocator = alloc }); |
| 726 | } |
| 727 | // one of the libsodium test vectors |
| 728 | { |
| 729 | const str = "$7$B6....1....75gBMAGwfFWZqBdyF3WdTQnWdUsuTiWjG1fF9c1jiSD$tc8RoB3.Em3/zNgMLWo2u00oGIoTyJv4fl3Fl8Tix72"; |
| 730 | const password = "^T5H$JYt39n%K*j:W]!1s?vg!:jGi]Ax?..l7[p0v:1jHTpla9;]bUN;?bWyCbtqg nrDFal+Jxl3,2`#^tFSu%v_+7iYse8-cCkNf!tD=KrW)"; |
| 731 | try strVerify(str, password, .{ .allocator = alloc }); |
| 732 | } |
| 733 | } |
| 734 | |
| 735 | test "crypt format" { |
| 736 | const str = "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"; |
| 737 | const params = try crypt_format.deserialize(crypt_format.HashResult(32), str); |
| 738 | var buf: [str.len]u8 = undefined; |
| 739 | const s1 = try crypt_format.serialize(params, &buf); |
| 740 | try std.testing.expectEqualStrings(s1, str); |
| 741 | } |
| 742 | |
| 743 | test "kdf fast" { |
| 744 | const TestVector = struct { |
| 745 | password: []const u8, |
| 746 | salt: []const u8, |
| 747 | params: Params, |
| 748 | want: []const u8, |
| 749 | }; |
| 750 | const test_vectors = [_]TestVector{ |
| 751 | .{ |
| 752 | .password = "p", |
| 753 | .salt = "s", |
| 754 | .params = .{ .ln = 1, .r = 1, .p = 1 }, |
| 755 | .want = &([_]u8{ |
| 756 | 0x48, 0xb0, 0xd2, 0xa8, 0xa3, 0x27, 0x26, 0x11, |
| 757 | 0x98, 0x4c, 0x50, 0xeb, 0xd6, 0x30, 0xaf, 0x52, |
| 758 | }), |
| 759 | }, |
| 760 | }; |
| 761 | inline for (test_vectors) |v| { |
| 762 | var dk: [v.want.len]u8 = undefined; |
| 763 | try kdf(std.testing.allocator, &dk, v.password, v.salt, v.params); |
| 764 | try std.testing.expectEqualSlices(u8, &dk, v.want); |
| 765 | } |
| 766 | } |
| 767 | |
| 768 | test "strHashWithSalt deterministic" { |
| 769 | const alloc = std.testing.allocator; |
| 770 | const password = "testpass"; |
| 771 | const salt: [default_salt_len]u8 = "0123456789abcdef0123456789abcdef".*; |
| 772 | const params: Params = .{ .ln = 1, .r = 1, .p = 1 }; |
| 773 | |
| 774 | var buf1: [128]u8 = undefined; |
| 775 | var buf2: [128]u8 = undefined; |
| 776 | |
| 777 | const str1 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf1, &salt); |
| 778 | const str2 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf2, &salt); |
| 779 | try std.testing.expectEqualStrings(str1, str2); |
| 780 | try strVerify(str1, password, .{ .allocator = alloc }); |
| 781 | |
| 782 | const str3 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf1, &salt); |
| 783 | const str4 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf2, &salt); |
| 784 | try std.testing.expectEqualStrings(str3, str4); |
| 785 | try strVerify(str3, password, .{ .allocator = alloc }); |
| 786 | } |