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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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]));
}
|