@pgpm/totp 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Makefile +1 -1
- package/package.json +5 -5
- package/sql/pgpm-totp--0.15.5.sql +217 -0
package/Makefile
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpm/totp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Time-based One-Time Password (TOTP) authentication",
|
|
5
5
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"test:watch": "jest --watch"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@pgpm/base32": "0.
|
|
25
|
-
"@pgpm/verify": "0.
|
|
24
|
+
"@pgpm/base32": "0.22.0",
|
|
25
|
+
"@pgpm/verify": "0.22.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"pgpm": "^4.2
|
|
28
|
+
"pgpm": "^4.23.2"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"bugs": {
|
|
36
36
|
"url": "https://github.com/constructive-io/pgpm-modules/issues"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "96ae0195a8b3a3dd056808c344f0e3c31a199e5e"
|
|
39
39
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
\echo Use "CREATE EXTENSION pgpm-totp" to load this file. \quit
|
|
2
|
+
CREATE SCHEMA totp;
|
|
3
|
+
|
|
4
|
+
CREATE FUNCTION totp.urlencode(
|
|
5
|
+
in_str text
|
|
6
|
+
) RETURNS text AS $EOFCODE$
|
|
7
|
+
DECLARE
|
|
8
|
+
_i int4;
|
|
9
|
+
_temp varchar;
|
|
10
|
+
_ascii int4;
|
|
11
|
+
_result text := '';
|
|
12
|
+
BEGIN
|
|
13
|
+
FOR _i IN 1..length(in_str)
|
|
14
|
+
LOOP
|
|
15
|
+
_temp := substr(in_str, _i, 1);
|
|
16
|
+
IF _temp ~ '[0-9a-zA-Z:/@._?#-]+' THEN
|
|
17
|
+
_result := _result || _temp;
|
|
18
|
+
ELSE
|
|
19
|
+
_ascii := ascii(_temp);
|
|
20
|
+
IF _ascii > x'07ff'::int4 THEN
|
|
21
|
+
RAISE exception 'won''t deal with 3 (or more) byte sequences.';
|
|
22
|
+
END IF;
|
|
23
|
+
IF _ascii <= x'07f'::int4 THEN
|
|
24
|
+
_temp := '%' || to_hex(_ascii);
|
|
25
|
+
ELSE
|
|
26
|
+
_temp := '%' || to_hex((_ascii & x'03f'::int4) + x'80'::int4);
|
|
27
|
+
_ascii := _ascii >> 6;
|
|
28
|
+
_temp := '%' || to_hex((_ascii & x'01f'::int4) + x'c0'::int4) || _temp;
|
|
29
|
+
END IF;
|
|
30
|
+
_result := _result || upper(_temp);
|
|
31
|
+
END IF;
|
|
32
|
+
END LOOP;
|
|
33
|
+
RETURN _result;
|
|
34
|
+
END;
|
|
35
|
+
$EOFCODE$ LANGUAGE plpgsql STRICT IMMUTABLE;
|
|
36
|
+
|
|
37
|
+
CREATE FUNCTION totp.pad_secret(
|
|
38
|
+
input bytea,
|
|
39
|
+
len int
|
|
40
|
+
) RETURNS bytea AS $EOFCODE$
|
|
41
|
+
DECLARE
|
|
42
|
+
output bytea;
|
|
43
|
+
orig_length int = octet_length(input);
|
|
44
|
+
BEGIN
|
|
45
|
+
IF (orig_length = len) THEN
|
|
46
|
+
RETURN input;
|
|
47
|
+
END IF;
|
|
48
|
+
|
|
49
|
+
-- create blank bytea size of new length
|
|
50
|
+
output = lpad('', len, 'x')::bytea;
|
|
51
|
+
|
|
52
|
+
FOR i IN 0 .. len-1 LOOP
|
|
53
|
+
output = set_byte(output, i, get_byte(input, i % orig_length));
|
|
54
|
+
END LOOP;
|
|
55
|
+
|
|
56
|
+
RETURN output;
|
|
57
|
+
END;
|
|
58
|
+
$EOFCODE$ LANGUAGE plpgsql IMMUTABLE;
|
|
59
|
+
|
|
60
|
+
CREATE FUNCTION totp.base32_to_hex(
|
|
61
|
+
input text
|
|
62
|
+
) RETURNS text AS $EOFCODE$
|
|
63
|
+
DECLARE
|
|
64
|
+
output text[];
|
|
65
|
+
decoded text = base32.decode(input);
|
|
66
|
+
len int = character_length(decoded);
|
|
67
|
+
hx text;
|
|
68
|
+
BEGIN
|
|
69
|
+
|
|
70
|
+
FOR i IN 1 .. len LOOP
|
|
71
|
+
hx = to_hex(ascii(substring(decoded from i for 1)))::text;
|
|
72
|
+
IF (character_length(hx) = 1) THEN
|
|
73
|
+
-- if it is odd number of digits, pad a 0 so it can later
|
|
74
|
+
hx = '0' || hx;
|
|
75
|
+
END IF;
|
|
76
|
+
output = array_append(output, hx);
|
|
77
|
+
END LOOP;
|
|
78
|
+
|
|
79
|
+
RETURN array_to_string(output, '');
|
|
80
|
+
END;
|
|
81
|
+
$EOFCODE$ LANGUAGE plpgsql IMMUTABLE;
|
|
82
|
+
|
|
83
|
+
CREATE FUNCTION totp.hotp(
|
|
84
|
+
key bytea,
|
|
85
|
+
c int,
|
|
86
|
+
digits int DEFAULT 6,
|
|
87
|
+
hash text DEFAULT 'sha1'
|
|
88
|
+
) RETURNS text AS $EOFCODE$
|
|
89
|
+
DECLARE
|
|
90
|
+
c BYTEA := '\x' || LPAD(TO_HEX(c), 16, '0');
|
|
91
|
+
mac BYTEA := HMAC(c, key, hash);
|
|
92
|
+
trunc_offset INT := GET_BYTE(mac, length(mac) - 1) % 16;
|
|
93
|
+
result TEXT := SUBSTRING(SET_BIT(SUBSTRING(mac FROM 1 + trunc_offset FOR 4), 7, 0)::TEXT, 2)::BIT(32)::INT % (10 ^ digits)::INT;
|
|
94
|
+
BEGIN
|
|
95
|
+
RETURN LPAD(result, digits, '0');
|
|
96
|
+
END;
|
|
97
|
+
$EOFCODE$ LANGUAGE plpgsql IMMUTABLE;
|
|
98
|
+
|
|
99
|
+
CREATE FUNCTION totp.generate(
|
|
100
|
+
secret text,
|
|
101
|
+
period int DEFAULT 30,
|
|
102
|
+
digits int DEFAULT 6,
|
|
103
|
+
time_from timestamptz DEFAULT now(),
|
|
104
|
+
hash text DEFAULT 'sha1',
|
|
105
|
+
encoding text DEFAULT 'base32',
|
|
106
|
+
clock_offset int DEFAULT 0
|
|
107
|
+
) RETURNS text AS $EOFCODE$
|
|
108
|
+
DECLARE
|
|
109
|
+
c int := FLOOR(EXTRACT(EPOCH FROM time_from) / period)::int + clock_offset;
|
|
110
|
+
key bytea;
|
|
111
|
+
BEGIN
|
|
112
|
+
|
|
113
|
+
IF (encoding = 'base32') THEN
|
|
114
|
+
key = ( '\x' || totp.base32_to_hex(secret) )::bytea;
|
|
115
|
+
ELSE
|
|
116
|
+
key = secret::bytea;
|
|
117
|
+
END IF;
|
|
118
|
+
|
|
119
|
+
RETURN totp.hotp(key, c, digits, hash);
|
|
120
|
+
END;
|
|
121
|
+
$EOFCODE$ LANGUAGE plpgsql STABLE;
|
|
122
|
+
|
|
123
|
+
CREATE FUNCTION totp.timing_safe_equals(
|
|
124
|
+
a bytea,
|
|
125
|
+
b bytea
|
|
126
|
+
) RETURNS boolean AS $EOFCODE$
|
|
127
|
+
DECLARE
|
|
128
|
+
la int := length(a);
|
|
129
|
+
lb int := length(b);
|
|
130
|
+
maxlen int := GREATEST(la, lb);
|
|
131
|
+
i int;
|
|
132
|
+
diff int := la # lb;
|
|
133
|
+
ca int;
|
|
134
|
+
cb int;
|
|
135
|
+
BEGIN
|
|
136
|
+
FOR i IN 0..(maxlen - 1) LOOP
|
|
137
|
+
ca := CASE WHEN i < la THEN get_byte(a, i) ELSE 0 END;
|
|
138
|
+
cb := CASE WHEN i < lb THEN get_byte(b, i) ELSE 0 END;
|
|
139
|
+
diff := diff | (ca # cb);
|
|
140
|
+
END LOOP;
|
|
141
|
+
RETURN diff = 0;
|
|
142
|
+
END;
|
|
143
|
+
$EOFCODE$ LANGUAGE plpgsql IMMUTABLE STRICT;
|
|
144
|
+
|
|
145
|
+
CREATE FUNCTION totp.timing_safe_equals(
|
|
146
|
+
a text,
|
|
147
|
+
b text
|
|
148
|
+
) RETURNS boolean AS $EOFCODE$
|
|
149
|
+
-- Verify uses timing-safe equality to avoid leaking mismatch position via timing; do not use direct '=' here.
|
|
150
|
+
-- See HN discussion for background: https://news.ycombinator.com/item?id=26260667
|
|
151
|
+
|
|
152
|
+
SELECT totp.timing_safe_equals(convert_to(a, 'UTF8'), convert_to(b, 'UTF8'));
|
|
153
|
+
$EOFCODE$ LANGUAGE sql IMMUTABLE STRICT;
|
|
154
|
+
|
|
155
|
+
CREATE FUNCTION totp.verify(
|
|
156
|
+
secret text,
|
|
157
|
+
check_totp text,
|
|
158
|
+
period int DEFAULT 30,
|
|
159
|
+
digits int DEFAULT 6,
|
|
160
|
+
time_from timestamptz DEFAULT now(),
|
|
161
|
+
hash text DEFAULT 'sha1',
|
|
162
|
+
encoding text DEFAULT 'base32',
|
|
163
|
+
clock_offset int DEFAULT 0
|
|
164
|
+
) RETURNS boolean AS $EOFCODE$
|
|
165
|
+
SELECT totp.timing_safe_equals(
|
|
166
|
+
totp.generate(
|
|
167
|
+
secret,
|
|
168
|
+
period,
|
|
169
|
+
digits,
|
|
170
|
+
time_from,
|
|
171
|
+
hash,
|
|
172
|
+
encoding,
|
|
173
|
+
clock_offset
|
|
174
|
+
),
|
|
175
|
+
check_totp
|
|
176
|
+
);
|
|
177
|
+
$EOFCODE$ LANGUAGE sql;
|
|
178
|
+
|
|
179
|
+
CREATE FUNCTION totp.url(
|
|
180
|
+
email text,
|
|
181
|
+
totp_secret text,
|
|
182
|
+
totp_interval int,
|
|
183
|
+
totp_issuer text
|
|
184
|
+
) RETURNS text AS $EOFCODE$
|
|
185
|
+
SELECT
|
|
186
|
+
concat('otpauth://totp/', totp.urlencode (email), '?secret=', totp.urlencode (totp_secret), '&period=', totp.urlencode (totp_interval::text), '&issuer=', totp.urlencode (totp_issuer));
|
|
187
|
+
$EOFCODE$ LANGUAGE sql STRICT IMMUTABLE;
|
|
188
|
+
|
|
189
|
+
CREATE FUNCTION totp.random_base32(
|
|
190
|
+
_length int DEFAULT 20
|
|
191
|
+
) RETURNS text LANGUAGE sql AS $EOFCODE$
|
|
192
|
+
SELECT
|
|
193
|
+
string_agg(
|
|
194
|
+
('{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,2,3,4,5,6,7}'::text[])
|
|
195
|
+
[ (get_byte(b, i) % 32) + 1 ],
|
|
196
|
+
''
|
|
197
|
+
)
|
|
198
|
+
FROM (SELECT gen_random_bytes(_length) AS b) t,
|
|
199
|
+
LATERAL generate_series(0, _length - 1) g(i);
|
|
200
|
+
$EOFCODE$;
|
|
201
|
+
|
|
202
|
+
CREATE FUNCTION totp.generate_secret(
|
|
203
|
+
hash text DEFAULT 'sha1'
|
|
204
|
+
) RETURNS bytea AS $EOFCODE$
|
|
205
|
+
BEGIN
|
|
206
|
+
-- See https://tools.ietf.org/html/rfc4868#section-2.1.2
|
|
207
|
+
-- The optimal key length for HMAC is the block size of the algorithm
|
|
208
|
+
CASE
|
|
209
|
+
WHEN hash = 'sha1' THEN RETURN totp.random_base32(20); -- = 160 bits
|
|
210
|
+
WHEN hash = 'sha256' THEN RETURN totp.random_base32(32); -- = 256 bits
|
|
211
|
+
WHEN hash = 'sha512' THEN RETURN totp.random_base32(64); -- = 512 bits
|
|
212
|
+
ELSE
|
|
213
|
+
RAISE EXCEPTION 'Unsupported hash algorithm for OTP (see RFC6238/4226).';
|
|
214
|
+
RETURN NULL;
|
|
215
|
+
END CASE;
|
|
216
|
+
END;
|
|
217
|
+
$EOFCODE$ LANGUAGE plpgsql VOLATILE;
|