aboutsummaryrefslogtreecommitdiff
path: root/lib/std/crypto/phc_encoding.zig
blob: 69324dc5b3a107261ed374a0d8152ed6d1118e6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// 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: *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 {
    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);
}