aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.zig187
-rw-r--r--src/root.zig138
2 files changed, 325 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..4a70d26
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,187 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const Io = std.Io;
+
+const totp = @import("totp");
+const hidapi = @import("hidapi");
+
+const CLIError = error {
+ MissingParameterDeviceId,
+ MissingParameterVendorId,
+ MissingKeyParameter,
+ InvalidBase32,
+ PasswordTooLong,
+ UnknowDevice
+};
+
+/// Receives the base32 encoded code decodes it and stores the output in output. Returns the size of output.
+pub fn base32decode(code: []const u8, output: []u8) !usize {
+ var position:usize = 0;
+ var value:u64 = 0;
+ var bits:usize = 0;
+ if (std.mem.findScalar(u8, code, ' ')) |_| {
+ std.log.warn("Key contains whitespace which is not valid base32, ignoring",.{});
+ }
+ for(code,0..) |b,pos| {
+ if (b==' ') {
+ continue;
+ }
+ else if (b>'Z' or b < '2' or (b>'9' and b < 'A')) {
+ std.log.err("The character at position {}: {c} is invalid base32",.{pos,b});
+ return CLIError.InvalidBase32;
+ }
+ const val = if (b >= 'A') b-'A' else (b-'2')+26;
+
+ value = (value<<5) | val;
+ bits += 5;
+ if (bits >= 8) {
+ bits -= 8;
+ output[position] = @as(u8,@intCast((value>>@intCast(bits))&0xff));
+ position+=1;
+ }
+ }
+ return position;
+}
+
+/// Prints the given prompt before asking for user input. reader is assumed to be a reader for stdin.
+/// if with_delim is set to true returns the user input in addition to the final carriage return
+pub fn read_password(prompt: []const u8, reader: *std.Io.Reader, with_delim: bool) ![]u8 {
+ std.debug.print("{s}",.{prompt});
+ const stdin = std.Io.File.stdin();
+ const original_termios = try std.posix.tcgetattr(stdin.handle);
+ var new_termios = original_termios;
+ new_termios.lflag.ECHO = false;
+ try std.posix.tcsetattr(stdin.handle, .FLUSH, new_termios);
+ defer std.posix.tcsetattr(stdin.handle, .FLUSH, original_termios) catch {};
+
+ const password = if (with_delim) try reader.takeDelimiterInclusive('\n') else (try reader.takeDelimiter('\n')).?;
+ std.debug.print("\n",.{});
+ return password;
+}
+
+pub fn main(init: std.process.Init) !void {
+ var iter = init.minimal.args.iterate();
+ _ = iter.next();
+
+ var buffer: [256]u8 = undefined;
+ if(iter.next()) |arg| {
+ const stdin = std.Io.File.stdin();
+ var reader = stdin.reader(init.io, &buffer);
+ if (std.mem.eql(u8,arg, "add")) {
+ var output_buffer: [128]u8 = undefined;
+ const key_enc = if (iter.next()) |key| key else return CLIError.MissingKeyParameter;
+ const key_len = try base32decode(key_enc, &output_buffer);
+ const key = try init.gpa.dupe(u8,output_buffer[0..key_len]);
+ defer init.gpa.free(key);
+ const password = try read_password("Password: ", &reader.interface, false);
+
+ var output: [32]u8 = undefined;
+ std.crypto.hash.sha2.Sha256.hash(password, &output, .{});
+ std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..key.len], output_buffer[key.len..][0..16], key, "", totp.nonce, output);
+ const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..key.len+16]);
+ std.debug.print("Add this struct entry to the configuration {{KEY,6,{s}}}\n",.{slice});
+ }
+ else if (std.mem.eql(u8,arg, "decode")) {
+ const key_enc = if (iter.next()) |key| key else return CLIError.MissingKeyParameter;
+ const password = try read_password("Password: ", &reader.interface, true);
+ password[password.len-1] = 0;
+ var output: [32]u8 = undefined;
+ _ = totp.decode_secret_and_generate_2fa(password.ptr, key_enc,@as(u64,@intCast(std.Io.Timestamp.now(init.io,.real).toSeconds())), 6, output[0..]);
+ std.debug.print("2FA-Key: {s}\n", .{output[0..6]});
+ }
+ else if (std.mem.eql(u8,arg, "reencode")) {
+ var output_buffer: [128]u8 = undefined;
+ const key_enc = if (iter.next()) |key| key else return CLIError.MissingKeyParameter;
+ const password = try read_password("Old Password: ", &reader.interface, true);
+ password[password.len-1] = 0;
+ totp.sha256(password.ptr, output_buffer[0..]);
+ var secret_2fa: [128]u8 = undefined;
+ const result = totp.decode_2fa_key(key_enc, output_buffer[0..], secret_2fa[0..]);
+ if (result < 0) {
+ std.debug.print("Error decoding\n",.{});
+ return;
+ }
+ const length:usize = @intCast(result);
+ const newpassword = try read_password("New Password: ", &reader.interface, false);
+ var output: [32]u8 = undefined;
+ std.crypto.hash.sha2.Sha256.hash(newpassword, &output, .{});
+ std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..length], output_buffer[length..][0..16], secret_2fa[0..length], "", totp.nonce, output);
+ const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..length+16]);
+ std.debug.print("Add this struct entry to the configuration {{KEY,6,{s}}}\n",.{slice});
+ }
+ else if(std.mem.eql(u8,arg,"nonce")) {
+ var op = try std.Io.Dir.openFileAbsolute(init.io,"/dev/urandom",.{});
+ var reader_nonce = op.reader(init.io, &buffer);
+ var bytes:[24]u8 = undefined;
+ _ = try reader_nonce.interface.readSliceShort(&bytes);
+ std.debug.print("Nonce {any}\n",.{bytes});
+ }
+ else if(std.mem.eql(u8,arg,"hid")) {
+ const vendorid = if (iter.next()) |vendorid| try std.fmt.parseInt(u16, vendorid, 16) else return CLIError.MissingParameterVendorId;
+ const deviceid = if (iter.next()) |devid| try std.fmt.parseInt(u16, devid, 16) else return CLIError.MissingParameterDeviceId;
+ _ = hidapi.hid_init();
+ defer _ = hidapi.hid_exit();
+ var enumerate = hidapi.hid_enumerate(vendorid, deviceid);
+ while(enumerate) |interface| {
+ if (interface.*.usage == 0x61 and interface.*.usage_page == 0xFF60) {
+ break;
+ }
+ enumerate = enumerate.*.next;
+ }
+ if(enumerate == null) {
+ std.debug.print("Could not find device.\n", .{});
+ return CLIError.UnknowDevice;
+ }
+ const dev: ?*hidapi.hid_device = hidapi.hid_open_path(enumerate.*.path);
+ std.debug.print("Found device at {s}\n", .{enumerate.*.path});
+ if (dev) |device_handle| {
+ defer hidapi.hid_close(device_handle);
+ var write_buffer: [33]u8 = undefined;
+ // Report ID
+ write_buffer[0] = 0;
+
+ const now:u64 = @intCast(std.Io.Timestamp.now(init.io, .real).toSeconds());
+
+ // Payload
+ inline for(0..8) |x| {
+ write_buffer[1+x] = @intCast((now>>(x*8))&0xFF);
+ }
+ const password = try read_password("Password: ", &reader.interface, false);
+ // Password length is bounded since HIDAPI report length is bounded
+ if (password.len > 23) {
+ std.debug.print("Only passwords lengths of at most 23 bytes supported.\n", .{});
+ return CLIError.PasswordTooLong;
+ }
+
+ @memcpy(write_buffer[9..9+password.len], password);
+ write_buffer[9+password.len] = 0;
+ const res2 = hidapi.hid_write(device_handle, &write_buffer, 33);
+ if(res2 == 33) {
+ std.debug.print("Set up keyboard.\n", .{});
+ }
+ }
+ else {
+ std.debug.print("Could not open device with vendor id: {x}, device id: {x}\n", .{vendorid, deviceid});
+ }
+ }
+ }
+ else {
+ std.debug.print("Usage: totp [command]\nCommunicates and sets up time base one time passwords for programmable keyboards.\nCommands:\n\tadd key\n\t\tAdds a new base32 encoded key.\n\n\tdecode key\n\t\tDecodes the key and generates one time password based on it.\n\n\tnonce\n\t\tGenerates a new nonce for the symmetric cipher.\n\n\thid vendorid deviceid\n\t\tSets up HID device. IDS are interpreted as base 16 numbers.\n",.{});
+ }
+}
+
+
+test "base32 decode" {
+ var output: [20]u8 = undefined;
+ const encoded = "AABBDDEEFFHH";
+ const size = try base32decode(encoded, &output);
+
+ const encoded_err = "AABBDDEEFFHH--";
+ try std.testing.expectError(CLIError.InvalidBase32,base32decode(encoded_err,&output));
+
+ var output_2: [20]u8 = undefined;
+ const encoded_spaces = "AABBD DEEFF HH";
+ const size2 = try base32decode(encoded_spaces, &output_2);
+ try std.testing.expectEqual(size, size2);
+ try std.testing.expect(std.mem.eql(u8, output[0..size], output_2[0..size2]));
+}
diff --git a/src/root.zig b/src/root.zig
new file mode 100644
index 0000000..e4ff7a8
--- /dev/null
+++ b/src/root.zig
@@ -0,0 +1,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..]));
+}