aboutsummaryrefslogtreecommitdiffstats
path: root/src/root.zig
blob: e4ff7a86e4fe4e73ce522a41ee0ed1bd9e52dff3 (plain) (blame)
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
const std = @import("std");

// TODO: Must be replaced by each user by his/her own nonce (for example from /dev/urandom).
pub const nonce = [_]u8{143, 54, 214, 156, 114, 22, 191, 156, 175, 116, 138, 187, 248, 246, 227, 91, 108, 217, 79, 141, 154, 107, 51, 122};

/// input is a zero terminated byte string to be hashed.
/// output is a buffer of at least 32 bytes
pub export fn sha256(input: [*c]const u8, output: [*c]u8) void {
    std.crypto.hash.sha2.Sha256.hash(std.mem.span(input), output[0..32], .{});
}


/// This code never aborts (asserts or throws an exception)
/// password_hashed is the sha256 hashed password with exactly 32 bytes length
/// encoded_secret is the base64 encoded encrypted secret for the TOTP
/// timestamp is the seconds since the unix epoch
/// length refers to the supposed length of the TOTP key, range 4-10
/// output must be a buffer of at least 32 bytes, since it is reused throughout this function as buffer
pub export fn decode_secret_and_generate_2fa(password_hashed: [*c]const u8, encoded_secret: [*c]const u8, timestamp: u64, length: u8, output: [*c]u8) bool {
    var buffer: [128]u8 = undefined;
    const result = decode_2fa_key(std.mem.span(encoded_secret),password_hashed[0..32],buffer[0..]);
    if(result < 0) return false;
    hotp(buffer[0..@intCast(result)], timestamp/30, length, output[0..32]);
    return true;
}

/// This code never aborts (asserts or throws an exception)
/// encoded_secret is the base64 encoded encrypted secret for TOTP
/// password is the hashed password of exactly 32 bytes length
/// output_buffer holds the decrypted TOTP secret
/// The return value holds the number of bytes written to the output buffer
pub fn decode_2fa_key(encoded_secret: []const u8, password: []const u8, output_buffer: []u8) i16 {
    if (password.len != 32) return -1;
    var buffer: [128]u8 = undefined;
    const size = std.base64.standard_no_pad.Decoder.calcSizeForSlice(encoded_secret) catch return -2;
    if (size>buffer.len or size-16 > output_buffer.len) return -5;
    std.base64.standard_no_pad.Decoder.decode(&buffer, encoded_secret) catch return -3;
    const decodedb = buffer[0..size];

    std.crypto.aead.chacha_poly.XChaCha20Poly1305.decrypt(output_buffer[0..size-16], decodedb[0..size-16], decodedb[size-16..][0..16].*, "", nonce, password[0..32].*) catch return -4;
    return @as(i16,@intCast(size))-16;
}

/// Generates a HOTP key given
/// key is the plain bytes of the TOTP secret
/// counter is the counter parameter for HOTP (seconds since epoch/30) for TOTP
/// length refers to the outputted key length
/// output_buffer contains the zero terminated code in ASCII format (must have a size of at least 20 bytes)
pub fn hotp(key: []const u8, counter: u64, length: u8, output_buffer: []u8) void {
    std.crypto.auth.hmac.HmacSha1.create(output_buffer[0..20], &@as([8]u8,@bitCast(std.mem.nativeToBig(u64,counter))), key);
    const offset = output_buffer[19] & 0xf;
    var truncated: [4]u8 = [4]u8{output_buffer[offset],output_buffer[offset+1],output_buffer[offset+2],output_buffer[offset+3]};
    truncated[0] &= 0x7f;
        
    var value = std.mem.nativeToBig(u32,@as(u32,@bitCast(truncated))) % (std.math.pow(u32,10,@intCast(length)));
    for(1..length+1) |i| {
        const modulo = value % 10;
        value = value/10;
        output_buffer[length-i] = @as(u8,@intCast(modulo))+'0';
    }
    output_buffer[length] = 0;
}



test "Check decode" {
    var buffer: [128]u8 = undefined;
    const payload = [_]u8{143, 54, 214, 156, 114, 22};
    var output_buffer: [128]u8 = undefined;
    const testpassword = "Test123";
    var output: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(testpassword, &output, .{});
    std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..payload.len], output_buffer[payload.len..][0..16], payload[0..], "", nonce, output);
    const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..payload.len+16]);
    const dupe = try std.testing.allocator.dupeZ(u8, slice);
    defer std.testing.allocator.free(dupe);


    try std.testing.expect(decode_2fa_key(dupe, output[0..], output_buffer[0..])>0);
    try std.testing.expectEqualStrings(output_buffer[0..payload.len], &payload);
}


test "Check hotp output" {
    var output_buffer: [128]u8 = undefined;
    const hello_world:[:0]const u8 = "Hello, world!";
    hotp(hello_world, 42, 6, output_buffer[0..]); 

    try std.testing.expectEqualStrings(output_buffer[0..6], "439256");
}

test "Check output size of hotp" {
    var output_buffer: [128]u8 = undefined;
    const hello_world:[:0]const u8 = "Hello, world!";
    for(4..10) |length| {
        hotp(hello_world, 42, @intCast(length), output_buffer[0..]); 

        try std.testing.expectEqual(std.mem.span(@as([*c]u8, @ptrCast(output_buffer[0..]))).len, length);
    }
}

test "Check full pipeline" {
    var buffer: [128]u8 = undefined;
    const hello_world:[:0]const u8 = "Hello, world!";
    var output_buffer: [128]u8 = undefined;
    const testpassword = "Test123";
    var output: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(testpassword, &output, .{});
    std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..hello_world.len], output_buffer[hello_world.len..][0..16], hello_world, "", nonce, output);
    const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..hello_world.len+16]);
    const dupe = try std.testing.allocator.dupeZ(u8, slice);
    defer std.testing.allocator.free(dupe);

    try std.testing.expect(decode_secret_and_generate_2fa(output[0..], dupe, 42*30, 6, output_buffer[0..]));

    try std.testing.expectEqualStrings(output_buffer[0..6], "439256");

    try std.testing.expect(decode_secret_and_generate_2fa(output[0..], dupe, 43*30, 6, output_buffer[0..]));
    try std.testing.expectEqualStrings(output_buffer[0..6], "208805");
}


test "Check fail on wrong password" {
    var buffer: [128]u8 = undefined;
    const hello_world:[:0]const u8 = "Hello, world!";
    var output_buffer: [128]u8 = undefined;
    const testpassword = "Test123";
    var output: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(testpassword, &output, .{});
    std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..hello_world.len], output_buffer[hello_world.len..][0..16], hello_world, "", nonce, output);
    const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..hello_world.len+16]);
    const dupe = try std.testing.allocator.dupeZ(u8, slice);
    defer std.testing.allocator.free(dupe);

    output[10] = ~output[10];

    try std.testing.expect(!decode_secret_and_generate_2fa(output[0..], dupe, 42*30, 6, output_buffer[0..]));
}