struct phc_format [src]

Alias for std.crypto.phc_encoding

Members

Source

const std = @import("std"); const fmt = std.fmt; const io = std.io; const mem = std.mem; const meta = std.meta; const fields_delimiter = "$"; const fields_delimiter_scalar = '$'; const version_param_name = "v"; const params_delimiter = ","; const params_delimiter_scalar = ','; const kv_delimiter = "="; const kv_delimiter_scalar = '='; pub const Error = std.crypto.errors.EncodingError || error{NoSpaceLeft}; const B64Decoder = std.base64.standard_no_pad.Decoder; const B64Encoder = std.base64.standard_no_pad.Encoder; /// A wrapped binary value whose maximum size is `max_len`. /// /// This type must be used whenever a binary value is encoded in a PHC-formatted string. /// This includes `salt`, `hash`, and any other binary parameters such as keys. /// /// Once initialized, the actual value can be read with the `constSlice()` function. pub fn BinValue(comptime max_len: usize) type { return struct { const Self = @This(); const capacity = max_len; const max_encoded_length = B64Encoder.calcSize(max_len); buf: [max_len]u8 = undefined, len: usize = 0, /// Wrap an existing byte slice pub fn fromSlice(slice: []const u8) Error!Self { if (slice.len > capacity) return Error.NoSpaceLeft; var bin_value: Self = undefined; @memcpy(bin_value.buf[0..slice.len], slice); bin_value.len = slice.len; return bin_value; } /// Return the slice containing the actual value. pub fn constSlice(self: *const Self) []const u8 { return self.buf[0..self.len]; } fn fromB64(self: *Self, str: []const u8) !void { const len = B64Decoder.calcSizeForSlice(str) catch return Error.InvalidEncoding; if (len > self.buf.len) return Error.NoSpaceLeft; B64Decoder.decode(&self.buf, str) catch return Error.InvalidEncoding; self.len = len; } fn toB64(self: *const Self, buf: []u8) ![]const u8 { const value = self.constSlice(); const len = B64Encoder.calcSize(value.len); if (len > buf.len) return Error.NoSpaceLeft; return B64Encoder.encode(buf, value); } }; } /// Deserialize a PHC-formatted string into a structure `HashResult`. /// /// Required field in the `HashResult` structure: /// - `alg_id`: algorithm identifier /// Optional, special fields: /// - `alg_version`: algorithm version (unsigned integer) /// - `salt`: salt /// - `hash`: output of the hash function /// /// Other fields will also be deserialized from the function parameters section. pub fn deserialize(comptime HashResult: type, str: []const u8) Error!HashResult { if (@hasField(HashResult, version_param_name)) { @compileError("Field name '" ++ version_param_name ++ "'' is reserved for the algorithm version"); } var out = mem.zeroes(HashResult); var it = mem.splitScalar(u8, str, fields_delimiter_scalar); var set_fields: usize = 0; while (true) { // Read the algorithm identifier if ((it.next() orelse return Error.InvalidEncoding).len != 0) return Error.InvalidEncoding; out.alg_id = it.next() orelse return Error.InvalidEncoding; set_fields += 1; // Read the optional version number var field = it.next() orelse break; if (kvSplit(field)) |opt_version| { if (mem.eql(u8, opt_version.key, version_param_name)) { if (@hasField(HashResult, "alg_version")) { const value_type_info = switch (@typeInfo(@TypeOf(out.alg_version))) { .optional => |opt| @typeInfo(opt.child), else => |t| t, }; out.alg_version = fmt.parseUnsigned( @Type(value_type_info), opt_version.value, 10, ) catch return Error.InvalidEncoding; set_fields += 1; } field = it.next() orelse break; } } else |_| {} // Read optional parameters var has_params = false; var it_params = mem.splitScalar(u8, field, params_delimiter_scalar); while (it_params.next()) |params| { const param = kvSplit(params) catch break; var found = false; inline for (comptime meta.fields(HashResult)) |p| { if (mem.eql(u8, p.name, param.key)) { switch (@typeInfo(p.type)) { .int => @field(out, p.name) = fmt.parseUnsigned( p.type, param.value, 10, ) catch return Error.InvalidEncoding, .pointer => |ptr| { if (!ptr.is_const) @compileError("Value slice must be constant"); @field(out, p.name) = param.value; }, .@"struct" => try @field(out, p.name).fromB64(param.value), else => std.debug.panic( "Value for [{s}] must be an integer, a constant slice or a BinValue", .{p.name}, ), } set_fields += 1; found = true; break; } } if (!found) return Error.InvalidEncoding; // An unexpected parameter was found in the string has_params = true; } // No separator between an empty parameters set and the salt if (has_params) field = it.next() orelse break; // Read an optional salt if (@hasField(HashResult, "salt")) { try out.salt.fromB64(field); set_fields += 1; } else { return Error.InvalidEncoding; } // Read an optional hash field = it.next() orelse break; if (@hasField(HashResult, "hash")) { try out.hash.fromB64(field); set_fields += 1; } else { return Error.InvalidEncoding; } break; } // Check that all the required fields have been set, excluding optional values and parameters // with default values var expected_fields: usize = 0; inline for (comptime meta.fields(HashResult)) |p| { if (@typeInfo(p.type) != .optional and p.default_value_ptr == null) { expected_fields += 1; } } if (set_fields < expected_fields) return Error.InvalidEncoding; return out; } /// Serialize parameters into a PHC string. /// /// Required field for `params`: /// - `alg_id`: algorithm identifier /// Optional, special fields: /// - `alg_version`: algorithm version (unsigned integer) /// - `salt`: salt /// - `hash`: output of the hash function /// /// `params` can also include any additional parameters. pub fn serialize(params: anytype, str: []u8) Error![]const u8 { var buf = io.fixedBufferStream(str); try serializeTo(params, buf.writer()); return buf.getWritten(); } /// Compute the number of bytes required to serialize `params` pub fn calcSize(params: anytype) usize { var buf = io.countingWriter(io.null_writer); serializeTo(params, buf.writer()) catch unreachable; return @as(usize, @intCast(buf.bytes_written)); } fn serializeTo(params: anytype, out: anytype) !void { const HashResult = @TypeOf(params); if (@hasField(HashResult, version_param_name)) { @compileError("Field name '" ++ version_param_name ++ "'' is reserved for the algorithm version"); } try out.writeAll(fields_delimiter); try out.writeAll(params.alg_id); if (@hasField(HashResult, "alg_version")) { if (@typeInfo(@TypeOf(params.alg_version)) == .optional) { if (params.alg_version) |alg_version| { try out.print( "{s}{s}{s}{}", .{ fields_delimiter, version_param_name, kv_delimiter, alg_version }, ); } } else { try out.print( "{s}{s}{s}{}", .{ fields_delimiter, version_param_name, kv_delimiter, params.alg_version }, ); } } var has_params = false; inline for (comptime meta.fields(HashResult)) |p| { if (comptime !(mem.eql(u8, p.name, "alg_id") or mem.eql(u8, p.name, "alg_version") or mem.eql(u8, p.name, "hash") or mem.eql(u8, p.name, "salt"))) { const value = @field(params, p.name); try out.writeAll(if (has_params) params_delimiter else fields_delimiter); if (@typeInfo(p.type) == .@"struct") { var buf: [@TypeOf(value).max_encoded_length]u8 = undefined; try out.print("{s}{s}{s}", .{ p.name, kv_delimiter, try value.toB64(&buf) }); } else { try out.print( if (@typeInfo(@TypeOf(value)) == .pointer) "{s}{s}{s}" else "{s}{s}{}", .{ p.name, kv_delimiter, value }, ); } has_params = true; } } var has_salt = false; if (@hasField(HashResult, "salt")) { var buf: [@TypeOf(params.salt).max_encoded_length]u8 = undefined; try out.print("{s}{s}", .{ fields_delimiter, try params.salt.toB64(&buf) }); has_salt = true; } if (@hasField(HashResult, "hash")) { var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined; if (!has_salt) try out.writeAll(fields_delimiter); try out.print("{s}{s}", .{ fields_delimiter, try params.hash.toB64(&buf) }); } } // Split a `key=value` string into `key` and `value` fn kvSplit(str: []const u8) !struct { key: []const u8, value: []const u8 } { var it = mem.splitScalar(u8, str, kv_delimiter_scalar); const key = it.first(); const value = it.next() orelse return Error.InvalidEncoding; return .{ .key = key, .value = value }; } test "phc format - encoding/decoding" { const Input = struct { str: []const u8, HashResult: type, }; const inputs = [_]Input{ .{ .str = "$argon2id$v=19$key=a2V5,m=4096,t=0,p=1$X1NhbHQAAAAAAAAAAAAAAA$bWh++MKN1OiFHKgIWTLvIi1iHicmHH7+Fv3K88ifFfI", .HashResult = struct { alg_id: []const u8, alg_version: u16, key: BinValue(16), m: usize, t: u64, p: u32, salt: BinValue(16), hash: BinValue(32), }, }, .{ .str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M", .HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30, salt: BinValue(16), hash: BinValue(16), }, }, .{ .str = "$scrypt", .HashResult = struct { alg_id: []const u8 }, }, .{ .str = "$scrypt$v=1", .HashResult = struct { alg_id: []const u8, alg_version: u16 } }, .{ .str = "$scrypt$ln=15,r=8,p=1", .HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 }, }, .{ .str = "$scrypt$c2FsdHNhbHQ", .HashResult = struct { alg_id: []const u8, salt: BinValue(16) }, }, .{ .str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ", .HashResult = struct { alg_id: []const u8, alg_version: u16, ln: u6, r: u30, p: u30, salt: BinValue(16), }, }, .{ .str = "$scrypt$v=1$ln=15,r=8,p=1", .HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 }, }, .{ .str = "$scrypt$v=1$c2FsdHNhbHQ$dGVzdHBhc3M", .HashResult = struct { alg_id: []const u8, alg_version: u16, salt: BinValue(16), hash: BinValue(16), }, }, .{ .str = "$scrypt$v=1$c2FsdHNhbHQ", .HashResult = struct { alg_id: []const u8, alg_version: u16, salt: BinValue(16) }, }, .{ .str = "$scrypt$c2FsdHNhbHQ$dGVzdHBhc3M", .HashResult = struct { alg_id: []const u8, salt: BinValue(16), hash: BinValue(16) }, }, }; inline for (inputs) |input| { const v = try deserialize(input.HashResult, input.str); var buf: [input.str.len]u8 = undefined; const s1 = try serialize(v, &buf); try std.testing.expectEqualSlices(u8, input.str, s1); } } test "phc format - empty input string" { const s = ""; const v = deserialize(struct { alg_id: []const u8 }, s); try std.testing.expectError(Error.InvalidEncoding, v); } test "phc format - hash without salt" { const s = "$scrypt"; const v = deserialize(struct { alg_id: []const u8, hash: BinValue(16) }, s); try std.testing.expectError(Error.InvalidEncoding, v); } test "phc format - calcSize" { const s = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M"; const v = try deserialize(struct { alg_id: []const u8, alg_version: u16, ln: u6, r: u30, p: u30, salt: BinValue(8), hash: BinValue(8), }, s); try std.testing.expectEqual(calcSize(v), s.len); }