| author | |
| committer | |
| log | 8c41a8e761cb609d927b0f2c3c3c094970d2eb58 |
| tree | 3d4c0e5b39427c257144d5fe109c77499a26a0f6 |
| parent | a98fa56ae9ad437d3e4241bc2c231e0745766ba9 |
| signature |
add phc encoding parser
add password hash functions to benchmark
change bcrypt to be consistent with scrypt
Co-authored-by: lucky <>5 files changed, 1391 insertions(+), 103 deletions(-)
lib/std/crypto.zig+9| ... | @@ -110,7 +110,16 @@ pub const onetimeauth = struct { | ... | @@ -110,7 +110,16 @@ pub const onetimeauth = struct { |
| 110 | /// | 110 | /// |
| 111 | /// Password hashing functions must be used whenever sensitive data has to be directly derived from a password. | 111 | /// Password hashing functions must be used whenever sensitive data has to be directly derived from a password. |
| 112 | pub const pwhash = struct { | 112 | pub const pwhash = struct { |
| 113 | pub const Encoding = enum { | ||
| 114 | phc, | ||
| 115 | crypt, | ||
| 116 | }; | ||
| 117 | pub const KdfError = errors.Error || std.mem.Allocator.Error; | ||
| 118 | pub const HasherError = KdfError || @import("crypto/phc_encoding.zig").Error; | ||
| 119 | pub const Error = HasherError || error{AllocatorRequired}; | ||
| 120 | |||
| 113 | pub const bcrypt = @import("crypto/bcrypt.zig"); | 121 | pub const bcrypt = @import("crypto/bcrypt.zig"); |
| 122 | pub const scrypt = @import("crypto/scrypt.zig"); | ||
| 114 | pub const pbkdf2 = @import("crypto/pbkdf2.zig").pbkdf2; | 123 | pub const pbkdf2 = @import("crypto/pbkdf2.zig").pbkdf2; |
| 115 | }; | 124 | }; |
| 116 | 125 |
lib/std/crypto/bcrypt.zig+298-103| ... | @@ -6,21 +6,28 @@ | ... | @@ -6,21 +6,28 @@ |
| 6 | 6 | ||
| 7 | const std = @import("std"); | 7 | const std = @import("std"); |
| 8 | const crypto = std.crypto; | 8 | const crypto = std.crypto; |
| 9 | const debug = std.debug; | ||
| 9 | const fmt = std.fmt; | 10 | const fmt = std.fmt; |
| 10 | const math = std.math; | 11 | const math = std.math; |
| 11 | const mem = std.mem; | 12 | const mem = std.mem; |
| 12 | const debug = std.debug; | 13 | const pwhash = crypto.pwhash; |
| 13 | const testing = std.testing; | 14 | const testing = std.testing; |
| 14 | const utils = crypto.utils; | 15 | const utils = crypto.utils; |
| 15 | const EncodingError = crypto.errors.EncodingError; | 16 | |
| 16 | const PasswordVerificationError = crypto.errors.PasswordVerificationError; | 17 | const phc_format = @import("phc_encoding.zig"); |
| 18 | |||
| 19 | const KdfError = pwhash.KdfError; | ||
| 20 | const HasherError = pwhash.HasherError; | ||
| 21 | const EncodingError = phc_format.Error; | ||
| 22 | const Error = pwhash.Error; | ||
| 17 | 23 | ||
| 18 | const salt_length: usize = 16; | 24 | const salt_length: usize = 16; |
| 19 | const salt_str_length: usize = 22; | 25 | const salt_str_length: usize = 22; |
| 20 | const ct_str_length: usize = 31; | 26 | const ct_str_length: usize = 31; |
| 21 | const ct_length: usize = 24; | 27 | const ct_length: usize = 24; |
| 28 | const dk_length: usize = ct_length - 1; | ||
| 22 | 29 | ||
| 23 | /// Length (in bytes) of a password hash | 30 | /// Length (in bytes) of a password hash in crypt encoding |
| 24 | pub const hash_length: usize = 60; | 31 | pub const hash_length: usize = 60; |
| 25 | 32 | ||
| 26 | const State = struct { | 33 | const State = struct { |
| ... | @@ -139,71 +146,15 @@ const State = struct { | ... | @@ -139,71 +146,15 @@ const State = struct { |
| 139 | } | 146 | } |
| 140 | }; | 147 | }; |
| 141 | 148 | ||
| 142 | // bcrypt has its own variant of base64, with its own alphabet and no padding | 149 | pub const Params = struct { |
| 143 | const Codec = struct { | 150 | rounds_log: u6, |
| 144 | const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||
| 145 | |||
| 146 | fn encode(b64: []u8, bin: []const u8) void { | ||
| 147 | var i: usize = 0; | ||
| 148 | var j: usize = 0; | ||
| 149 | while (i < bin.len) { | ||
| 150 | var c1 = bin[i]; | ||
| 151 | i += 1; | ||
| 152 | b64[j] = alphabet[c1 >> 2]; | ||
| 153 | j += 1; | ||
| 154 | c1 = (c1 & 3) << 4; | ||
| 155 | if (i >= bin.len) { | ||
| 156 | b64[j] = alphabet[c1]; | ||
| 157 | j += 1; | ||
| 158 | break; | ||
| 159 | } | ||
| 160 | var c2 = bin[i]; | ||
| 161 | i += 1; | ||
| 162 | c1 |= (c2 >> 4) & 0x0f; | ||
| 163 | b64[j] = alphabet[c1]; | ||
| 164 | j += 1; | ||
| 165 | c1 = (c2 & 0x0f) << 2; | ||
| 166 | if (i >= bin.len) { | ||
| 167 | b64[j] = alphabet[c1]; | ||
| 168 | j += 1; | ||
| 169 | break; | ||
| 170 | } | ||
| 171 | c2 = bin[i]; | ||
| 172 | i += 1; | ||
| 173 | c1 |= (c2 >> 6) & 3; | ||
| 174 | b64[j] = alphabet[c1]; | ||
| 175 | b64[j + 1] = alphabet[c2 & 0x3f]; | ||
| 176 | j += 2; | ||
| 177 | } | ||
| 178 | debug.assert(j == b64.len); | ||
| 179 | } | ||
| 180 | |||
| 181 | fn decode(bin: []u8, b64: []const u8) EncodingError!void { | ||
| 182 | var i: usize = 0; | ||
| 183 | var j: usize = 0; | ||
| 184 | while (j < bin.len) { | ||
| 185 | const c1 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i]) orelse return error.InvalidEncoding); | ||
| 186 | const c2 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 1]) orelse return error.InvalidEncoding); | ||
| 187 | bin[j] = (c1 << 2) | ((c2 & 0x30) >> 4); | ||
| 188 | j += 1; | ||
| 189 | if (j >= bin.len) { | ||
| 190 | break; | ||
| 191 | } | ||
| 192 | const c3 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 2]) orelse return error.InvalidEncoding); | ||
| 193 | bin[j] = ((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2); | ||
| 194 | j += 1; | ||
| 195 | if (j >= bin.len) { | ||
| 196 | break; | ||
| 197 | } | ||
| 198 | const c4 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 3]) orelse return error.InvalidEncoding); | ||
| 199 | bin[j] = ((c3 & 0x03) << 6) | c4; | ||
| 200 | j += 1; | ||
| 201 | i += 4; | ||
| 202 | } | ||
| 203 | } | ||
| 204 | }; | 151 | }; |
| 205 | 152 | ||
| 206 | fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) ![hash_length]u8 { | 153 | pub fn bcrypt( |
| 154 | password: []const u8, | ||
| 155 | salt: [salt_length]u8, | ||
| 156 | params: Params, | ||
| 157 | ) [dk_length]u8 { | ||
| 207 | var state = State{}; | 158 | var state = State{}; |
| 208 | var password_buf: [73]u8 = undefined; | 159 | var password_buf: [73]u8 = undefined; |
| 209 | const trimmed_len = math.min(password.len, password_buf.len - 1); | 160 | const trimmed_len = math.min(password.len, password_buf.len - 1); |
| ... | @@ -212,7 +163,7 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) | ... | @@ -212,7 +163,7 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) |
| 212 | var passwordZ = password_buf[0 .. trimmed_len + 1]; | 163 | var passwordZ = password_buf[0 .. trimmed_len + 1]; |
| 213 | state.expand(salt[0..], passwordZ); | 164 | state.expand(salt[0..], passwordZ); |
| 214 | 165 | ||
| 215 | const rounds: u64 = @as(u64, 1) << rounds_log; | 166 | const rounds: u64 = @as(u64, 1) << params.rounds_log; |
| 216 | var k: u64 = 0; | 167 | var k: u64 = 0; |
| 217 | while (k < rounds) : (k += 1) { | 168 | while (k < rounds) : (k += 1) { |
| 218 | state.expand0(passwordZ); | 169 | state.expand0(passwordZ); |
| ... | @@ -230,18 +181,203 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) | ... | @@ -230,18 +181,203 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) |
| 230 | for (cdata) |c, i| { | 181 | for (cdata) |c, i| { |
| 231 | mem.writeIntBig(u32, ct[i * 4 ..][0..4], c); | 182 | mem.writeIntBig(u32, ct[i * 4 ..][0..4], c); |
| 232 | } | 183 | } |
| 184 | return ct[0..dk_length].*; | ||
| 185 | } | ||
| 233 | 186 | ||
| 234 | var salt_str: [salt_str_length]u8 = undefined; | 187 | const crypt_format = struct { |
| 235 | Codec.encode(salt_str[0..], salt[0..]); | 188 | /// String prefix for bcrypt |
| 189 | pub const prefix = "$2"; | ||
| 190 | |||
| 191 | // bcrypt has its own variant of base64, with its own alphabet and no padding | ||
| 192 | const Codec = struct { | ||
| 193 | const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||
| 194 | |||
| 195 | fn encode(b64: []u8, bin: []const u8) void { | ||
| 196 | var i: usize = 0; | ||
| 197 | var j: usize = 0; | ||
| 198 | while (i < bin.len) { | ||
| 199 | var c1 = bin[i]; | ||
| 200 | i += 1; | ||
| 201 | b64[j] = alphabet[c1 >> 2]; | ||
| 202 | j += 1; | ||
| 203 | c1 = (c1 & 3) << 4; | ||
| 204 | if (i >= bin.len) { | ||
| 205 | b64[j] = alphabet[c1]; | ||
| 206 | j += 1; | ||
| 207 | break; | ||
| 208 | } | ||
| 209 | var c2 = bin[i]; | ||
| 210 | i += 1; | ||
| 211 | c1 |= (c2 >> 4) & 0x0f; | ||
| 212 | b64[j] = alphabet[c1]; | ||
| 213 | j += 1; | ||
| 214 | c1 = (c2 & 0x0f) << 2; | ||
| 215 | if (i >= bin.len) { | ||
| 216 | b64[j] = alphabet[c1]; | ||
| 217 | j += 1; | ||
| 218 | break; | ||
| 219 | } | ||
| 220 | c2 = bin[i]; | ||
| 221 | i += 1; | ||
| 222 | c1 |= (c2 >> 6) & 3; | ||
| 223 | b64[j] = alphabet[c1]; | ||
| 224 | b64[j + 1] = alphabet[c2 & 0x3f]; | ||
| 225 | j += 2; | ||
| 226 | } | ||
| 227 | debug.assert(j == b64.len); | ||
| 228 | } | ||
| 229 | |||
| 230 | fn decode(bin: []u8, b64: []const u8) EncodingError!void { | ||
| 231 | var i: usize = 0; | ||
| 232 | var j: usize = 0; | ||
| 233 | while (j < bin.len) { | ||
| 234 | const c1 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i]) orelse | ||
| 235 | return EncodingError.InvalidEncoding); | ||
| 236 | const c2 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 1]) orelse | ||
| 237 | return EncodingError.InvalidEncoding); | ||
| 238 | bin[j] = (c1 << 2) | ((c2 & 0x30) >> 4); | ||
| 239 | j += 1; | ||
| 240 | if (j >= bin.len) { | ||
| 241 | break; | ||
| 242 | } | ||
| 243 | const c3 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 2]) orelse | ||
| 244 | return EncodingError.InvalidEncoding); | ||
| 245 | bin[j] = ((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2); | ||
| 246 | j += 1; | ||
| 247 | if (j >= bin.len) { | ||
| 248 | break; | ||
| 249 | } | ||
| 250 | const c4 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 3]) orelse | ||
| 251 | return EncodingError.InvalidEncoding); | ||
| 252 | bin[j] = ((c3 & 0x03) << 6) | c4; | ||
| 253 | j += 1; | ||
| 254 | i += 4; | ||
| 255 | } | ||
| 256 | } | ||
| 257 | }; | ||
| 258 | |||
| 259 | fn strHashInternal( | ||
| 260 | password: []const u8, | ||
| 261 | salt: [salt_length]u8, | ||
| 262 | params: Params, | ||
| 263 | ) [hash_length]u8 { | ||
| 264 | var dk = bcrypt(password, salt, params); | ||
| 265 | |||
| 266 | var salt_str: [salt_str_length]u8 = undefined; | ||
| 267 | Codec.encode(salt_str[0..], salt[0..]); | ||
| 268 | |||
| 269 | var ct_str: [ct_str_length]u8 = undefined; | ||
| 270 | Codec.encode(ct_str[0..], dk[0..]); | ||
| 271 | |||
| 272 | var s_buf: [hash_length]u8 = undefined; | ||
| 273 | const s = fmt.bufPrint( | ||
| 274 | s_buf[0..], | ||
| 275 | "{s}b${d}{d}${s}{s}", | ||
| 276 | .{ prefix, params.rounds_log / 10, params.rounds_log % 10, salt_str, ct_str }, | ||
| 277 | ) catch unreachable; | ||
| 278 | debug.assert(s.len == s_buf.len); | ||
| 279 | return s_buf; | ||
| 280 | } | ||
| 281 | }; | ||
| 236 | 282 | ||
| 237 | var ct_str: [ct_str_length]u8 = undefined; | 283 | /// Hash and verify passwords using the PHC format. |
| 238 | Codec.encode(ct_str[0..], ct[0 .. ct.len - 1]); | 284 | const PhcFormatHasher = struct { |
| 285 | const alg_id = "bcrypt"; | ||
| 286 | const BinValue = phc_format.BinValue; | ||
| 287 | |||
| 288 | const HashResult = struct { | ||
| 289 | alg_id: []const u8, | ||
| 290 | r: u6, | ||
| 291 | salt: BinValue(salt_length), | ||
| 292 | hash: BinValue(dk_length), | ||
| 293 | }; | ||
| 294 | |||
| 295 | /// Return a non-deterministic hash of the password encoded as a PHC-format string | ||
| 296 | pub fn create( | ||
| 297 | password: []const u8, | ||
| 298 | params: Params, | ||
| 299 | buf: []u8, | ||
| 300 | ) HasherError![]const u8 { | ||
| 301 | var salt: [salt_length]u8 = undefined; | ||
| 302 | crypto.random.bytes(&salt); | ||
| 303 | |||
| 304 | const hash = bcrypt(password, salt, params); | ||
| 305 | |||
| 306 | return phc_format.serialize(HashResult{ | ||
| 307 | .alg_id = alg_id, | ||
| 308 | .r = params.rounds_log, | ||
| 309 | .salt = try BinValue(salt_length).fromSlice(&salt), | ||
| 310 | .hash = try BinValue(dk_length).fromSlice(&hash), | ||
| 311 | }, buf); | ||
| 312 | } | ||
| 239 | 313 | ||
| 240 | var s_buf: [hash_length]u8 = undefined; | 314 | /// Verify a password against a PHC-format encoded string |
| 241 | const s = fmt.bufPrint(s_buf[0..], "$2b${d}{d}${s}{s}", .{ rounds_log / 10, rounds_log % 10, salt_str, ct_str }) catch unreachable; | 315 | pub fn verify( |
| 242 | debug.assert(s.len == s_buf.len); | 316 | str: []const u8, |
| 243 | return s_buf; | 317 | password: []const u8, |
| 244 | } | 318 | ) HasherError!void { |
| 319 | const hash_result = try phc_format.deserialize(HashResult, str); | ||
| 320 | |||
| 321 | if (!mem.eql(u8, hash_result.alg_id, alg_id)) return HasherError.PasswordVerificationFailed; | ||
| 322 | if (hash_result.salt.len != salt_length or hash_result.hash.len != dk_length) | ||
| 323 | return HasherError.InvalidEncoding; | ||
| 324 | |||
| 325 | const hash = bcrypt(password, hash_result.salt.buf, .{ .rounds_log = hash_result.r }); | ||
| 326 | const expected_hash = hash_result.hash.constSlice(); | ||
| 327 | |||
| 328 | if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed; | ||
| 329 | } | ||
| 330 | }; | ||
| 331 | |||
| 332 | /// Hash and verify passwords using the modular crypt format. | ||
| 333 | const CryptFormatHasher = struct { | ||
| 334 | /// Length of a string returned by the create() function | ||
| 335 | pub const pwhash_str_length: usize = hash_length; | ||
| 336 | |||
| 337 | /// Return a non-deterministic hash of the password encoded into the modular crypt format | ||
| 338 | pub fn create( | ||
| 339 | password: []const u8, | ||
| 340 | params: Params, | ||
| 341 | buf: []u8, | ||
| 342 | ) HasherError![]const u8 { | ||
| 343 | if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft; | ||
| 344 | |||
| 345 | var salt: [salt_length]u8 = undefined; | ||
| 346 | crypto.random.bytes(&salt); | ||
| 347 | |||
| 348 | const hash = crypt_format.strHashInternal(password, salt, params); | ||
| 349 | mem.copy(u8, buf, &hash); | ||
| 350 | |||
| 351 | return buf[0..pwhash_str_length]; | ||
| 352 | } | ||
| 353 | |||
| 354 | /// Verify a password against a string in modular crypt format | ||
| 355 | pub fn verify( | ||
| 356 | str: []const u8, | ||
| 357 | password: []const u8, | ||
| 358 | ) HasherError!void { | ||
| 359 | if (str.len != pwhash_str_length or str[3] != '$' or str[6] != '$') | ||
| 360 | return HasherError.InvalidEncoding; | ||
| 361 | |||
| 362 | const rounds_log_str = str[4..][0..2]; | ||
| 363 | const rounds_log = fmt.parseInt(u6, rounds_log_str[0..], 10) catch | ||
| 364 | return HasherError.InvalidEncoding; | ||
| 365 | |||
| 366 | const salt_str = str[7..][0..salt_str_length]; | ||
| 367 | var salt: [salt_length]u8 = undefined; | ||
| 368 | try crypt_format.Codec.decode(salt[0..], salt_str[0..]); | ||
| 369 | |||
| 370 | const wanted_s = crypt_format.strHashInternal(password, salt, .{ .rounds_log = rounds_log }); | ||
| 371 | if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed; | ||
| 372 | } | ||
| 373 | }; | ||
| 374 | |||
| 375 | /// Options for hashing a password. | ||
| 376 | pub const HashOptions = struct { | ||
| 377 | allocator: ?*mem.Allocator = null, | ||
| 378 | params: Params, | ||
| 379 | encoding: pwhash.Encoding, | ||
| 380 | }; | ||
| 245 | 381 | ||
| 246 | /// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function. | 382 | /// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function. |
| 247 | /// bcrypt is a computationally expensive and cache-hard function, explicitly designed to slow down exhaustive searches. | 383 | /// bcrypt is a computationally expensive and cache-hard function, explicitly designed to slow down exhaustive searches. |
| ... | @@ -251,24 +387,32 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) | ... | @@ -251,24 +387,32 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) |
| 251 | /// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes. | 387 | /// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes. |
| 252 | /// If this is an issue for your application, hash the password first using a function such as SHA-512, | 388 | /// If this is an issue for your application, hash the password first using a function such as SHA-512, |
| 253 | /// and then use the resulting hash as the password parameter for bcrypt. | 389 | /// and then use the resulting hash as the password parameter for bcrypt. |
| 254 | pub fn strHash(password: []const u8, rounds_log: u6) ![hash_length]u8 { | 390 | pub fn strHash( |
| 255 | var salt: [salt_length]u8 = undefined; | 391 | password: []const u8, |
| 256 | crypto.random.bytes(&salt); | 392 | options: HashOptions, |
| 257 | return strHashInternal(password, rounds_log, salt); | 393 | out: []u8, |
| 394 | ) Error![]const u8 { | ||
| 395 | switch (options.encoding) { | ||
| 396 | .phc => return PhcFormatHasher.create(password, options.params, out), | ||
| 397 | .crypt => return CryptFormatHasher.create(password, options.params, out), | ||
| 398 | } | ||
| 258 | } | 399 | } |
| 259 | 400 | ||
| 401 | /// Options for hash verification. | ||
| 402 | pub const VerifyOptions = struct { | ||
| 403 | allocator: ?*mem.Allocator = null, | ||
| 404 | }; | ||
| 405 | |||
| 260 | /// Verify that a previously computed hash is valid for a given password. | 406 | /// Verify that a previously computed hash is valid for a given password. |
| 261 | pub fn strVerify(h: [hash_length]u8, password: []const u8) (EncodingError || PasswordVerificationError)!void { | 407 | pub fn strVerify( |
| 262 | if (!mem.eql(u8, "$2", h[0..2])) return error.InvalidEncoding; | 408 | str: []const u8, |
| 263 | if (h[3] != '$' or h[6] != '$') return error.InvalidEncoding; | 409 | password: []const u8, |
| 264 | const rounds_log_str = h[4..][0..2]; | 410 | _: VerifyOptions, |
| 265 | const salt_str = h[7..][0..salt_str_length]; | 411 | ) Error!void { |
| 266 | var salt: [salt_length]u8 = undefined; | 412 | if (mem.startsWith(u8, str, crypt_format.prefix)) { |
| 267 | try Codec.decode(salt[0..], salt_str[0..]); | 413 | return CryptFormatHasher.verify(str, password); |
| 268 | const rounds_log = fmt.parseInt(u6, rounds_log_str[0..], 10) catch return error.InvalidEncoding; | 414 | } else { |
| 269 | const wanted_s = try strHashInternal(password, rounds_log, salt); | 415 | return PhcFormatHasher.verify(str, password); |
| 270 | if (!mem.eql(u8, wanted_s[0..], h[0..])) { | ||
| 271 | return error.PasswordVerificationFailed; | ||
| 272 | } | 416 | } |
| 273 | } | 417 | } |
| 274 | 418 | ||
| ... | @@ -276,20 +420,71 @@ test "bcrypt codec" { | ... | @@ -276,20 +420,71 @@ test "bcrypt codec" { |
| 276 | var salt: [salt_length]u8 = undefined; | 420 | var salt: [salt_length]u8 = undefined; |
| 277 | crypto.random.bytes(&salt); | 421 | crypto.random.bytes(&salt); |
| 278 | var salt_str: [salt_str_length]u8 = undefined; | 422 | var salt_str: [salt_str_length]u8 = undefined; |
| 279 | Codec.encode(salt_str[0..], salt[0..]); | 423 | crypt_format.Codec.encode(salt_str[0..], salt[0..]); |
| 280 | var salt2: [salt_length]u8 = undefined; | 424 | var salt2: [salt_length]u8 = undefined; |
| 281 | try Codec.decode(salt2[0..], salt_str[0..]); | 425 | try crypt_format.Codec.decode(salt2[0..], salt_str[0..]); |
| 282 | try testing.expectEqualSlices(u8, salt[0..], salt2[0..]); | 426 | try testing.expectEqualSlices(u8, salt[0..], salt2[0..]); |
| 283 | } | 427 | } |
| 284 | 428 | ||
| 285 | test "bcrypt" { | 429 | test "bcrypt crypt format" { |
| 286 | const s = try strHash("password", 5); | 430 | const hash_options = HashOptions{ |
| 287 | try strVerify(s, "password"); | 431 | .params = .{ .rounds_log = 5 }, |
| 288 | try testing.expectError(error.PasswordVerificationFailed, strVerify(s, "invalid password")); | 432 | .encoding = .crypt, |
| 289 | 433 | }; | |
| 290 | const long_s = try strHash("password" ** 100, 5); | 434 | const verify_options = VerifyOptions{}; |
| 291 | try strVerify(long_s, "password" ** 100); | 435 | |
| 292 | try strVerify(long_s, "password" ** 101); | 436 | var buf: [hash_length]u8 = undefined; |
| 437 | const s = try strHash("password", hash_options, &buf); | ||
| 438 | |||
| 439 | try testing.expect(mem.startsWith(u8, s, crypt_format.prefix)); | ||
| 440 | try strVerify(s, "password", verify_options); | ||
| 441 | try testing.expectError( | ||
| 442 | error.PasswordVerificationFailed, | ||
| 443 | strVerify(s, "invalid password", verify_options), | ||
| 444 | ); | ||
| 445 | |||
| 446 | var long_buf: [hash_length]u8 = undefined; | ||
| 447 | const long_s = try strHash("password" ** 100, hash_options, &long_buf); | ||
| 448 | |||
| 449 | try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix)); | ||
| 450 | try strVerify(long_s, "password" ** 100, verify_options); | ||
| 451 | try strVerify(long_s, "password" ** 101, verify_options); | ||
| 452 | |||
| 453 | try strVerify( | ||
| 454 | "$2b$08$WUQKyBCaKpziCwUXHiMVvu40dYVjkTxtWJlftl0PpjY2BxWSvFIEe", | ||
| 455 | "The devil himself", | ||
| 456 | verify_options, | ||
| 457 | ); | ||
| 458 | } | ||
| 293 | 459 | ||
| 294 | try strVerify("$2b$08$WUQKyBCaKpziCwUXHiMVvu40dYVjkTxtWJlftl0PpjY2BxWSvFIEe".*, "The devil himself"); | 460 | test "bcrypt phc format" { |
| 461 | const hash_options = HashOptions{ | ||
| 462 | .params = .{ .rounds_log = 5 }, | ||
| 463 | .encoding = .phc, | ||
| 464 | }; | ||
| 465 | const verify_options = VerifyOptions{}; | ||
| 466 | const prefix = "$bcrypt$"; | ||
| 467 | |||
| 468 | var buf: [hash_length * 2]u8 = undefined; | ||
| 469 | const s = try strHash("password", hash_options, &buf); | ||
| 470 | |||
| 471 | try testing.expect(mem.startsWith(u8, s, prefix)); | ||
| 472 | try strVerify(s, "password", verify_options); | ||
| 473 | try testing.expectError( | ||
| 474 | error.PasswordVerificationFailed, | ||
| 475 | strVerify(s, "invalid password", verify_options), | ||
| 476 | ); | ||
| 477 | |||
| 478 | var long_buf: [hash_length * 2]u8 = undefined; | ||
| 479 | const long_s = try strHash("password" ** 100, hash_options, &long_buf); | ||
| 480 | |||
| 481 | try testing.expect(mem.startsWith(u8, long_s, prefix)); | ||
| 482 | try strVerify(long_s, "password" ** 100, verify_options); | ||
| 483 | try strVerify(long_s, "password" ** 101, verify_options); | ||
| 484 | |||
| 485 | try strVerify( | ||
| 486 | "$bcrypt$r=5$2NopntlgE2lX3cTwr4qz8A$r3T7iKYQNnY4hAhGjk9RmuyvgrYJZwc", | ||
| 487 | "The devil himself", | ||
| 488 | verify_options, | ||
| 489 | ); | ||
| 295 | } | 490 | } |
lib/std/crypto/benchmark.zig+44| ... | @@ -300,6 +300,43 @@ pub fn benchmarkAes8(comptime Aes: anytype, comptime count: comptime_int) !u64 { | ... | @@ -300,6 +300,43 @@ pub fn benchmarkAes8(comptime Aes: anytype, comptime count: comptime_int) !u64 { |
| 300 | return throughput; | 300 | return throughput; |
| 301 | } | 301 | } |
| 302 | 302 | ||
| 303 | const CryptoPwhash = struct { | ||
| 304 | hashFn: anytype, | ||
| 305 | params: anytype, | ||
| 306 | name: []const u8, | ||
| 307 | }; | ||
| 308 | const bcrypt_params = bcrypt.Params{ .rounds_log = 5 }; | ||
| 309 | const pwhashes = [_]CryptoPwhash{ | ||
| 310 | CryptoPwhash{ .hashFn = bcrypt.strHash, .params = bcrypt_params, .name = "bcrypt" }, | ||
| 311 | CryptoPwhash{ .hashFn = scrypt.strHash, .params = scrypt.Params.interactive, .name = "scrypt" }, | ||
| 312 | }; | ||
| 313 | |||
| 314 | fn benchmarkPwhash( | ||
| 315 | comptime hashFn: anytype, | ||
| 316 | comptime params: anytype, | ||
| 317 | comptime count: comptime_int, | ||
| 318 | ) !u64 { | ||
| 319 | const password = "testpass" ** 2; | ||
| 320 | const opts = .{ .allocator = std.testing.allocator, .params = params, .encoding = .phc }; | ||
| 321 | var buf: [256]u8 = undefined; | ||
| 322 | |||
| 323 | var timer = try Timer.start(); | ||
| 324 | const start = timer.lap(); | ||
| 325 | { | ||
| 326 | var i: usize = 0; | ||
| 327 | while (i < count) : (i += 1) { | ||
| 328 | _ = try hashFn(password, opts, &buf); | ||
| 329 | mem.doNotOptimizeAway(&buf); | ||
| 330 | } | ||
| 331 | } | ||
| 332 | const end = timer.read(); | ||
| 333 | |||
| 334 | const elapsed_s = @intToFloat(f64, end - start) / time.ns_per_s; | ||
| 335 | const throughput = @floatToInt(u64, count / elapsed_s); | ||
| 336 | |||
| 337 | return throughput; | ||
| 338 | } | ||
| 339 | |||
| 303 | fn usage() void { | 340 | fn usage() void { |
| 304 | std.debug.warn( | 341 | std.debug.warn( |
| 305 | \\throughput_test [options] | 342 | \\throughput_test [options] |
| ... | @@ -418,4 +455,11 @@ pub fn main() !void { | ... | @@ -418,4 +455,11 @@ pub fn main() !void { |
| 418 | try stdout.print("{s:>17}: {:10} ops/s\n", .{ E.name, throughput }); | 455 | try stdout.print("{s:>17}: {:10} ops/s\n", .{ E.name, throughput }); |
| 419 | } | 456 | } |
| 420 | } | 457 | } |
| 458 | |||
| 459 | inline for (pwhashes) |H| { | ||
| 460 | if (filter == null or std.mem.indexOf(u8, H.name, filter.?) != null) { | ||
| 461 | const throughput = try benchmarkPwhash(H.hashFn, H.params, mode(64)); | ||
| 462 | try stdout.print("{s:>17}: {:10} ops/s\n", .{ H.name, throughput }); | ||
| 463 | } | ||
| 464 | } | ||
| 421 | } | 465 | } |
lib/std/crypto/phc_encoding.zig created+377| ... | @@ -0,0 +1,377 @@ | ||
| 1 | // SPDX-License-Identifier: MIT | ||
| 2 | // Copyright (c) 2015-2021 Zig Contributors | ||
| 3 | // This file is part of [zig](https://ziglang.org/), which is MIT licensed. | ||
| 4 | // The MIT license requires this copyright notice to be included in all copies | ||
| 5 | // and substantial portions of the software. | ||
| 6 | |||
| 7 | // https://github.com/P-H-C/phc-string-format | ||
| 8 | |||
| 9 | const std = @import("std"); | ||
| 10 | const fmt = std.fmt; | ||
| 11 | const io = std.io; | ||
| 12 | const mem = std.mem; | ||
| 13 | const meta = std.meta; | ||
| 14 | |||
| 15 | const fields_delimiter = "$"; | ||
| 16 | const version_param_name = "v"; | ||
| 17 | const params_delimiter = ","; | ||
| 18 | const kv_delimiter = "="; | ||
| 19 | |||
| 20 | pub const Error = std.crypto.errors.EncodingError || error{NoSpaceLeft}; | ||
| 21 | |||
| 22 | const B64Decoder = std.base64.standard_no_pad.Decoder; | ||
| 23 | const B64Encoder = std.base64.standard_no_pad.Encoder; | ||
| 24 | |||
| 25 | /// A wrapped binary value whose maximum size is `max_len`. | ||
| 26 | /// | ||
| 27 | /// This type must be used whenever a binary value is encoded in a PHC-formatted string. | ||
| 28 | /// This includes `salt`, `hash`, and any other binary parameters such as keys. | ||
| 29 | /// | ||
| 30 | /// Once initialized, the actual value can be read with the `constSlice()` function. | ||
| 31 | pub fn BinValue(comptime max_len: usize) type { | ||
| 32 | return struct { | ||
| 33 | const Self = @This(); | ||
| 34 | const capacity = max_len; | ||
| 35 | const max_encoded_length = B64Encoder.calcSize(max_len); | ||
| 36 | |||
| 37 | buf: [max_len]u8 = undefined, | ||
| 38 | len: usize = 0, | ||
| 39 | |||
| 40 | /// Wrap an existing byte slice | ||
| 41 | pub fn fromSlice(slice: []const u8) Error!Self { | ||
| 42 | if (slice.len > capacity) return Error.NoSpaceLeft; | ||
| 43 | var bin_value: Self = undefined; | ||
| 44 | mem.copy(u8, &bin_value.buf, slice); | ||
| 45 | bin_value.len = slice.len; | ||
| 46 | return bin_value; | ||
| 47 | } | ||
| 48 | |||
| 49 | /// Return the slice containing the actual value. | ||
| 50 | pub fn constSlice(self: Self) []const u8 { | ||
| 51 | return self.buf[0..self.len]; | ||
| 52 | } | ||
| 53 | |||
| 54 | fn fromB64(self: *Self, str: []const u8) !void { | ||
| 55 | const len = B64Decoder.calcSizeForSlice(str) catch return Error.InvalidEncoding; | ||
| 56 | if (len > self.buf.len) return Error.NoSpaceLeft; | ||
| 57 | B64Decoder.decode(&self.buf, str) catch return Error.InvalidEncoding; | ||
| 58 | self.len = len; | ||
| 59 | } | ||
| 60 | |||
| 61 | fn toB64(self: Self, buf: []u8) ![]const u8 { | ||
| 62 | const value = self.constSlice(); | ||
| 63 | const len = B64Encoder.calcSize(value.len); | ||
| 64 | if (len > buf.len) return Error.NoSpaceLeft; | ||
| 65 | return B64Encoder.encode(buf, value); | ||
| 66 | } | ||
| 67 | }; | ||
| 68 | } | ||
| 69 | |||
| 70 | /// Deserialize a PHC-formatted string into a structure `HashResult`. | ||
| 71 | /// | ||
| 72 | /// Required field in the `HashResult` structure: | ||
| 73 | /// - `alg_id`: algorithm identifier | ||
| 74 | /// Optional, special fields: | ||
| 75 | /// - `alg_version`: algorithm version (unsigned integer) | ||
| 76 | /// - `salt`: salt | ||
| 77 | /// - `hash`: output of the hash function | ||
| 78 | /// | ||
| 79 | /// Other fields will also be deserialized from the function parameters section. | ||
| 80 | pub fn deserialize(comptime HashResult: type, str: []const u8) Error!HashResult { | ||
| 81 | var out = mem.zeroes(HashResult); | ||
| 82 | var it = mem.split(u8, str, fields_delimiter); | ||
| 83 | var set_fields: usize = 0; | ||
| 84 | |||
| 85 | while (true) { | ||
| 86 | // Read the algorithm identifier | ||
| 87 | if ((it.next() orelse return Error.InvalidEncoding).len != 0) return Error.InvalidEncoding; | ||
| 88 | out.alg_id = it.next() orelse return Error.InvalidEncoding; | ||
| 89 | set_fields += 1; | ||
| 90 | |||
| 91 | // Read the optional version number | ||
| 92 | var field = it.next() orelse break; | ||
| 93 | if (kvSplit(field)) |opt_version| { | ||
| 94 | if (mem.eql(u8, opt_version.key, version_param_name)) { | ||
| 95 | if (@hasField(HashResult, "alg_version")) { | ||
| 96 | const value_type_info = switch (@typeInfo(@TypeOf(out.alg_version))) { | ||
| 97 | .Optional => |opt| comptime @typeInfo(opt.child), | ||
| 98 | else => |t| t, | ||
| 99 | }; | ||
| 100 | out.alg_version = fmt.parseUnsigned( | ||
| 101 | @Type(value_type_info), | ||
| 102 | opt_version.value, | ||
| 103 | 10, | ||
| 104 | ) catch return Error.InvalidEncoding; | ||
| 105 | set_fields += 1; | ||
| 106 | } | ||
| 107 | field = it.next() orelse break; | ||
| 108 | } | ||
| 109 | } else |_| {} | ||
| 110 | |||
| 111 | // Read optional parameters | ||
| 112 | var has_params = false; | ||
| 113 | var it_params = mem.split(u8, field, params_delimiter); | ||
| 114 | while (it_params.next()) |params| { | ||
| 115 | const param = kvSplit(params) catch break; | ||
| 116 | var found = false; | ||
| 117 | inline for (comptime meta.fields(HashResult)) |p| { | ||
| 118 | if (mem.eql(u8, p.name, param.key)) { | ||
| 119 | switch (@typeInfo(p.field_type)) { | ||
| 120 | .Int => @field(out, p.name) = fmt.parseUnsigned( | ||
| 121 | p.field_type, | ||
| 122 | param.value, | ||
| 123 | 10, | ||
| 124 | ) catch return Error.InvalidEncoding, | ||
| 125 | .Pointer => |ptr| { | ||
| 126 | if (!ptr.is_const) @compileError("Value slice must be constant"); | ||
| 127 | @field(out, p.name) = param.value; | ||
| 128 | }, | ||
| 129 | .Struct => try @field(out, p.name).fromB64(param.value), | ||
| 130 | else => std.debug.panic( | ||
| 131 | "Value for [{s}] must be an integer, a constant slice or a BinValue", | ||
| 132 | .{p.name}, | ||
| 133 | ), | ||
| 134 | } | ||
| 135 | set_fields += 1; | ||
| 136 | found = true; | ||
| 137 | break; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | if (!found) return Error.InvalidEncoding; // An unexpected parameter was found in the string | ||
| 141 | has_params = true; | ||
| 142 | } | ||
| 143 | |||
| 144 | // No separator between an empty parameters set and the salt | ||
| 145 | if (has_params) field = it.next() orelse break; | ||
| 146 | |||
| 147 | // Read an optional salt | ||
| 148 | if (@hasField(HashResult, "salt")) { | ||
| 149 | try out.salt.fromB64(field); | ||
| 150 | set_fields += 1; | ||
| 151 | } else { | ||
| 152 | return Error.InvalidEncoding; | ||
| 153 | } | ||
| 154 | |||
| 155 | // Read an optional hash | ||
| 156 | field = it.next() orelse break; | ||
| 157 | if (@hasField(HashResult, "hash")) { | ||
| 158 | try out.hash.fromB64(field); | ||
| 159 | set_fields += 1; | ||
| 160 | } else { | ||
| 161 | return Error.InvalidEncoding; | ||
| 162 | } | ||
| 163 | break; | ||
| 164 | } | ||
| 165 | |||
| 166 | // Check that all the required fields have been set, excluding optional values and parameters | ||
| 167 | // with default values | ||
| 168 | var expected_fields: usize = 0; | ||
| 169 | inline for (comptime meta.fields(HashResult)) |p| { | ||
| 170 | if (@typeInfo(p.field_type) != .Optional and p.default_value == null) { | ||
| 171 | expected_fields += 1; | ||
| 172 | } | ||
| 173 | } | ||
| 174 | if (set_fields < expected_fields) return Error.InvalidEncoding; | ||
| 175 | |||
| 176 | return out; | ||
| 177 | } | ||
| 178 | |||
| 179 | /// Serialize parameters into a PHC string. | ||
| 180 | /// | ||
| 181 | /// Required field for `params`: | ||
| 182 | /// - `alg_id`: algorithm identifier | ||
| 183 | /// Optional, special fields: | ||
| 184 | /// - `alg_version`: algorithm version (unsigned integer) | ||
| 185 | /// - `salt`: salt | ||
| 186 | /// - `hash`: output of the hash function | ||
| 187 | /// | ||
| 188 | /// `params` can also include any additional parameters. | ||
| 189 | pub fn serialize(params: anytype, str: []u8) Error![]const u8 { | ||
| 190 | var buf = io.fixedBufferStream(str); | ||
| 191 | try serializeTo(params, buf.writer()); | ||
| 192 | return buf.getWritten(); | ||
| 193 | } | ||
| 194 | |||
| 195 | /// Compute the number of bytes required to serialize `params` | ||
| 196 | pub fn calcSize(params: anytype) usize { | ||
| 197 | var buf = io.countingWriter(io.null_writer); | ||
| 198 | serializeTo(params, buf.writer()) catch unreachable; | ||
| 199 | return @intCast(usize, buf.bytes_written); | ||
| 200 | } | ||
| 201 | |||
| 202 | fn serializeTo(params: anytype, out: anytype) !void { | ||
| 203 | const HashResult = @TypeOf(params); | ||
| 204 | try out.writeAll(fields_delimiter); | ||
| 205 | try out.writeAll(params.alg_id); | ||
| 206 | |||
| 207 | if (@hasField(HashResult, "alg_version")) { | ||
| 208 | if (@typeInfo(@TypeOf(params.alg_version)) == .Optional) { | ||
| 209 | if (params.alg_version) |alg_version| { | ||
| 210 | try out.print( | ||
| 211 | "{s}{s}{s}{}", | ||
| 212 | .{ fields_delimiter, version_param_name, kv_delimiter, alg_version }, | ||
| 213 | ); | ||
| 214 | } | ||
| 215 | } else { | ||
| 216 | try out.print( | ||
| 217 | "{s}{s}{s}{}", | ||
| 218 | .{ fields_delimiter, version_param_name, kv_delimiter, params.alg_version }, | ||
| 219 | ); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | var has_params = false; | ||
| 224 | inline for (comptime meta.fields(HashResult)) |p| { | ||
| 225 | if (!(mem.eql(u8, p.name, "alg_id") or | ||
| 226 | mem.eql(u8, p.name, "alg_version") or | ||
| 227 | mem.eql(u8, p.name, "hash") or | ||
| 228 | mem.eql(u8, p.name, "salt"))) | ||
| 229 | { | ||
| 230 | const value = @field(params, p.name); | ||
| 231 | try out.writeAll(if (has_params) params_delimiter else fields_delimiter); | ||
| 232 | if (@typeInfo(p.field_type) == .Struct) { | ||
| 233 | var buf: [@TypeOf(value).max_encoded_length]u8 = undefined; | ||
| 234 | try out.print("{s}{s}{s}", .{ p.name, kv_delimiter, try value.toB64(&buf) }); | ||
| 235 | } else { | ||
| 236 | try out.print( | ||
| 237 | if (@typeInfo(@TypeOf(value)) == .Pointer) "{s}{s}{s}" else "{s}{s}{}", | ||
| 238 | .{ p.name, kv_delimiter, value }, | ||
| 239 | ); | ||
| 240 | } | ||
| 241 | has_params = true; | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | var has_salt = false; | ||
| 246 | if (@hasField(HashResult, "salt")) { | ||
| 247 | var buf: [@TypeOf(params.salt).max_encoded_length]u8 = undefined; | ||
| 248 | try out.print("{s}{s}", .{ fields_delimiter, try params.salt.toB64(&buf) }); | ||
| 249 | has_salt = true; | ||
| 250 | } | ||
| 251 | |||
| 252 | if (@hasField(HashResult, "hash")) { | ||
| 253 | var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined; | ||
| 254 | if (!has_salt) try out.writeAll(fields_delimiter); | ||
| 255 | try out.print("{s}{s}", .{ fields_delimiter, try params.hash.toB64(&buf) }); | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
| 259 | // Split a `key=value` string into `key` and `value` | ||
| 260 | fn kvSplit(str: []const u8) !struct { key: []const u8, value: []const u8 } { | ||
| 261 | var it = mem.split(u8, str, kv_delimiter); | ||
| 262 | const key = it.next() orelse return Error.InvalidEncoding; | ||
| 263 | const value = it.next() orelse return Error.InvalidEncoding; | ||
| 264 | const ret = .{ .key = key, .value = value }; | ||
| 265 | return ret; | ||
| 266 | } | ||
| 267 | |||
| 268 | test "phc format - encoding/decoding" { | ||
| 269 | const Input = struct { | ||
| 270 | str: []const u8, | ||
| 271 | HashResult: type, | ||
| 272 | }; | ||
| 273 | const inputs = [_]Input{ | ||
| 274 | .{ | ||
| 275 | .str = "$argon2id$v=19$key=a2V5,m=4096,t=0,p=1$X1NhbHQAAAAAAAAAAAAAAA$bWh++MKN1OiFHKgIWTLvIi1iHicmHH7+Fv3K88ifFfI", | ||
| 276 | .HashResult = struct { | ||
| 277 | alg_id: []const u8, | ||
| 278 | alg_version: u16, | ||
| 279 | key: BinValue(16), | ||
| 280 | m: usize, | ||
| 281 | t: u64, | ||
| 282 | p: u32, | ||
| 283 | salt: BinValue(16), | ||
| 284 | hash: BinValue(32), | ||
| 285 | }, | ||
| 286 | }, | ||
| 287 | .{ | ||
| 288 | .str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M", | ||
| 289 | .HashResult = struct { | ||
| 290 | alg_id: []const u8, | ||
| 291 | alg_version: ?u30, | ||
| 292 | ln: u6, | ||
| 293 | r: u30, | ||
| 294 | p: u30, | ||
| 295 | salt: BinValue(16), | ||
| 296 | hash: BinValue(16), | ||
| 297 | }, | ||
| 298 | }, | ||
| 299 | .{ | ||
| 300 | .str = "$scrypt", | ||
| 301 | .HashResult = struct { alg_id: []const u8 }, | ||
| 302 | }, | ||
| 303 | .{ .str = "$scrypt$v=1", .HashResult = struct { alg_id: []const u8, alg_version: u16 } }, | ||
| 304 | .{ | ||
| 305 | .str = "$scrypt$ln=15,r=8,p=1", | ||
| 306 | .HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 }, | ||
| 307 | }, | ||
| 308 | .{ | ||
| 309 | .str = "$scrypt$c2FsdHNhbHQ", | ||
| 310 | .HashResult = struct { alg_id: []const u8, salt: BinValue(16) }, | ||
| 311 | }, | ||
| 312 | .{ | ||
| 313 | .str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ", | ||
| 314 | .HashResult = struct { | ||
| 315 | alg_id: []const u8, | ||
| 316 | alg_version: u16, | ||
| 317 | ln: u6, | ||
| 318 | r: u30, | ||
| 319 | p: u30, | ||
| 320 | salt: BinValue(16), | ||
| 321 | }, | ||
| 322 | }, | ||
| 323 | .{ | ||
| 324 | .str = "$scrypt$v=1$ln=15,r=8,p=1", | ||
| 325 | .HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 }, | ||
| 326 | }, | ||
| 327 | .{ | ||
| 328 | .str = "$scrypt$v=1$c2FsdHNhbHQ$dGVzdHBhc3M", | ||
| 329 | .HashResult = struct { | ||
| 330 | alg_id: []const u8, | ||
| 331 | alg_version: u16, | ||
| 332 | salt: BinValue(16), | ||
| 333 | hash: BinValue(16), | ||
| 334 | }, | ||
| 335 | }, | ||
| 336 | .{ | ||
| 337 | .str = "$scrypt$v=1$c2FsdHNhbHQ", | ||
| 338 | .HashResult = struct { alg_id: []const u8, alg_version: u16, salt: BinValue(16) }, | ||
| 339 | }, | ||
| 340 | .{ | ||
| 341 | .str = "$scrypt$c2FsdHNhbHQ$dGVzdHBhc3M", | ||
| 342 | .HashResult = struct { alg_id: []const u8, salt: BinValue(16), hash: BinValue(16) }, | ||
| 343 | }, | ||
| 344 | }; | ||
| 345 | inline for (inputs) |input| { | ||
| 346 | const v = try deserialize(input.HashResult, input.str); | ||
| 347 | var buf: [input.str.len]u8 = undefined; | ||
| 348 | const s1 = try serialize(v, &buf); | ||
| 349 | try std.testing.expectEqualSlices(u8, input.str, s1); | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | test "phc format - empty input string" { | ||
| 354 | const s = ""; | ||
| 355 | const v = deserialize(struct { alg_id: []const u8 }, s); | ||
| 356 | try std.testing.expectError(Error.InvalidEncoding, v); | ||
| 357 | } | ||
| 358 | |||
| 359 | test "phc format - hash without salt" { | ||
| 360 | const s = "$scrypt"; | ||
| 361 | const v = deserialize(struct { alg_id: []const u8, hash: BinValue(16) }, s); | ||
| 362 | try std.testing.expectError(Error.InvalidEncoding, v); | ||
| 363 | } | ||
| 364 | |||
| 365 | test "phc format - calcSize" { | ||
| 366 | const s = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M"; | ||
| 367 | const v = try deserialize(struct { | ||
| 368 | alg_id: []const u8, | ||
| 369 | alg_version: u16, | ||
| 370 | ln: u6, | ||
| 371 | r: u30, | ||
| 372 | p: u30, | ||
| 373 | salt: BinValue(8), | ||
| 374 | hash: BinValue(8), | ||
| 375 | }, s); | ||
| 376 | try std.testing.expectEqual(calcSize(v), s.len); | ||
| 377 | } | ||
lib/std/crypto/scrypt.zig created+663| ... | @@ -0,0 +1,663 @@ | ||
| 1 | // SPDX-License-Identifier: MIT | ||
| 2 | // Copyright (c) 2015-2021 Zig Contributors | ||
| 3 | // This file is part of [zig](https://ziglang.org/), which is MIT licensed. | ||
| 4 | // The MIT license requires this copyright notice to be included in all copies | ||
| 5 | // and substantial portions of the software. | ||
| 6 | |||
| 7 | // https://tools.ietf.org/html/rfc7914 | ||
| 8 | // https://github.com/golang/crypto/blob/master/scrypt/scrypt.go | ||
| 9 | |||
| 10 | const std = @import("std"); | ||
| 11 | const crypto = std.crypto; | ||
| 12 | const fmt = std.fmt; | ||
| 13 | const io = std.io; | ||
| 14 | const math = std.math; | ||
| 15 | const mem = std.mem; | ||
| 16 | const meta = std.meta; | ||
| 17 | const pwhash = crypto.pwhash; | ||
| 18 | |||
| 19 | const phc_format = @import("phc_encoding.zig"); | ||
| 20 | |||
| 21 | const HmacSha256 = crypto.auth.hmac.sha2.HmacSha256; | ||
| 22 | const KdfError = pwhash.KdfError; | ||
| 23 | const HasherError = pwhash.HasherError; | ||
| 24 | const EncodingError = phc_format.Error; | ||
| 25 | const Error = pwhash.Error; | ||
| 26 | |||
| 27 | const max_size = math.maxInt(usize); | ||
| 28 | const max_int = max_size >> 1; | ||
| 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 | fn blockCopy(dst: []align(16) u32, src: []align(16) const u32, n: usize) void { | ||
| 35 | mem.copy(u32, dst, src[0 .. n * 16]); | ||
| 36 | } | ||
| 37 | |||
| 38 | fn blockXor(dst: []align(16) u32, src: []align(16) const u32, n: usize) void { | ||
| 39 | for (src[0 .. n * 16]) |v, i| { | ||
| 40 | dst[i] ^= v; | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | const QuarterRound = struct { a: usize, b: usize, c: usize, d: u6 }; | ||
| 45 | |||
| 46 | fn Rp(a: usize, b: usize, c: usize, d: u6) QuarterRound { | ||
| 47 | return QuarterRound{ .a = a, .b = b, .c = c, .d = d }; | ||
| 48 | } | ||
| 49 | |||
| 50 | fn salsa8core(b: *align(16) [16]u32) void { | ||
| 51 | const arx_steps = comptime [_]QuarterRound{ | ||
| 52 | Rp(4, 0, 12, 7), Rp(8, 4, 0, 9), Rp(12, 8, 4, 13), Rp(0, 12, 8, 18), | ||
| 53 | Rp(9, 5, 1, 7), Rp(13, 9, 5, 9), Rp(1, 13, 9, 13), Rp(5, 1, 13, 18), | ||
| 54 | Rp(14, 10, 6, 7), Rp(2, 14, 10, 9), Rp(6, 2, 14, 13), Rp(10, 6, 2, 18), | ||
| 55 | Rp(3, 15, 11, 7), Rp(7, 3, 15, 9), Rp(11, 7, 3, 13), Rp(15, 11, 7, 18), | ||
| 56 | Rp(1, 0, 3, 7), Rp(2, 1, 0, 9), Rp(3, 2, 1, 13), Rp(0, 3, 2, 18), | ||
| 57 | Rp(6, 5, 4, 7), Rp(7, 6, 5, 9), Rp(4, 7, 6, 13), Rp(5, 4, 7, 18), | ||
| 58 | Rp(11, 10, 9, 7), Rp(8, 11, 10, 9), Rp(9, 8, 11, 13), Rp(10, 9, 8, 18), | ||
| 59 | Rp(12, 15, 14, 7), Rp(13, 12, 15, 9), Rp(14, 13, 12, 13), Rp(15, 14, 13, 18), | ||
| 60 | }; | ||
| 61 | var x = b.*; | ||
| 62 | var j: usize = 0; | ||
| 63 | while (j < 8) : (j += 2) { | ||
| 64 | inline for (arx_steps) |r| { | ||
| 65 | x[r.a] ^= math.rotl(u32, x[r.b] +% x[r.c], r.d); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | j = 0; | ||
| 69 | while (j < 16) : (j += 1) { | ||
| 70 | b[j] +%= x[j]; | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | fn salsaXor(tmp: *align(16) [16]u32, in: []align(16) const u32, out: []align(16) u32) void { | ||
| 75 | blockXor(tmp, in, 1); | ||
| 76 | salsa8core(tmp); | ||
| 77 | blockCopy(out, tmp, 1); | ||
| 78 | } | ||
| 79 | |||
| 80 | fn blockMix(tmp: *align(16) [16]u32, in: []align(16) const u32, out: []align(16) u32, r: u30) void { | ||
| 81 | blockCopy(tmp, in[(2 * r - 1) * 16 ..], 1); | ||
| 82 | var i: usize = 0; | ||
| 83 | while (i < 2 * r) : (i += 2) { | ||
| 84 | salsaXor(tmp, in[i * 16 ..], out[i * 8 ..]); | ||
| 85 | salsaXor(tmp, in[i * 16 + 16 ..], out[i * 8 + r * 16 ..]); | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | fn integerify(b: []align(16) const u32, r: u30) u64 { | ||
| 90 | const j = (2 * r - 1) * 16; | ||
| 91 | return @as(u64, b[j]) | @as(u64, b[j + 1]) << 32; | ||
| 92 | } | ||
| 93 | |||
| 94 | fn smix(b: []align(16) u8, r: u30, n: usize, v: []align(16) u32, xy: []align(16) u32) void { | ||
| 95 | var x = xy[0 .. 32 * r]; | ||
| 96 | var y = xy[32 * r ..]; | ||
| 97 | |||
| 98 | for (x) |*v1, j| { | ||
| 99 | v1.* = mem.readIntSliceLittle(u32, b[4 * j ..]); | ||
| 100 | } | ||
| 101 | |||
| 102 | var tmp: [16]u32 align(16) = undefined; | ||
| 103 | var i: usize = 0; | ||
| 104 | while (i < n) : (i += 2) { | ||
| 105 | blockCopy(v[i * (32 * r) ..], x, 2 * r); | ||
| 106 | blockMix(&tmp, x, y, r); | ||
| 107 | |||
| 108 | blockCopy(v[(i + 1) * (32 * r) ..], y, 2 * r); | ||
| 109 | blockMix(&tmp, y, x, r); | ||
| 110 | } | ||
| 111 | |||
| 112 | i = 0; | ||
| 113 | while (i < n) : (i += 2) { | ||
| 114 | var j = @intCast(usize, integerify(x, r) & (n - 1)); | ||
| 115 | blockXor(x, v[j * (32 * r) ..], 2 * r); | ||
| 116 | blockMix(&tmp, x, y, r); | ||
| 117 | |||
| 118 | j = @intCast(usize, integerify(y, r) & (n - 1)); | ||
| 119 | blockXor(y, v[j * (32 * r) ..], 2 * r); | ||
| 120 | blockMix(&tmp, y, x, r); | ||
| 121 | } | ||
| 122 | |||
| 123 | for (x) |v1, j| { | ||
| 124 | mem.writeIntLittle(u32, b[4 * j ..][0..4], v1); | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | pub const Params = struct { | ||
| 129 | const Self = @This(); | ||
| 130 | |||
| 131 | ln: u6, | ||
| 132 | r: u30, | ||
| 133 | p: u30, | ||
| 134 | |||
| 135 | /// Baseline parameters for interactive logins | ||
| 136 | pub const interactive = Self.fromLimits(524288, 16777216); | ||
| 137 | |||
| 138 | /// Baseline parameters for offline usage | ||
| 139 | pub const sensitive = Self.fromLimits(33554432, 1073741824); | ||
| 140 | |||
| 141 | /// Create parameters from ops and mem limits | ||
| 142 | pub fn fromLimits(ops_limit: u64, mem_limit: usize) Self { | ||
| 143 | const ops = math.max(32768, ops_limit); | ||
| 144 | const r: u30 = 8; | ||
| 145 | if (ops < mem_limit / 32) { | ||
| 146 | const max_n = ops / (r * 4); | ||
| 147 | return Self{ .r = r, .p = 1, .ln = @intCast(u6, math.log2(max_n)) }; | ||
| 148 | } else { | ||
| 149 | const max_n = mem_limit / (@intCast(usize, r) * 128); | ||
| 150 | const ln = @intCast(u6, math.log2(max_n)); | ||
| 151 | const max_rp = math.min(0x3fffffff, (ops / 4) / (@as(u64, 1) << ln)); | ||
| 152 | return Self{ .r = r, .p = @intCast(u30, max_rp / @as(u64, r)), .ln = ln }; | ||
| 153 | } | ||
| 154 | } | ||
| 155 | }; | ||
| 156 | |||
| 157 | /// Apply scrypt to generate a key from a password. | ||
| 158 | /// | ||
| 159 | /// scrypt is defined in RFC 7914. | ||
| 160 | /// | ||
| 161 | /// allocator: *mem.Allocator. | ||
| 162 | /// | ||
| 163 | /// derived_key: Slice of appropriate size for generated key. Generally 16 or 32 bytes in length. | ||
| 164 | /// May be uninitialized. All bytes will be overwritten. | ||
| 165 | /// Maximum size is `derived_key.len / 32 == 0xffff_ffff`. | ||
| 166 | /// | ||
| 167 | /// password: Arbitrary sequence of bytes of any length. | ||
| 168 | /// | ||
| 169 | /// salt: Arbitrary sequence of bytes of any length. | ||
| 170 | /// | ||
| 171 | /// params: Params. | ||
| 172 | pub fn kdf( | ||
| 173 | allocator: *mem.Allocator, | ||
| 174 | derived_key: []u8, | ||
| 175 | password: []const u8, | ||
| 176 | salt: []const u8, | ||
| 177 | params: Params, | ||
| 178 | ) KdfError!void { | ||
| 179 | if (derived_key.len == 0 or derived_key.len / 32 > 0xffff_ffff) return KdfError.OutputTooLong; | ||
| 180 | if (params.ln == 0 or params.r == 0 or params.p == 0) return KdfError.WeakParameters; | ||
| 181 | |||
| 182 | const n64 = @as(u64, 1) << params.ln; | ||
| 183 | if (n64 > max_size) return KdfError.WeakParameters; | ||
| 184 | const n = @intCast(usize, n64); | ||
| 185 | if (@as(u64, params.r) * @as(u64, params.p) >= 1 << 30 or | ||
| 186 | params.r > max_int / 128 / @as(u64, params.p) or | ||
| 187 | params.r > max_int / 256 or | ||
| 188 | n > max_int / 128 / @as(u64, params.r)) return KdfError.WeakParameters; | ||
| 189 | |||
| 190 | var xy = try allocator.alignedAlloc(u32, 16, 64 * params.r); | ||
| 191 | defer allocator.free(xy); | ||
| 192 | var v = try allocator.alignedAlloc(u32, 16, 32 * n * params.r); | ||
| 193 | defer allocator.free(v); | ||
| 194 | var dk = try allocator.alignedAlloc(u8, 16, params.p * 128 * params.r); | ||
| 195 | defer allocator.free(dk); | ||
| 196 | |||
| 197 | try pwhash.pbkdf2(dk, password, salt, 1, HmacSha256); | ||
| 198 | var i: u32 = 0; | ||
| 199 | while (i < params.p) : (i += 1) { | ||
| 200 | smix(dk[i * 128 * params.r ..], params.r, n, v, xy); | ||
| 201 | } | ||
| 202 | try pwhash.pbkdf2(derived_key, password, dk, 1, HmacSha256); | ||
| 203 | } | ||
| 204 | |||
| 205 | const crypt_format = struct { | ||
| 206 | /// String prefix for scrypt | ||
| 207 | pub const prefix = "$7$"; | ||
| 208 | |||
| 209 | /// Standard type for a set of scrypt parameters, with the salt and hash. | ||
| 210 | pub fn HashResult(comptime crypt_max_hash_len: usize) type { | ||
| 211 | return struct { | ||
| 212 | ln: u6, | ||
| 213 | r: u30, | ||
| 214 | p: u30, | ||
| 215 | salt: []const u8, | ||
| 216 | hash: BinValue(crypt_max_hash_len), | ||
| 217 | }; | ||
| 218 | } | ||
| 219 | |||
| 220 | const Codec = CustomB64Codec("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".*); | ||
| 221 | |||
| 222 | /// A wrapped binary value whose maximum size is `max_len`. | ||
| 223 | /// | ||
| 224 | /// This type must be used whenever a binary value is encoded in a PHC-formatted string. | ||
| 225 | /// This includes `salt`, `hash`, and any other binary parameters such as keys. | ||
| 226 | /// | ||
| 227 | /// Once initialized, the actual value can be read with the `constSlice()` function. | ||
| 228 | pub fn BinValue(comptime max_len: usize) type { | ||
| 229 | return struct { | ||
| 230 | const Self = @This(); | ||
| 231 | const capacity = max_len; | ||
| 232 | const max_encoded_length = Codec.encodedLen(max_len); | ||
| 233 | |||
| 234 | buf: [max_len]u8 = undefined, | ||
| 235 | len: usize = 0, | ||
| 236 | |||
| 237 | /// Wrap an existing byte slice | ||
| 238 | pub fn fromSlice(slice: []const u8) EncodingError!Self { | ||
| 239 | if (slice.len > capacity) return EncodingError.NoSpaceLeft; | ||
| 240 | var bin_value: Self = undefined; | ||
| 241 | mem.copy(u8, &bin_value.buf, slice); | ||
| 242 | bin_value.len = slice.len; | ||
| 243 | return bin_value; | ||
| 244 | } | ||
| 245 | |||
| 246 | /// Return the slice containing the actual value. | ||
| 247 | pub fn constSlice(self: Self) []const u8 { | ||
| 248 | return self.buf[0..self.len]; | ||
| 249 | } | ||
| 250 | |||
| 251 | fn fromB64(self: *Self, str: []const u8) !void { | ||
| 252 | const len = Codec.decodedLen(str.len); | ||
| 253 | if (len > self.buf.len) return EncodingError.NoSpaceLeft; | ||
| 254 | try Codec.decode(self.buf[0..len], str); | ||
| 255 | self.len = len; | ||
| 256 | } | ||
| 257 | |||
| 258 | fn toB64(self: Self, buf: []u8) ![]const u8 { | ||
| 259 | const value = self.constSlice(); | ||
| 260 | const len = Codec.encodedLen(value.len); | ||
| 261 | if (len > buf.len) return EncodingError.NoSpaceLeft; | ||
| 262 | var encoded = buf[0..len]; | ||
| 263 | Codec.encode(encoded, value); | ||
| 264 | return encoded; | ||
| 265 | } | ||
| 266 | }; | ||
| 267 | } | ||
| 268 | |||
| 269 | /// Expand binary data into a salt for the modular crypt format. | ||
| 270 | pub fn saltFromBin(comptime len: usize, salt: [len]u8) [Codec.encodedLen(len)]u8 { | ||
| 271 | var buf: [Codec.encodedLen(len)]u8 = undefined; | ||
| 272 | Codec.encode(&buf, &salt); | ||
| 273 | return buf; | ||
| 274 | } | ||
| 275 | |||
| 276 | /// Deserialize a string into a structure `T` (matching `HashResult`). | ||
| 277 | pub fn deserialize(comptime T: type, str: []const u8) EncodingError!T { | ||
| 278 | var out: T = undefined; | ||
| 279 | |||
| 280 | if (str.len < 16) return EncodingError.InvalidEncoding; | ||
| 281 | if (!mem.eql(u8, prefix, str[0..3])) return EncodingError.InvalidEncoding; | ||
| 282 | out.ln = try Codec.intDecode(u6, str[3..4]); | ||
| 283 | out.r = try Codec.intDecode(u30, str[4..9]); | ||
| 284 | out.p = try Codec.intDecode(u30, str[9..14]); | ||
| 285 | |||
| 286 | var it = mem.split(u8, str[14..], "$"); | ||
| 287 | |||
| 288 | const salt = it.next() orelse return EncodingError.InvalidEncoding; | ||
| 289 | if (@hasField(T, "salt")) out.salt = salt; | ||
| 290 | |||
| 291 | const hash_str = it.next() orelse return EncodingError.InvalidEncoding; | ||
| 292 | if (@hasField(T, "hash")) try out.hash.fromB64(hash_str); | ||
| 293 | |||
| 294 | return out; | ||
| 295 | } | ||
| 296 | |||
| 297 | /// Serialize parameters into a string in modular crypt format. | ||
| 298 | pub fn serialize(params: anytype, str: []u8) EncodingError![]const u8 { | ||
| 299 | var buf = io.fixedBufferStream(str); | ||
| 300 | try serializeTo(params, buf.writer()); | ||
| 301 | return buf.getWritten(); | ||
| 302 | } | ||
| 303 | |||
| 304 | /// Compute the number of bytes required to serialize `params` | ||
| 305 | pub fn calcSize(params: anytype) usize { | ||
| 306 | var buf = io.countingWriter(io.null_writer); | ||
| 307 | serializeTo(params, buf.writer()) catch unreachable; | ||
| 308 | return @intCast(usize, buf.bytes_written); | ||
| 309 | } | ||
| 310 | |||
| 311 | fn serializeTo(params: anytype, out: anytype) !void { | ||
| 312 | var header: [14]u8 = undefined; | ||
| 313 | mem.copy(u8, header[0..3], prefix); | ||
| 314 | Codec.intEncode(header[3..4], params.ln); | ||
| 315 | Codec.intEncode(header[4..9], params.r); | ||
| 316 | Codec.intEncode(header[9..14], params.p); | ||
| 317 | try out.writeAll(&header); | ||
| 318 | try out.writeAll(params.salt); | ||
| 319 | try out.writeAll("$"); | ||
| 320 | var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined; | ||
| 321 | const hash_str = try params.hash.toB64(&buf); | ||
| 322 | try out.writeAll(hash_str); | ||
| 323 | } | ||
| 324 | |||
| 325 | /// Custom codec that maps 6 bits into 8 like regular Base64, but uses its own alphabet, | ||
| 326 | /// encodes bits in little-endian, and can also encode integers. | ||
| 327 | fn CustomB64Codec(comptime map: [64]u8) type { | ||
| 328 | return struct { | ||
| 329 | const map64 = map; | ||
| 330 | |||
| 331 | fn encodedLen(len: usize) usize { | ||
| 332 | return (len * 4 + 2) / 3; | ||
| 333 | } | ||
| 334 | |||
| 335 | fn decodedLen(len: usize) usize { | ||
| 336 | return len / 4 * 3 + (len % 4) * 3 / 4; | ||
| 337 | } | ||
| 338 | |||
| 339 | fn intEncode(dst: []u8, src: anytype) void { | ||
| 340 | var n = src; | ||
| 341 | for (dst) |*x| { | ||
| 342 | x.* = map64[@truncate(u6, n)]; | ||
| 343 | n = math.shr(@TypeOf(src), n, 6); | ||
| 344 | } | ||
| 345 | } | ||
| 346 | |||
| 347 | fn intDecode(comptime T: type, src: *const [(meta.bitCount(T) + 5) / 6]u8) !T { | ||
| 348 | var v: T = 0; | ||
| 349 | for (src) |x, i| { | ||
| 350 | const vi = mem.indexOfScalar(u8, &map64, x) orelse return EncodingError.InvalidEncoding; | ||
| 351 | v |= @intCast(T, vi) << @intCast(math.Log2Int(T), i * 6); | ||
| 352 | } | ||
| 353 | return v; | ||
| 354 | } | ||
| 355 | |||
| 356 | fn decode(dst: []u8, src: []const u8) !void { | ||
| 357 | std.debug.assert(dst.len == decodedLen(src.len)); | ||
| 358 | var i: usize = 0; | ||
| 359 | while (i < src.len / 4) : (i += 1) { | ||
| 360 | mem.writeIntSliceLittle(u24, dst[i * 3 ..], try intDecode(u24, src[i * 4 ..][0..4])); | ||
| 361 | } | ||
| 362 | const leftover = src[i * 4 ..]; | ||
| 363 | var v: u24 = 0; | ||
| 364 | for (leftover) |_, j| { | ||
| 365 | v |= @as(u24, try intDecode(u6, leftover[j..][0..1])) << @intCast(u5, j * 6); | ||
| 366 | } | ||
| 367 | for (dst[i * 3 ..]) |*x, j| { | ||
| 368 | x.* = @truncate(u8, v >> @intCast(u5, j * 8)); | ||
| 369 | } | ||
| 370 | } | ||
| 371 | |||
| 372 | fn encode(dst: []u8, src: []const u8) void { | ||
| 373 | std.debug.assert(dst.len == encodedLen(src.len)); | ||
| 374 | var i: usize = 0; | ||
| 375 | while (i < src.len / 3) : (i += 1) { | ||
| 376 | intEncode(dst[i * 4 ..][0..4], mem.readIntSliceLittle(u24, src[i * 3 ..])); | ||
| 377 | } | ||
| 378 | const leftover = src[i * 3 ..]; | ||
| 379 | var v: u24 = 0; | ||
| 380 | for (leftover) |x, j| { | ||
| 381 | v |= @as(u24, x) << @intCast(u5, j * 8); | ||
| 382 | } | ||
| 383 | intEncode(dst[i * 4 ..], v); | ||
| 384 | } | ||
| 385 | }; | ||
| 386 | } | ||
| 387 | }; | ||
| 388 | |||
| 389 | /// Hash and verify passwords using the PHC format. | ||
| 390 | const PhcFormatHasher = struct { | ||
| 391 | const alg_id = "scrypt"; | ||
| 392 | const BinValue = phc_format.BinValue; | ||
| 393 | |||
| 394 | const HashResult = struct { | ||
| 395 | alg_id: []const u8, | ||
| 396 | ln: u6, | ||
| 397 | r: u30, | ||
| 398 | p: u30, | ||
| 399 | salt: BinValue(max_salt_len), | ||
| 400 | hash: BinValue(max_hash_len), | ||
| 401 | }; | ||
| 402 | |||
| 403 | /// Return a non-deterministic hash of the password encoded as a PHC-format string | ||
| 404 | pub fn create( | ||
| 405 | allocator: *mem.Allocator, | ||
| 406 | password: []const u8, | ||
| 407 | params: Params, | ||
| 408 | buf: []u8, | ||
| 409 | ) HasherError![]const u8 { | ||
| 410 | var salt: [default_salt_len]u8 = undefined; | ||
| 411 | crypto.random.bytes(&salt); | ||
| 412 | |||
| 413 | var hash: [default_hash_len]u8 = undefined; | ||
| 414 | try kdf(allocator, &hash, password, &salt, params); | ||
| 415 | |||
| 416 | return phc_format.serialize(HashResult{ | ||
| 417 | .alg_id = alg_id, | ||
| 418 | .ln = params.ln, | ||
| 419 | .r = params.r, | ||
| 420 | .p = params.p, | ||
| 421 | .salt = try BinValue(max_salt_len).fromSlice(&salt), | ||
| 422 | .hash = try BinValue(max_hash_len).fromSlice(&hash), | ||
| 423 | }, buf); | ||
| 424 | } | ||
| 425 | |||
| 426 | /// Verify a password against a PHC-format encoded string | ||
| 427 | pub fn verify( | ||
| 428 | allocator: *mem.Allocator, | ||
| 429 | str: []const u8, | ||
| 430 | password: []const u8, | ||
| 431 | ) HasherError!void { | ||
| 432 | const hash_result = try phc_format.deserialize(HashResult, str); | ||
| 433 | if (!mem.eql(u8, hash_result.alg_id, alg_id)) return HasherError.PasswordVerificationFailed; | ||
| 434 | const params = Params{ .ln = hash_result.ln, .r = hash_result.r, .p = hash_result.p }; | ||
| 435 | const expected_hash = hash_result.hash.constSlice(); | ||
| 436 | var hash_buf: [max_hash_len]u8 = undefined; | ||
| 437 | if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; | ||
| 438 | var hash = hash_buf[0..expected_hash.len]; | ||
| 439 | try kdf(allocator, hash, password, hash_result.salt.constSlice(), params); | ||
| 440 | if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; | ||
| 441 | } | ||
| 442 | }; | ||
| 443 | |||
| 444 | /// Hash and verify passwords using the modular crypt format. | ||
| 445 | const CryptFormatHasher = struct { | ||
| 446 | const BinValue = crypt_format.BinValue; | ||
| 447 | const HashResult = crypt_format.HashResult(max_hash_len); | ||
| 448 | |||
| 449 | /// Length of a string returned by the create() function | ||
| 450 | pub const pwhash_str_length: usize = 101; | ||
| 451 | |||
| 452 | /// Return a non-deterministic hash of the password encoded into the modular crypt format | ||
| 453 | pub fn create( | ||
| 454 | allocator: *mem.Allocator, | ||
| 455 | password: []const u8, | ||
| 456 | params: Params, | ||
| 457 | buf: []u8, | ||
| 458 | ) HasherError![]const u8 { | ||
| 459 | var salt_bin: [default_salt_len]u8 = undefined; | ||
| 460 | crypto.random.bytes(&salt_bin); | ||
| 461 | const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin); | ||
| 462 | |||
| 463 | var hash: [default_hash_len]u8 = undefined; | ||
| 464 | try kdf(allocator, &hash, password, &salt, params); | ||
| 465 | |||
| 466 | return crypt_format.serialize(HashResult{ | ||
| 467 | .ln = params.ln, | ||
| 468 | .r = params.r, | ||
| 469 | .p = params.p, | ||
| 470 | .salt = &salt, | ||
| 471 | .hash = try BinValue(max_hash_len).fromSlice(&hash), | ||
| 472 | }, buf); | ||
| 473 | } | ||
| 474 | |||
| 475 | /// Verify a password against a string in modular crypt format | ||
| 476 | pub fn verify( | ||
| 477 | allocator: *mem.Allocator, | ||
| 478 | str: []const u8, | ||
| 479 | password: []const u8, | ||
| 480 | ) HasherError!void { | ||
| 481 | const hash_result = try crypt_format.deserialize(HashResult, str); | ||
| 482 | const params = Params{ .ln = hash_result.ln, .r = hash_result.r, .p = hash_result.p }; | ||
| 483 | const expected_hash = hash_result.hash.constSlice(); | ||
| 484 | var hash_buf: [max_hash_len]u8 = undefined; | ||
| 485 | if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; | ||
| 486 | var hash = hash_buf[0..expected_hash.len]; | ||
| 487 | try kdf(allocator, hash, password, hash_result.salt, params); | ||
| 488 | if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; | ||
| 489 | } | ||
| 490 | }; | ||
| 491 | |||
| 492 | /// Options for hashing a password. | ||
| 493 | pub const HashOptions = struct { | ||
| 494 | allocator: ?*mem.Allocator, | ||
| 495 | params: Params, | ||
| 496 | encoding: pwhash.Encoding, | ||
| 497 | }; | ||
| 498 | |||
| 499 | /// Compute a hash of a password using the scrypt key derivation function. | ||
| 500 | /// The function returns a string that includes all the parameters required for verification. | ||
| 501 | pub fn strHash( | ||
| 502 | password: []const u8, | ||
| 503 | options: HashOptions, | ||
| 504 | out: []u8, | ||
| 505 | ) Error![]const u8 { | ||
| 506 | const allocator = options.allocator orelse return Error.AllocatorRequired; | ||
| 507 | switch (options.encoding) { | ||
| 508 | .phc => return PhcFormatHasher.create(allocator, password, options.params, out), | ||
| 509 | .crypt => return CryptFormatHasher.create(allocator, password, options.params, out), | ||
| 510 | } | ||
| 511 | } | ||
| 512 | |||
| 513 | /// Options for hash verification. | ||
| 514 | pub const VerifyOptions = struct { | ||
| 515 | allocator: ?*mem.Allocator, | ||
| 516 | }; | ||
| 517 | |||
| 518 | /// Verify that a previously computed hash is valid for a given password. | ||
| 519 | pub fn strVerify( | ||
| 520 | str: []const u8, | ||
| 521 | password: []const u8, | ||
| 522 | options: VerifyOptions, | ||
| 523 | ) Error!void { | ||
| 524 | const allocator = options.allocator orelse return Error.AllocatorRequired; | ||
| 525 | if (mem.startsWith(u8, str, crypt_format.prefix)) { | ||
| 526 | return CryptFormatHasher.verify(allocator, str, password); | ||
| 527 | } else { | ||
| 528 | return PhcFormatHasher.verify(allocator, str, password); | ||
| 529 | } | ||
| 530 | } | ||
| 531 | |||
| 532 | test "scrypt kdf" { | ||
| 533 | const password = "testpass"; | ||
| 534 | const salt = "saltsalt"; | ||
| 535 | |||
| 536 | var dk: [32]u8 = undefined; | ||
| 537 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 15, .r = 8, .p = 1 }); | ||
| 538 | |||
| 539 | const hex = "1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886de"; | ||
| 540 | var bytes: [hex.len / 2]u8 = undefined; | ||
| 541 | _ = try fmt.hexToBytes(&bytes, hex); | ||
| 542 | |||
| 543 | try std.testing.expectEqualSlices(u8, &bytes, &dk); | ||
| 544 | } | ||
| 545 | |||
| 546 | test "scrypt kdf rfc 1" { | ||
| 547 | const password = ""; | ||
| 548 | const salt = ""; | ||
| 549 | |||
| 550 | var dk: [64]u8 = undefined; | ||
| 551 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 4, .r = 1, .p = 1 }); | ||
| 552 | |||
| 553 | const hex = "77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906"; | ||
| 554 | var bytes: [hex.len / 2]u8 = undefined; | ||
| 555 | _ = try fmt.hexToBytes(&bytes, hex); | ||
| 556 | |||
| 557 | try std.testing.expectEqualSlices(u8, &bytes, &dk); | ||
| 558 | } | ||
| 559 | |||
| 560 | test "scrypt kdf rfc 2" { | ||
| 561 | const password = "password"; | ||
| 562 | const salt = "NaCl"; | ||
| 563 | |||
| 564 | var dk: [64]u8 = undefined; | ||
| 565 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 10, .r = 8, .p = 16 }); | ||
| 566 | |||
| 567 | const hex = "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"; | ||
| 568 | var bytes: [hex.len / 2]u8 = undefined; | ||
| 569 | _ = try fmt.hexToBytes(&bytes, hex); | ||
| 570 | |||
| 571 | try std.testing.expectEqualSlices(u8, &bytes, &dk); | ||
| 572 | } | ||
| 573 | |||
| 574 | test "scrypt kdf rfc 3" { | ||
| 575 | const password = "pleaseletmein"; | ||
| 576 | const salt = "SodiumChloride"; | ||
| 577 | |||
| 578 | var dk: [64]u8 = undefined; | ||
| 579 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 14, .r = 8, .p = 1 }); | ||
| 580 | |||
| 581 | const hex = "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887"; | ||
| 582 | var bytes: [hex.len / 2]u8 = undefined; | ||
| 583 | _ = try fmt.hexToBytes(&bytes, hex); | ||
| 584 | |||
| 585 | try std.testing.expectEqualSlices(u8, &bytes, &dk); | ||
| 586 | } | ||
| 587 | |||
| 588 | test "scrypt kdf rfc 4" { | ||
| 589 | // skip slow test | ||
| 590 | if (true) { | ||
| 591 | return error.SkipZigTest; | ||
| 592 | } | ||
| 593 | |||
| 594 | const password = "pleaseletmein"; | ||
| 595 | const salt = "SodiumChloride"; | ||
| 596 | |||
| 597 | var dk: [64]u8 = undefined; | ||
| 598 | try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 20, .r = 8, .p = 1 }); | ||
| 599 | |||
| 600 | const hex = "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4"; | ||
| 601 | var bytes: [hex.len / 2]u8 = undefined; | ||
| 602 | _ = try fmt.hexToBytes(&bytes, hex); | ||
| 603 | |||
| 604 | try std.testing.expectEqualSlices(u8, &bytes, &dk); | ||
| 605 | } | ||
| 606 | |||
| 607 | test "scrypt password hashing (crypt format)" { | ||
| 608 | const str = "$7$A6....1....TrXs5Zk6s8sWHpQgWDIXTR8kUU3s6Jc3s.DtdS8M2i4$a4ik5hGDN7foMuHOW.cp.CtX01UyCeO0.JAG.AHPpx5"; | ||
| 609 | const password = "Y0!?iQa9M%5ekffW(`"; | ||
| 610 | try CryptFormatHasher.verify(std.testing.allocator, str, password); | ||
| 611 | |||
| 612 | const params = Params.interactive; | ||
| 613 | var buf: [CryptFormatHasher.pwhash_str_length]u8 = undefined; | ||
| 614 | const str2 = try CryptFormatHasher.create(std.testing.allocator, password, params, &buf); | ||
| 615 | try CryptFormatHasher.verify(std.testing.allocator, str2, password); | ||
| 616 | } | ||
| 617 | |||
| 618 | test "scrypt strHash and strVerify" { | ||
| 619 | const alloc = std.testing.allocator; | ||
| 620 | |||
| 621 | const password = "testpass"; | ||
| 622 | const verify_options = VerifyOptions{ .allocator = alloc }; | ||
| 623 | var buf: [128]u8 = undefined; | ||
| 624 | |||
| 625 | const s = try strHash( | ||
| 626 | password, | ||
| 627 | HashOptions{ .allocator = alloc, .params = Params.interactive, .encoding = .crypt }, | ||
| 628 | &buf, | ||
| 629 | ); | ||
| 630 | try strVerify(s, password, verify_options); | ||
| 631 | |||
| 632 | const s1 = try strHash( | ||
| 633 | password, | ||
| 634 | HashOptions{ .allocator = alloc, .params = Params.interactive, .encoding = .phc }, | ||
| 635 | &buf, | ||
| 636 | ); | ||
| 637 | try strVerify(s1, password, verify_options); | ||
| 638 | } | ||
| 639 | |||
| 640 | test "scrypt unix-scrypt" { | ||
| 641 | const alloc = std.testing.allocator; | ||
| 642 | |||
| 643 | // https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt | ||
| 644 | { | ||
| 645 | const str = "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"; | ||
| 646 | const password = "pleaseletmein"; | ||
| 647 | try strVerify(str, password, .{ .allocator = alloc }); | ||
| 648 | } | ||
| 649 | // one of the libsodium test vectors | ||
| 650 | { | ||
| 651 | const str = "$7$B6....1....75gBMAGwfFWZqBdyF3WdTQnWdUsuTiWjG1fF9c1jiSD$tc8RoB3.Em3/zNgMLWo2u00oGIoTyJv4fl3Fl8Tix72"; | ||
| 652 | 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)"; | ||
| 653 | try strVerify(str, password, .{ .allocator = alloc }); | ||
| 654 | } | ||
| 655 | } | ||
| 656 | |||
| 657 | test "scrypt crypt format" { | ||
| 658 | const str = "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"; | ||
| 659 | const params = try crypt_format.deserialize(crypt_format.HashResult(32), str); | ||
| 660 | var buf: [str.len]u8 = undefined; | ||
| 661 | const s1 = try crypt_format.serialize(params, &buf); | ||
| 662 | try std.testing.expectEqualStrings(s1, str); | ||
| 663 | } | ||