lib/reticulum/src/crypto/pkcs7.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const block_length: u8 = 16;
4 pub const max_data_length: u16 = 65_519;
5 pub const max_padded_length: u16 = 65_520;
6
7 pub const PadError = error{
8 InputTooLong,
9 OutputTooSmall,
10 };
11
12 pub const UnpadError = error{
13 EmptyInput,
14 InvalidPadding,
15 };
16
17 pub fn paddedLength(data_length: u17) PadError!u16 {
18 if (data_length > max_data_length) return error.InputTooLong;
19 const remainder: u8 = @intCast(data_length % block_length);
20 const padding: u8 = block_length - remainder;
21 const result = data_length + padding;
22 std.debug.assert(result <= max_padded_length);
23 return @intCast(result);
24 }
25
26 /// Writes the padded data into `out` and returns it, with between one and
27 /// sixteen copies of the padding count appended, so a caller fills a plaintext
28 /// out to whole blocks before the cipher runs, following Reticulum@1.5.0
29 /// RNS/Cryptography/PKCS7.py:32-39. Data already a whole number of blocks long
30 /// gains a full block of padding, so the count is never zero. The call returns
31 /// `error.InputTooLong` past 65,519 bytes and `error.OutputTooSmall` when `out`
32 /// is shorter than the padded length.
33 pub fn pad(data: []const u8, out: []u8) PadError![]u8 {
34 if (data.len > max_data_length) return error.InputTooLong;
35 const result_length = try paddedLength(@intCast(data.len));
36 const result_length_usize: usize = result_length;
37 if (out.len < result_length_usize) return error.OutputTooSmall;
38 const result = out[0..result_length_usize];
39 @memmove(result[0..data.len], data);
40 const padding: u8 = @intCast(result.len - data.len);
41 @memset(result[data.len..], padding);
42 return result;
43 }
44
45 /// Returns the data with as many trailing bytes removed as its final byte
46 /// counts, so a caller cuts the padding back off a plaintext the cipher
47 /// produced, following Reticulum@1.5.0 RNS/Cryptography/PKCS7.py:41-48. A final
48 /// byte counting more than sixteen returns `error.InvalidPadding`, and empty
49 /// data returns `error.EmptyInput`. A final byte counting more than the data
50 /// holds is handled the way the reference's Python slicing handles it: that
51 /// slicing leaves twice the length less the count, or nothing at all once the
52 /// count reaches twice the length. A final byte of zero removes nothing.
53 pub fn unpad(data: []const u8) UnpadError![]const u8 {
54 if (data.len == 0) return error.EmptyInput;
55 const padding = data[data.len - 1];
56 if (padding > block_length) return error.InvalidPadding;
57 const stop = pythonSliceStop(data.len, padding);
58 return data[0..stop];
59 }
60
61 fn pythonSliceStop(length: usize, padding: u8) usize {
62 if (padding <= length) return length - padding;
63 std.debug.assert(length < block_length);
64 const doubled = length * 2;
65 if (padding >= doubled) return 0;
66 return doubled - padding;
67 }