mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
We already have a LICENSE file that covers the Zig Standard Library. We no longer need to remind everyone that the license is MIT in every single file. Previously this was introduced to clarify the situation for a fork of Zig that made Zig's LICENSE file harder to find, and replaced it with their own license that required annual payments to their company. However that fork now appears to be dead. So there is no need to reinforce the copyright notice in every single file.
371 lines
13 KiB
Zig
371 lines
13 KiB
Zig
// https://github.com/P-H-C/phc-string-format
|
|
|
|
const std = @import("std");
|
|
const fmt = std.fmt;
|
|
const io = std.io;
|
|
const mem = std.mem;
|
|
const meta = std.meta;
|
|
|
|
const fields_delimiter = "$";
|
|
const version_param_name = "v";
|
|
const params_delimiter = ",";
|
|
const kv_delimiter = "=";
|
|
|
|
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;
|
|
mem.copy(u8, &bin_value.buf, slice);
|
|
bin_value.len = slice.len;
|
|
return bin_value;
|
|
}
|
|
|
|
/// Return the slice containing the actual value.
|
|
pub fn constSlice(self: 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: 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 {
|
|
var out = mem.zeroes(HashResult);
|
|
var it = mem.split(u8, str, fields_delimiter);
|
|
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| comptime @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.split(u8, field, params_delimiter);
|
|
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.field_type)) {
|
|
.Int => @field(out, p.name) = fmt.parseUnsigned(
|
|
p.field_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.field_type) != .Optional and p.default_value == 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 @intCast(usize, buf.bytes_written);
|
|
}
|
|
|
|
fn serializeTo(params: anytype, out: anytype) !void {
|
|
const HashResult = @TypeOf(params);
|
|
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 (!(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.field_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.split(u8, str, kv_delimiter);
|
|
const key = it.next() orelse return Error.InvalidEncoding;
|
|
const value = it.next() orelse return Error.InvalidEncoding;
|
|
const ret = .{ .key = key, .value = value };
|
|
return ret;
|
|
}
|
|
|
|
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);
|
|
}
|