yubikey/init.lua

1
local _M = {};
2
 
3
local aeslua = require "aeslua";
4
local ciphermode = require "aeslua.ciphermode";
5
local crc16 = require "crc16";
6
 
7
local modhex2hex;
8
do
9
	local modhex_map = {};
10
	local modhex_chars = "cbdefghijklnrtuv";
11
	for i=1,16 do
12
		modhex_map[modhex_chars:sub(i,i)] = ("%x"):format(i-1);
13
	end
14
	function modhex2hex(modhex)
15
		if modhex then
16
			return (modhex:gsub(".", modhex_map));
17
		end
18
	end
19
end
20
_M.modhex2hex = modhex2hex;
21
 
22
local function hex2bin(hex)
23
	if hex then
24
		return (hex:gsub("..", function (c) return string.char(tonumber(c, 16)); end));
25
	end
26
end
27
_M.hex2bin = hex2bin;
28
 
29
local function bin2hex(bin)
30
	if bin then
31
		return (bin:gsub(".", function (b) return ("%02x"):format(b:byte()); end));
32
	end
33
end
34
_M.bin2hex = bin2hex;
35
 
36
local otp_parser = {};
37
local otp_parser_mt = { __index = otp_parser };
38
 
39
function _M.new_fetch_key_hex(fetchkeyhex)
40
	return function (...) return hex2bin(fetchkeyhex(...)); end
41
end
42
 
43
function _M.new_otp_parser(config)
44
	return setmetatable({
45
		config = config,
46
	}, otp_parser_mt);
47
end
48
 
49
function otp_parser:parse(otp, key)
50
	-- Password is any extra data before the real OTP
51
	local password;
52
	if #otp > 32 + self.config.prefix_length then
53
		password = otp:sub(1, #otp - (32 + self.config.prefix_length));
54
		otp = otp:sub(#password+1);
55
	end
56
	local prefix = otp:sub(1, self.config.prefix_length);
57
	local token = otp:sub(#prefix+1);
58
	if not key then
59
		key = self.config.fetch_key(prefix);
60
	else
61
		key = hex2bin(key);
62
	end
63
	if not key then return false, "no-key"; end
64
	key = {key:byte(1,#key)}
65
	local decrypted = ciphermode.decryptString(key, hex2bin(modhex2hex(token)), ciphermode.decryptCBC);
66
	if not decrypted then return false, "decrypt-failed"; end
67
	-- Extract private UID
68
	local uid = decrypted:sub(1,6);
69
	-- Build insertion counter (2 bytes)
70
	local use1, use2 = decrypted:sub(7,8):byte(1,2);
71
	local use_ctr = use1 + math.pow(2, 8)*use2;
72
	-- Build timestamp (3 bytes)
73
	local time1, time2, time3 = decrypted:sub(9,11):byte(1,3);
74
	local timestamp = time1 + math.pow(2,8)*time2 + math.pow(2, 16)*time3;
75
	-- Extract session counter (1 byte)
76
	local session_ctr = decrypted:sub(12, 12):byte();
77
	
78
	if crc16.hash(decrypted) ~= 0xf0b8 then
79
		return false, "invalid-checksum";
80
	end
81
	
82
	-- Return parsed fields
83
	return true, {
84
		password = password;
85
		public_id = prefix;
86
		token = token;
87
		private_id = bin2hex(uid);
88
		session_counter = session_ctr;
89
		use_counter = use_ctr;
90
		timestamp = timestamp;
91
	};
92
end
93
 
94
function _M.new_authenticator(config)
95
	local parser = _M.new_otp_parser(config);
96
	local function authenticate(self, otp, key, device_info, userdata)
97
		-- Parse OTP, get device data (from config callback)
98
		local ok, ret = parser:parse(otp, key);
99
		if not ok then return ok, ret; end
100
		
101
		if not device_info then
102
			device_info = config.fetch_device_info(ret);
103
		end
104
		
105
		if ret.use_counter < (device_info.use_counter or 0)
106
		or ((ret.use_counter == (device_info.use_counter or 0))
107
		  and (ret.session_counter <= (device_info.session_counter or 0))) then
108
			return false, "otp-already-used";
109
		end
110
		
111
		local authed, err = config.check_credentials(ret, device_info, userdata);
112
		if not authed then return authed, err; end
113
		
114
		device_info.use_counter = ret.use_counter;
115
		device_info.session_counter = ret.session_counter;
116
		
117
		config.store_device_info(device_info, userdata);
118
		
119
		return true, ret;
120
	end
121
	return { authenticate = authenticate };
122
end
123
 
124
return _M;