aboutsummaryrefslogblamecommitdiffstats
path: root/src/main.zig
blob: 4a70d265b02fe314c4f37b64a0d2cb6bd8cb63f2 (plain) (tree)


























































































































































































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