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