@pgpm/totp 0.21.2 → 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 CHANGED
@@ -1,5 +1,5 @@
1
1
  EXTENSION = pgpm-totp
2
- DATA = sql/pgpm-totp--0.15.3.sql
2
+ DATA = sql/pgpm-totp--0.15.5.sql
3
3
 
4
4
  PG_CONFIG = pg_config
5
5
  PGXS := $(shell $(PG_CONFIG) --pgxs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpm/totp",
3
- "version": "0.21.2",
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.21.2",
25
- "@pgpm/verify": "0.21.2"
24
+ "@pgpm/base32": "0.22.0",
25
+ "@pgpm/verify": "0.22.0"
26
26
  },
27
27
  "devDependencies": {
28
- "pgpm": "^4.16.6"
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": "c7d836c99c7ce519e9bb79e6343bee3741781766"
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;