authorgravatar for 53379023+x13a@users.noreply.github.comlucky <53379023+x13a@users.noreply.github.com> 2021-08-24 14:58:09+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2021-08-24 13:58:09+02:00
log8c41a8e761cb609d927b0f2c3c3c094970d2eb58
tree3d4c0e5b39427c257144d5fe109c77499a26a0f6
parenta98fa56ae9ad437d3e4241bc2c231e0745766ba9
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

add scrypt kdf (#9577)

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.
112pub const pwhash = struct {112pub 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};
116125
lib/std/crypto/bcrypt.zig+298-103
...@@ -6,21 +6,28 @@...@@ -6,21 +6,28 @@
66
7const std = @import("std");7const std = @import("std");
8const crypto = std.crypto;8const crypto = std.crypto;
9const debug = std.debug;
9const fmt = std.fmt;10const fmt = std.fmt;
10const math = std.math;11const math = std.math;
11const mem = std.mem;12const mem = std.mem;
12const debug = std.debug;13const pwhash = crypto.pwhash;
13const testing = std.testing;14const testing = std.testing;
14const utils = crypto.utils;15const utils = crypto.utils;
15const EncodingError = crypto.errors.EncodingError;16
16const PasswordVerificationError = crypto.errors.PasswordVerificationError;17const phc_format = @import("phc_encoding.zig");
18
19const KdfError = pwhash.KdfError;
20const HasherError = pwhash.HasherError;
21const EncodingError = phc_format.Error;
22const Error = pwhash.Error;
1723
18const salt_length: usize = 16;24const salt_length: usize = 16;
19const salt_str_length: usize = 22;25const salt_str_length: usize = 22;
20const ct_str_length: usize = 31;26const ct_str_length: usize = 31;
21const ct_length: usize = 24;27const ct_length: usize = 24;
28const dk_length: usize = ct_length - 1;
2229
23/// Length (in bytes) of a password hash30/// Length (in bytes) of a password hash in crypt encoding
24pub const hash_length: usize = 60;31pub const hash_length: usize = 60;
2532
26const State = struct {33const State = struct {
...@@ -139,71 +146,15 @@ const State = struct {...@@ -139,71 +146,15 @@ const State = struct {
139 }146 }
140};147};
141148
142// bcrypt has its own variant of base64, with its own alphabet and no padding149pub const Params = struct {
143const 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};
205152
206fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) ![hash_length]u8 {153pub 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);
214165
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}
233186
234 var salt_str: [salt_str_length]u8 = undefined;187const 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};
236282
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]);284const 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 }
239313
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.
333const 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.
376pub const HashOptions = struct {
377 allocator: ?*mem.Allocator = null,
378 params: Params,
379 encoding: pwhash.Encoding,
380};
245381
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.
254pub fn strHash(password: []const u8, rounds_log: u6) ![hash_length]u8 {390pub 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}
259400
401/// Options for hash verification.
402pub 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.
261pub fn strVerify(h: [hash_length]u8, password: []const u8) (EncodingError || PasswordVerificationError)!void {407pub 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}
274418
...@@ -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}
284428
285test "bcrypt" {429test "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,
289433 };
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}
293459
294 try strVerify("$2b$08$WUQKyBCaKpziCwUXHiMVvu40dYVjkTxtWJlftl0PpjY2BxWSvFIEe".*, "The devil himself");460test "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}
302302
303const CryptoPwhash = struct {
304 hashFn: anytype,
305 params: anytype,
306 name: []const u8,
307};
308const bcrypt_params = bcrypt.Params{ .rounds_log = 5 };
309const 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
314fn 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
303fn usage() void {340fn 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
9const std = @import("std");
10const fmt = std.fmt;
11const io = std.io;
12const mem = std.mem;
13const meta = std.meta;
14
15const fields_delimiter = "$";
16const version_param_name = "v";
17const params_delimiter = ",";
18const kv_delimiter = "=";
19
20pub const Error = std.crypto.errors.EncodingError || error{NoSpaceLeft};
21
22const B64Decoder = std.base64.standard_no_pad.Decoder;
23const 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.
31pub 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.
80pub 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.
189pub 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`
196pub 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
202fn 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`
260fn 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
268test "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
353test "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
359test "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
365test "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
10const std = @import("std");
11const crypto = std.crypto;
12const fmt = std.fmt;
13const io = std.io;
14const math = std.math;
15const mem = std.mem;
16const meta = std.meta;
17const pwhash = crypto.pwhash;
18
19const phc_format = @import("phc_encoding.zig");
20
21const HmacSha256 = crypto.auth.hmac.sha2.HmacSha256;
22const KdfError = pwhash.KdfError;
23const HasherError = pwhash.HasherError;
24const EncodingError = phc_format.Error;
25const Error = pwhash.Error;
26
27const max_size = math.maxInt(usize);
28const max_int = max_size >> 1;
29const default_salt_len = 32;
30const default_hash_len = 32;
31const max_salt_len = 64;
32const max_hash_len = 64;
33
34fn blockCopy(dst: []align(16) u32, src: []align(16) const u32, n: usize) void {
35 mem.copy(u32, dst, src[0 .. n * 16]);
36}
37
38fn 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
44const QuarterRound = struct { a: usize, b: usize, c: usize, d: u6 };
45
46fn Rp(a: usize, b: usize, c: usize, d: u6) QuarterRound {
47 return QuarterRound{ .a = a, .b = b, .c = c, .d = d };
48}
49
50fn 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
74fn 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
80fn 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
89fn 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
94fn 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
128pub 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.
172pub 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
205const 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.
390const 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.
445const 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.
493pub 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.
501pub 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.
514pub const VerifyOptions = struct {
515 allocator: ?*mem.Allocator,
516};
517
518/// Verify that a previously computed hash is valid for a given password.
519pub 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
532test "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
546test "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
560test "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
574test "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
588test "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
607test "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
618test "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
640test "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
657test "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}