aboutsummaryrefslogblamecommitdiffstats
path: root/src/root.zig
blob: e4ff7a86e4fe4e73ce522a41ee0ed1bd9e52dff3 (plain) (tree)









































































































































                                                                                                                                                                                       
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..]));
}