parsav  Check-in [0d10a378e9]

Overview
Comment:add auth docs and rsa auth
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 0d10a378e90159932431518d4fa74296f1090e4d3025e45f44e9ddbe012d99e7
User & Date: lexi on 2021-01-11 01:53:23
Other Links: manifest | tags
Context
2021-01-11
02:17
enable revoking credentials check-in: 41cbbca855 user: lexi tags: trunk
01:53
add auth docs and rsa auth check-in: 0d10a378e9 user: lexi tags: trunk
2021-01-10
16:44
add avatar panel check-in: 8398fcda5a user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [b388ba7244] to [7d329a757a].

116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131












132
133
134
135
136
137
138
...
292
293
294
295
296
297
298











299
300
301
302
303
304
305
....
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311











































1312
1313
1314
1315
1316
1317
1318
....
1615
1616
1617
1618
1619
1620
1621















1622
1623
1624
1625
1626
1627
1628
				$11::integer,$4::bigint
			) returning id
		]];
	};

	actor_auth_pw = {
		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
			select a.aid, a.uid, a.name from parsav_auth as a
				left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
					a.uid = 0 and a.name = $1::text
				)) and
				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
				(a.netmask is null or a.netmask >> $4::inet)
			order by blacklist desc limit 1
		]];












	};

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key, epithet,
			       knownsince::bigint,
................................................................................

	auth_create_pw = {
		params = {uint64, binblob, int64, pstring}, sql = [[
			insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values (
				$1::bigint,
				(select handle from parsav_actors where id = $1::bigint),
				'pw-sha256', $2::bytea,











				$3::bigint, $4::text
			) on conflict (name,kind,cred) do update set comment = $4::text returning aid
		]]
	};

	auth_privs_clear = {
		params = {uint64}, cmd = true, sql = [[
................................................................................
		(cs.trust << r:bool(0,3))
		return cs, true
	end];
	 
	actor_auth_pw = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: lib.mem.ptr(int8),
			cred: lib.mem.ptr(int8)
		): {uint64, uint64, pstring}

		[ checksha(`src, 256, ip, username, cred) ] -- most common
		[ checksha(`src, 512, ip, username, cred) ] -- most secure
		[ checksha(`src, 384, ip, username, cred) ] -- weird
		[ checksha(`src, 224, ip, username, cred) ] -- weirdest

		-- TODO: check pbkdf2-hmac
		-- TODO: check OTP
		return 0, 0, pstring.null()
	end];












































	actor_stats = [terra(src: &lib.store.source, uid: uint64)
		var r = queries.actor_stats.exec(src, uid)
		if r.sz == 0 then lib.bail('error fetching actor stats!') end
		var s: lib.store.actor_stats
		s.posts = r:int(uint64, 0, 0)
		s.follows = r:int(uint64, 0, 1)
................................................................................
		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
		var r = queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment)
		if r.sz == 0 then return 0 end
		var aid = r:int(uint64,0,0)
		r:free()
		return aid
	end];
















	auth_privs_set = [terra(
		src: &lib.store.source,
		aid: uint64,
		set: lib.store.privset
	): {}
		var map = array([lib.store.privmap])







|








>
>
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>
>
>
>
>
>
>
>







 







|
|











>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
...
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
....
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
....
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
				$11::integer,$4::bigint
			) returning id
		]];
	};

	actor_auth_pw = {
		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
			select a.aid, a.uid, a.name, a.blacklist from parsav_auth as a
				left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
					a.uid = 0 and a.name = $1::text
				)) and
				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
				(a.netmask is null or a.netmask >> $4::inet)
			order by blacklist desc limit 1
		]];
	};
	actor_auth_challenge = {
		params = {pstring,pstring,lib.store.inet}, sql = [[
			select a.aid, a.uid, a.name, a.blacklist, a.cred 
			from parsav_auth as a
				left join parsav_actors as u on u.id = a.uid
			where a.kind = 'challenge-' || $1::text and
				(a.netmask is null or a.netmask >> $3::inet) and
				(a.uid     is null or u.handle  =  $2::text or
					(a.uid = 0 and a.name = $2::text))
			order by blacklist desc
		]];
	};

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key, epithet,
			       knownsince::bigint,
................................................................................

	auth_create_pw = {
		params = {uint64, binblob, int64, pstring}, sql = [[
			insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values (
				$1::bigint,
				(select handle from parsav_actors where id = $1::bigint),
				'pw-sha256', $2::bytea,
				$3::bigint, $4::text
			) on conflict (name,kind,cred) do update set comment = $4::text returning aid
		]]
	};

	auth_create_rsa = {
		params = {uint64, binblob, int64, pstring}, sql = [[
			insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values (
				$1::bigint,
				(select handle from parsav_actors where id = $1::bigint),
				'challenge-rsa', $2::bytea,
				$3::bigint, $4::text
			) on conflict (name,kind,cred) do update set comment = $4::text returning aid
		]]
	};

	auth_privs_clear = {
		params = {uint64}, cmd = true, sql = [[
................................................................................
		(cs.trust << r:bool(0,3))
		return cs, true
	end];
	 
	actor_auth_pw = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: pstring,
			cred: pstring
		): {uint64, uint64, pstring}

		[ checksha(`src, 256, ip, username, cred) ] -- most common
		[ checksha(`src, 512, ip, username, cred) ] -- most secure
		[ checksha(`src, 384, ip, username, cred) ] -- weird
		[ checksha(`src, 224, ip, username, cred) ] -- weirdest

		-- TODO: check pbkdf2-hmac
		-- TODO: check OTP
		return 0, 0, pstring.null()
	end];

	actor_auth_challenge = [terra(
		src: &lib.store.source,
		ip: lib.store.inet,
		username: pstring,
		sig: binblob,
		token: pstring
	): {uint64, uint64, pstring}
		-- we need to iterate through all the challenge types. right now that's just RSA
		lib.dbg('checking against token ', {token.ptr,token.ct})
		var rsakeys = queries.actor_auth_challenge.exec(src, 'rsa', username, ip)
		var toprops = [terra(res: &pqr, i: intptr)
			return {
				aid = res:int(uint64, i, 0);
				uid = res:int(uint64, i, 1);
				name = res:_string(i, 2);
				blacklist = res:bool(i, 3);
				pubkey = res:bin(i, 4);
			}
		end]
		if rsakeys.sz > 0 then defer rsakeys:free()
			for i=0, rsakeys.sz do var props = toprops(&rsakeys, i)
				lib.dbg('loading next RSA pubkey')
				var pub = lib.crypt.loadpub(props.pubkey.ptr, props.pubkey.ct)
				if pub.ok then defer pub.val:free()
					lib.dbg('checking pubkey against response')
					var vfy, secl = lib.crypt.verify(&pub.val, token.ptr, token.ct, sig.ptr, sig.ct)
					if vfy then
						lib.dbg('signature verified')
						if props.blacklist then lib.dbg('key blacklisted!') goto fail end
						var dupname = lib.str.dup(props.name.ptr)
						return props.aid, props.uid, pstring {dupname, props.name.ct}
					end
				else lib.warn('invalid pubkey in authentication table for user ',{props.name.ptr, props.name.ct}) end
			end
		end

		-- and so on

		lib.dbg('no challenges were successful')
		::fail::return 0, 0, pstring.null()
	end];


	actor_stats = [terra(src: &lib.store.source, uid: uint64)
		var r = queries.actor_stats.exec(src, uid)
		if r.sz == 0 then lib.bail('error fetching actor stats!') end
		var s: lib.store.actor_stats
		s.posts = r:int(uint64, 0, 0)
		s.follows = r:int(uint64, 0, 1)
................................................................................
		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
		var r = queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment)
		if r.sz == 0 then return 0 end
		var aid = r:int(uint64,0,0)
		r:free()
		return aid
	end];

	auth_attach_rsa = [terra(
		src: &lib.store.source,
		uid: uint64,
		reset: bool,
		pub: binblob,
		comment: pstring
	): uint64
		if reset then queries.auth_purge_type.exec(src, nil, uid, 'challenge-%') end
		var r = queries.auth_create_rsa.exec(src, uid, pub, lib.osclock.time(nil), comment)
		if r.sz == 0 then return 0 end
		var aid = r:int(uint64,0,0)
		r:free()
		return aid
	end];

	auth_privs_set = [terra(
		src: &lib.store.source,
		aid: uint64,
		set: lib.store.privset
	): {}
		var map = array([lib.store.privmap])

Modified crypt.t from [f5b057e4fa] to [034fdb64c7].

10
11
12
13
14
15
16

17
18
19
20
21
22
23
..
78
79
80
81
82
83
84

85

86
87
88
89
90




91
92
93
94
95
96
97
...
104
105
106
107
108
109
110
111
112
113
114
115
116
117



















118
119
120
121
122
123
124
...
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
...
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
...
186
187
188
189
190
191
192






193
194
195
196
197
198
199
...
200
201
202
203
204
205
206
207



















208
	end;
	toobig = -lib.pk.MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE;
}
const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to
const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one

local ctx = lib.pk.mbedtls_pk_context


local struct hashalg { id: uint8 bytes: intptr }
local m = {
	pemfile = uint8[const.maxpemsz];
	const = const;
	algsz = {
		sha1 =   160/8;
................................................................................
	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
	else
		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
	end
end


terra m.der(pub: bool, key: &ctx, buf: &uint8): intptr

	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz)
	else
		return lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz)
	end




end

m.destroy = lib.dispatch {
	[ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end;

	[false] = function(ptr) return `ptr:free() end;
}
................................................................................
	lib.pk.mbedtls_pk_setup(&pk, lib.pk.mbedtls_pk_info_from_type(lib.pk.MBEDTLS_PK_RSA))
	var rsa = [&lib.rsa.mbedtls_rsa_context](pk.pk_ctx)
	lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537)

	return pk
end

terra m.loadpriv(buf: &uint8, len: intptr): ctx
	lib.dbg('parsing saved keypair')

	var pk: ctx
	lib.pk.mbedtls_pk_init(&pk)
	lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0)
	return pk



















end

terra m.sign(pk: &ctx, txt: rawstring, len: intptr)
	lib.dbg('signing message')
	var osz: intptr = 0
	var sig = lib.mem.heapa(int8, 2048)
	var hash: uint8[32]
................................................................................
	if ret ~= 0 then lib.bail('could not sign message hash')
	else sig:resize(osz) end

	return sig
end

terra m.verify(pk: &ctx, txt: rawstring, len: intptr,
                        sig: rawstring, siglen: intptr): {bool, uint8}
	lib.dbg('verifying signature')
	var osz: intptr = 0
	var hash: uint8[64]

	-- there does not appear to be any way to extract the hash algorithm
	-- from the message, so we just have to try likely algorithms until
	-- we find one that fits or give up. a security level is attached
................................................................................
		{lib.md.MBEDTLS_MD_SHA256, 'sha256', 2},
		{lib.md.MBEDTLS_MD_SHA512, 'sha512', 3},
		{lib.md.MBEDTLS_MD_SHA1,   'sha1',   1},
		-- uncommon hashes
		{lib.md.MBEDTLS_MD_SHA384, 'sha384', 2},
		{lib.md.MBEDTLS_MD_SHA224, 'sha224', 2},
		-- bad hashes
		{lib.md.MBEDTLS_MD_MD5,   'md5', 0},
		{lib.md.MBEDTLS_MD_MD4,   'md4', 0},
		{lib.md.MBEDTLS_MD_MD2,   'md2', 0}
	)
	
	for i = 0, [algs.type.N] do
		var hk, aname, secl = algs[i]

		lib.dbg('(1/2) trying hash algorithm ',aname)
		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(hk), [&uint8](txt), len, hash) ~= 0 then
................................................................................
end

terra m.hmaca(alg: hashalg, key: lib.mem.ptr(uint8), txt: lib.mem.ptr(int8))
	var buf = lib.mem.heapa(uint8, alg.bytes)
	m.hmac(alg, key, txt, buf.ptr)
	return buf
end







terra m.hotp(key: &(uint8[10]), counter: uint64)
	var hmac: uint8[20]
	var ctr = [lib.mem.ptr(int8)]{ptr = [&int8](&counter), ct = 8}
	m.hmac(m.alg.sha1,
		[lib.mem.ptr(uint8)]{ptr = [&uint8](key), ct = 10},
		ctr, hmac)
................................................................................
	
	var ofs = hmac[19] and 0x0F
	var p: uint8[4]
	for i=0,4 do p[i] = hmac[ofs + i] end

	return (@[&uint32](&p)) and 0x7FFFFFFF -- one hopes it's that easy
end




















return m







>







 







>
|
>

|

|

>
>
>
>







 







|
|



|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|







 







|
|
|







 







>
>
>
>
>
>







 








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
..
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
...
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
...
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
...
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
...
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
...
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
	end;
	toobig = -lib.pk.MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE;
}
const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to
const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one

local ctx = lib.pk.mbedtls_pk_context
terra ctx:free() lib.pk.mbedtls_pk_free(self) end

local struct hashalg { id: uint8 bytes: intptr }
local m = {
	pemfile = uint8[const.maxpemsz];
	const = const;
	algsz = {
		sha1 =   160/8;
................................................................................
	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
	else
		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
	end
end

local binblob = lib.mem.ptr(uint8)
terra m.der(pub: bool, key: &ctx, buf: &uint8): binblob
	var ofs: intptr
	if pub then
		ofs = lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz)
	else
		ofs = lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz)
	end
	return binblob {
		ptr = buf + (const.maxdersz - ofs);
		ct = ofs;
	}
end

m.destroy = lib.dispatch {
	[ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end;

	[false] = function(ptr) return `ptr:free() end;
}
................................................................................
	lib.pk.mbedtls_pk_setup(&pk, lib.pk.mbedtls_pk_info_from_type(lib.pk.MBEDTLS_PK_RSA))
	var rsa = [&lib.rsa.mbedtls_rsa_context](pk.pk_ctx)
	lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537)

	return pk
end

terra m.loadpriv(buf: &uint8, len: intptr): lib.stat(ctx)
	lib.dbg('parsing saved private key')

	var pk: ctx
	lib.pk.mbedtls_pk_init(&pk)
	var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0)
	if rt == 0 then
		return [lib.stat(ctx)] { ok = true, val = pk }
	else
		lib.pk.mbedtls_pk_free(&pk)
		return [lib.stat(ctx)] { ok = false }
	end
end

terra m.loadpub(buf: &uint8, len: intptr): lib.stat(ctx)
	lib.dbg('parsing saved key')

	var pk: ctx
	lib.pk.mbedtls_pk_init(&pk)
	var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf, len)
	if rt == 0 then
		return [lib.stat(ctx)] { ok = true, val = pk }
	else
		lib.pk.mbedtls_pk_free(&pk)
		return [lib.stat(ctx)] { ok = false, error = rt }
	end
end

terra m.sign(pk: &ctx, txt: rawstring, len: intptr)
	lib.dbg('signing message')
	var osz: intptr = 0
	var sig = lib.mem.heapa(int8, 2048)
	var hash: uint8[32]
................................................................................
	if ret ~= 0 then lib.bail('could not sign message hash')
	else sig:resize(osz) end

	return sig
end

terra m.verify(pk: &ctx, txt: rawstring, len: intptr,
                        sig: &uint8, siglen: intptr): {bool, uint8}
	lib.dbg('verifying signature')
	var osz: intptr = 0
	var hash: uint8[64]

	-- there does not appear to be any way to extract the hash algorithm
	-- from the message, so we just have to try likely algorithms until
	-- we find one that fits or give up. a security level is attached
................................................................................
		{lib.md.MBEDTLS_MD_SHA256, 'sha256', 2},
		{lib.md.MBEDTLS_MD_SHA512, 'sha512', 3},
		{lib.md.MBEDTLS_MD_SHA1,   'sha1',   1},
		-- uncommon hashes
		{lib.md.MBEDTLS_MD_SHA384, 'sha384', 2},
		{lib.md.MBEDTLS_MD_SHA224, 'sha224', 2},
		-- bad hashes
		{lib.md.MBEDTLS_MD_MD5,   'md5', 0}
		--{lib.md.MBEDTLS_MD_MD4,   'md4', 0},
		--{lib.md.MBEDTLS_MD_MD2,   'md2', 0}
	)
	
	for i = 0, [algs.type.N] do
		var hk, aname, secl = algs[i]

		lib.dbg('(1/2) trying hash algorithm ',aname)
		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(hk), [&uint8](txt), len, hash) ~= 0 then
................................................................................
end

terra m.hmaca(alg: hashalg, key: lib.mem.ptr(uint8), txt: lib.mem.ptr(int8))
	var buf = lib.mem.heapa(uint8, alg.bytes)
	m.hmac(alg, key, txt, buf.ptr)
	return buf
end

terra m.hmacp(p: &lib.mem.pool, alg: hashalg, key: lib.mem.ptr(uint8), txt: lib.mem.ptr(int8))
	var buf = p:alloc(uint8, alg.bytes)
	m.hmac(alg, key, txt, buf.ptr)
	return buf
end

terra m.hotp(key: &(uint8[10]), counter: uint64)
	var hmac: uint8[20]
	var ctr = [lib.mem.ptr(int8)]{ptr = [&int8](&counter), ct = 8}
	m.hmac(m.alg.sha1,
		[lib.mem.ptr(uint8)]{ptr = [&uint8](key), ct = 10},
		ctr, hmac)
................................................................................
	
	var ofs = hmac[19] and 0x0F
	var p: uint8[4]
	for i=0,4 do p[i] = hmac[ofs + i] end

	return (@[&uint32](&p)) and 0x7FFFFFFF -- one hopes it's that easy
end

local splitwords = macro(function(str)
	local words = {}
	for w in str:asvalue():gmatch('(%g+)') do words[#words + 1] = w end
	return `arrayof(lib.str.t, [words])
end)

terra m.cryptogram(a: &lib.str.acc, len: intptr)
	var words = splitwords [[
		alpha beta gamma delta epsilon psi eta nu omicron omega
		red crimson green verdant golden silver blue cyan navy
		carnelian opal sapphire amethyst ruby jade emerald
		chalice peacock cabernet windmill saxony tunnel waterspout
	]]
	for i = 0, len do
		a:ppush(words[m.random(intptr,0,[words.type.N])]):lpush '-'
	end
	a:ipush(m.random(uint32,0,99999))
end

return m

Added doc/auth.md version [ac7a8d6f57].























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
# credentials & authentication

parsav features a highly flexibly authentication system, and provides alternatives to simple per-user passwords to help users keep their accounts secure. you can create as many credentials for you account as you wish, and tie them to specific IP addresses or regions. you can even restrict the capabilities of a given credential -- for instance, you could have one password that allows full access and one that only allows posting new tweets when logged in with it.

## mechanisms

you're not limited to passwords, however. parsav intends to support a very wide range of authentication mechanisms; this page lists all the mechanisms that have been implemented so far.

### password auth

of course, parsav lets you access your account with passwords/passphrases like most other software. when you create a new password, you'll be asked to enter it twice to confirm that you haven't mistyped it; you'll then be able to use it by typing it into the login screen.

passwords have many disadvantages, however. they're easy to steal, easy to forget, they get more and more difficult to use the more secure they are, and people have an awful habit of *writing them down.* if you don't have any problems memorizing long, secure passwords, this may not be a problem for you. but even then, there's no way around the fundamental problem that passwords are *static* -- you only have to slip up once (say, by typing it into a username field by mistake just as the black helicopters are passing overhead) and then the password is compromised forever.

of course, if you have a static IP address, you can get around some of the insecurity by setting a netmask on the password -- it won't do mongolian bitcoin scammers much good if only IPs from mecklenburg-vorpommern are allowed to use it. netmasks are however best used on VPNs, LANs, and similar arrangements where you have an absolute guarantee of a static IP address. for other circumstances, *challenge auth* may be a worthwhile means of improving your security.

### challenge auth

parsav also supports *challenge auth,* which is a form of authentication where are presented with a *challenge token* at login and have to provide with a response digest based on the token to authenticate yourself. this mechanism has the very useful property that the same digest can only be used for a very short period of time, after which they are permanently deactivated, giving you a bit of protection even if your HTTP session is exposed to a man-in-the-middle. due to the way they're implemented, they're effectively immune to shouldersurfing. challenge auth is generally based on cryptographic keys.

right now, the only form of challenge authentication supported is RSA asymmetric keypairs, though other methods based on elliptic curve cryptography and shared secrets are planned. an RSA keypair is a pair of very long numbers -- called the *public key* and the *private key* -- with special mathematical properties: anyone who holds the public key can encrypt data such that only the person with the private key can read it, and whoever holds the public key can place a digital signature on a piece of data such that anyone with the public key can confirm the data was endorsed by the holder of the private key. (private keys can of course be encrypted with a password; the advantage this has over normal passwords is that the password never leaves your computer's memory.) so when you log in with RSA challenge auth, you'll be given a short string to sign with your private key. all you have to do is paste the signature into the "digest" box and you'll be logged in.

keypairs are bit more complex to use than passwords, however. you have to use a special tool to create them. on linux and other unix-like systems, you can do this with the `openssl` command:

	$ openssl genrsa 2048 -out private.pem
	  # creates a reasonably secure 2048-bit private key

	$ openssl genrsa 4096 -out private.pem
	  # creates an *extremely secure 4096-bit key

	$ openssl genrsa 2048 -aes256 -out private.pem
	  # pass -aes256 to encrypt your key

once you've created your private key with a command like one of the above, you'll need to separate out a public key. if you used the `-aes256` flag, you'll be prompted for your password. (keep in mind, this password *cannot* be recovered if it is forgotten!)

	$ openssl rsa -in private.pem -pubout -out public.pem

`public.pem` is the file you'll want to copy and paste into the text box when you add this keypair as a credential to your parsav account. do *not* ever upload `private.pem` anywhere! if you ever do so by accident, delete the keypair credential from every account that uses it immediately, as you have irreversibly compromised their security.

finally, you'll need to use this key to actually sign things:

	$ echo -n "this is the string that will be signed" | openssl dgst -sha256 -sign private.pem | openssl base64

this command is somewhat complex, so you may want to write a short script to save yourself some time. on computers with the X windows system, you can use the following convenient command to encrypt whatever is currently in the clipboard:

	$ xsel -bo | openssl dgst -sha256 -sign private.pem | openssl base64 | xsel -bi

if you later want to change the password on your private key, you can use this command to do so:

    $ openssl rsa -in private.pem -aes256 -out private.pem
	  # omit the -aes256 to remove the encryption

## managing credentials

you can use the "security" panel in the configuration menu to manage your credentials. this panel has a wide range of options. firstly, if you suspect someone may have unwanted access to your account, you can press the "invalidate other sessions" button to instantly log out every computer but your own. of course, this will only briefly inconvenience evildoers if they have your password -- it's mainly useful for instances where you forgot to log out of a public computer, or one that belongs to someone else.

you can manage existing credentials with the "revoke" button, which wipes out a selected credential so it can no longer be used to log in (and logs out every device logged in under it!), or `reset`, which lets you change the credentials without affecting their privilege sets.

finally, you can create new credentials by picking the desired properties (what privileges and netmask they are restricted to, if any) and pressing the relevant button.

Modified doc/load.lua from [783a358256] to [e78e99763f].

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
local path = ...
local sources = {
-- user section
	acl = {title = 'access control lists'};

-- admin section
	--overview = {title = 'server overview', priv = 'config'};
	invocation = {title = 'daemon invocation', priv = 'config'};
	usr = {title = 'user accounting', priv = {'elevate','demote','purge','herald'}};
	--srvcfg = {title = 'server configuration policies', priv = 'config'};
	--discipline = {title = 'disciplinary measures', priv = 'discipline'};
	--backends = {title = 'storage backends', priv = 'config'};
		--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
}

local util = dofile 'common.lua'
local ingest = function(filename)
	return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','')

end

local doc = {}
for n,meta in pairs(sources) do doc[n] = {
	name = n;
	text = ingest(n .. '.md');
	meta = meta;
} end
return doc




>












|
>









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
local path = ...
local sources = {
-- user section
	acl = {title = 'access control lists'};
	auth = {title = 'credentials &amp; authentication', priv = 'account'}; 
-- admin section
	--overview = {title = 'server overview', priv = 'config'};
	invocation = {title = 'daemon invocation', priv = 'config'};
	usr = {title = 'user accounting', priv = {'elevate','demote','purge','herald'}};
	--srvcfg = {title = 'server configuration policies', priv = 'config'};
	--discipline = {title = 'disciplinary measures', priv = 'discipline'};
	--backends = {title = 'storage backends', priv = 'config'};
		--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
}

local util = dofile 'common.lua'
local ingest = function(filename)
	return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) })
		--:gsub('\n','')
end

local doc = {}
for n,meta in pairs(sources) do doc[n] = {
	name = n;
	text = ingest(n .. '.md');
	meta = meta;
} end
return doc

Modified mgtool.t from [600c3c1067] to [4fc07a70c1].

291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
				dlg:tx_enter()
				if dlg:dbsetup() then
					srv:conprep(lib.store.prepmode.conf)

					do var newkp = lib.crypt.genkp()
					 -- generate server privkey
						var kbuf: uint8[lib.crypt.const.maxdersz]
						var privsz = lib.crypt.der(false,&newkp, kbuf)
						dlg:server_setup_self(dbmode.arglist(1), [lib.mem.ptr(uint8)] {
							ptr = &kbuf[0], ct = privsz
						})
					end

					dlg:conf_set('instance-name', dbmode.arglist(1))
					dlg:conf_set('domain', dbmode.arglist(1))
					do var sec: int8[65] gensec(&sec[0])
						dlg:conf_set('server-secret', &sec[0])
					end







|
|
<
<







291
292
293
294
295
296
297
298
299


300
301
302
303
304
305
306
				dlg:tx_enter()
				if dlg:dbsetup() then
					srv:conprep(lib.store.prepmode.conf)

					do var newkp = lib.crypt.genkp()
					 -- generate server privkey
						var kbuf: uint8[lib.crypt.const.maxdersz]
						var derkey = lib.crypt.der(false,&newkp, kbuf)
						dlg:server_setup_self(dbmode.arglist(1), derkey)


					end

					dlg:conf_set('instance-name', dbmode.arglist(1))
					dlg:conf_set('domain', dbmode.arglist(1))
					do var sec: int8[65] gensec(&sec[0])
						dlg:conf_set('server-secret', &sec[0])
					end

Modified parsav.md from [af14d66fc5] to [23851a52a8].

102
103
104
105
106
107
108




109

110
111
112
113
114
115
116


117
118
119
120
121
122
123
* ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2
* ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server
* ☐ pw-extern-cyrus: try to authenticate against saslauthd
* ☐ pw-extern-dovecot: try to authenticate against a dovecot SASL socket
* ☐ pw-extern-krb5: abuse MIT kerberos as a password verifier
* ☐ pw-extern-imap: abuse an email server as a password verifier
* (extra credit) ☐ pw-extern-radius: verify a user against a radius server




* ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`.

* ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key
* ☐ tls-cert-fp: a fingerprint of a client certificate
* ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user
* ☐ challenge-rsa-sha256: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc-sha256: a Curve25519 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc448-sha256: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☑ trust: authentication always succeeds (or fails, if blacklisted). only use in combination with netmask!!!



## legal

parsav is released under the terms of the EUPL v1.2. copies of this license are included in the repository. by contributing any intellectual property to this project, you reassign ownership and all attendant rights over that intellectual property to the current maintainer. this is to ensure that the project can be relicensed without difficulty in the unlikely event that it is necessary.

## code of conduct








>
>
>
>

>



|
|
|

>
>







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
* ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2
* ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server
* ☐ pw-extern-cyrus: try to authenticate against saslauthd
* ☐ pw-extern-dovecot: try to authenticate against a dovecot SASL socket
* ☐ pw-extern-krb5: abuse MIT kerberos as a password verifier
* ☐ pw-extern-imap: abuse an email server as a password verifier
* (extra credit) ☐ pw-extern-radius: verify a user against a radius server
* ☐ http-oauth: automatically created when a user grants access to an oauth application, consisting of a series of TLVs. these generally should not be created or fiddled with manually
* ☐ http-gssapi: log in with a kerberos principle through the http-authenticate "negotiate" mechanism. do any browsers actually support this??
* ☐ http-extern-header: a value of `H=V` where `H` is a header passed by an app server such as nginx, and `V` is the required value. could be used to e.g. tie parsav into an existing client certificate verification infrastructure with minimal effort.
* ☐ http-extern-header: a value of `H=V` where `H` is a header passed by an app server such as nginx, and `V` is the required value. could be used to tie parsav into an existing client certificate verification infrastructure with minimal effort.
* ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`.
* ☐ api-token-sha{…}: a password (ideally a very long, randomly generated one) that can be sent in the headers to automatically authenticate the user. far less secure than `api-digest-*`!
* ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key
* ☐ tls-cert-fp: a fingerprint of a client certificate
* ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user
* ☐ challenge-rsa: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using any one of the supported hash algorithms, ideally SHA512 or -256.
* ☐ challenge-ecc a Curve25519 public key. the user is presented with a challenge and must sign it with a supported hash algorithm
* ☐ challenge-ecc448: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using a supported hash algorithm.
* ☑ trust: authentication always succeeds (or fails, if blacklisted). only use in combination with netmask!!!

we should also look into support for various kinds of hardware auth. we already have TPM support through RSA auth, but external devices like security keys should be supported as well.

## legal

parsav is released under the terms of the EUPL v1.2. copies of this license are included in the repository. by contributing any intellectual property to this project, you reassign ownership and all attendant rights over that intellectual property to the current maintainer. this is to ensure that the project can be relicensed without difficulty in the unlikely event that it is necessary.

## code of conduct

Modified parsav.t from [04e1a3fbb9] to [392d24dbd5].

310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
			self._store[i/8] = self._store[i/8] and not (1 << (i % 8))
		end
	end
	set.bits = {}
	set.idvmap = {}
	for i,v in ipairs(tbl) do
		set.idvmap[v] = i
		set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end
	end
	set.metamethods.__add = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] or other._store[i]







|







310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
			self._store[i/8] = self._store[i/8] and not (1 << (i % 8))
		end
	end
	set.bits = {}
	set.idvmap = {}
	for i,v in ipairs(tbl) do
		set.idvmap[v] = i
		set.bits[v] = quote var b: set b:clear() b:setbit([i-1], true) in b end
	end
	set.metamethods.__add = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] or other._store[i]

Modified render/conf/sec.t from [157639932a] to [3bc273639f].

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
					end
				end
				credmgr.credlist = cl:finalize()
			end
			credmgr:append(&a)
			--if credmgr.credlist.ct > 0 then credmgr.credlist:free() end
		else
			if new:cmp('pw') then
				var d: data.view.conf_sec_pwnew
				var time = lib.osclock.time(nil)
				var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
				var cmt = co:stra(48)
				cmt:lpush('enrolled over http on '):push(&timestr[0],0)


				d.comment = cmt:finalize()

				var st = d:poolstr(&co.srv.pool)
				--d.comment:free()
				return st
			elseif new:cmp('challenge') then













			-- we're going to break the rules a bit and do database munging from
			-- the rendering code, because doing otherwise in this case would be
			-- genuinely nightmarish
			elseif new:cmp('otp') then
			elseif new:cmp('api') then
			else return pstr.null() end
		end







<
<
|
|
|
|
>
>





|
>
>
>
>
>
>
>
>
>
>
>
>
>







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
					end
				end
				credmgr.credlist = cl:finalize()
			end
			credmgr:append(&a)
			--if credmgr.credlist.ct > 0 then credmgr.credlist:free() end
		else


			var time = lib.osclock.time(nil)
			var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
			var cmt = co:stra(48)
			cmt:lpush('enrolled over http on '):push(&timestr[0],0)
			if new:cmp('pw') then
				var d: data.view.conf_sec_pwnew
				d.comment = cmt:finalize()

				var st = d:poolstr(&co.srv.pool)
				--d.comment:free()
				return st
			elseif new:cmp('rsa') then
				var c = co:stra(64)
				lib.crypt.cryptogram(&c, 8)
				var cptr = c:finalize();
				var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), cptr); -- TODO should expire after 10min
				var hmacte: int8[lib.math.shorthand.maxlen]
				var hmacte_len = lib.math.shorthand.gen(lib.math.truncate64(hmac.ptr, hmac.ct), &hmacte[0])
				var d = data.view.conf_sec_keynew {
					comment = cmt:finalize();
					nonce = cptr;
					noncevld = pstr { ptr = &hmacte[0], ct = hmacte_len };
				}

				return d:poolstr(&co.srv.pool)
			-- we're going to break the rules a bit and do database munging from
			-- the rendering code, because doing otherwise in this case would be
			-- genuinely nightmarish
			elseif new:cmp('otp') then
			elseif new:cmp('api') then
			else return pstr.null() end
		end

Modified render/login.t from [434636bebc] to [da67d9c6fd].

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
login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
	var doc = [lib.srv.convo.page] {
		title = 'instance logon';
		class = 'login';
		cache = false;
	}



	if user == nil then
		var form = data.view.login_username {
			loginmsg = msg;
		}
		if form.loginmsg.ptr == nil then
			form.loginmsg = 'identify yourself for access to this instance.'
		end
		doc.body = form:tostr()
	elseif creds:sz() == 0 then
		co:complain(403,'access denied','your host is not eligible to authenticate as this user')
		return
	elseif creds:sz() == 1 then










		if creds.trust() then
			-- TODO log in immediately
			return
		end

		var ch = data.view.login_challenge {
			handle = user.handle;
			name = lib.coalesce(user.nym, user.handle);
		}
		if creds.pw() then
			ch.challenge = 'enter the password associated with your account'
			ch.label = 'password'
			ch.method = 'pw'
			ch.auto = 'current-password';
		elseif creds.otp() then
			ch.challenge = 'enter a valid one-time password for your account'
			ch.label = 'OTP code'
			ch.method = 'otp'
			ch.auto = 'one-time-code';
		elseif creds.challenge() then






			ch.challenge = 'sign the challenge token: <code>...</code>'




			ch.label = 'digest'
			ch.method = 'challenge'
			ch.auto = 'one-time-code';














		else
			co:complain(500,'login failure','unknown login method')

			return
		end

		doc.body = ch:tostr()
	else
		-- pick a method









	end

	co:stdpage(doc)
	doc.body:free()
end

return login_form







>
>











|
>
>
>
>
>
>
>
>
>
>













|




|

>
>
>
>
>
>
|
>
>
>
>


<
>
>
>
>
>
>
>
>
>
>
>
>
>
>

<
>



|
<
|
>
>
>
>
>
>
>
>
>



|



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
login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
	var doc = [lib.srv.convo.page] {
		title = 'instance logon';
		class = 'login';
		cache = false;
	}

	var how = co:ppostv('how')

	if user == nil then
		var form = data.view.login_username {
			loginmsg = msg;
		}
		if form.loginmsg.ptr == nil then
			form.loginmsg = 'identify yourself for access to this instance.'
		end
		doc.body = form:tostr()
	elseif creds:sz() == 0 then
		co:complain(403,'access denied','your host is not eligible to authenticate as this user')
		return
	elseif creds:sz() == 1 or how:ref() then
		var newcreds: lib.store.credset
		if how:ref() then
			if     how:cmp('pw')    then newcreds = creds and [lib.store.credset.bits.pw]
			elseif how:cmp('chlg')  then newcreds = creds and [lib.store.credset.bits.challenge]
			elseif how:cmp('otp')   then newcreds = creds and [lib.store.credset.bits.otp]
			elseif how:cmp('trust') then newcreds = creds and [lib.store.credset.bits.trust]
			else co:complain(400, 'bad request', 'the requested authentication method is not available') return end
			creds = &newcreds
		end

		if creds.trust() then
			-- TODO log in immediately
			return
		end

		var ch = data.view.login_challenge {
			handle = user.handle;
			name = lib.coalesce(user.nym, user.handle);
		}
		if creds.pw() then
			ch.challenge = 'enter the password associated with your account'
			ch.label = 'password'
			ch.method = 'pw'
			ch.inputfield = '<input type="password" autocomplete="current-password" name="response" id="response" autofocus required>';
		elseif creds.otp() then
			ch.challenge = 'enter a valid one-time password for your account'
			ch.label = 'OTP code'
			ch.method = 'otp'
			ch.inputfield = '<input type="text" autocomplete="one-time-code" name="response" id="response" autofocus required>';
		elseif creds.challenge() then
			var tok   = co:stra(128)
			var chlg  = co:stra(128)
			var input = co:stra(256)
			var time = lib.osclock.time(nil)

			lib.crypt.cryptogram(&tok,6)
			chlg:lpush 'sign the challenge token <code style="display:block;user-select: all">'
			    :push(tok.buf,tok.sz)
				:lpush '</code>'

			ch.challenge = chlg:finalize()
			ch.label = 'digest'
			ch.method = 'challenge'

			input:lpush '<textarea autocomplete="one-time-code" name="response" id="response" autofocus required></textarea><input type="hidden" name="time" value="'
			     :shpush(time)
				 :lpush '"><input type="hidden" name="token" value="'
			     :push(tok.buf,tok.sz)
			tok:shpush(time)
			var hmac = lib.crypt.hmacp(&co.srv.pool,
							lib.crypt.alg.sha256,
							co.srv.cfg.secret:blob(),
							tok:finalize())
			input:lpush '"><input type="hidden" name="vfy" value="'
				 :shpush(lib.math.truncate64(hmac.ptr, hmac.ct)) -- FIXME this is probably not very secure...
				 :lpush '">'
				 
			ch.inputfield = input:finalize()
		else

			co:complain(400,'login failure','no usable login methods are available')
			return
		end

		doc.body = ch:poolstr(&co.srv.pool)

	else -- pick a method
		var a = co:stra(400)
		var username = lib.html.sanitize(&co.srv.pool, pstr{user.handle,0}, true)
		a:lpush '<form action="/login" method="post" class="auth-select"><p>multiple authentication mechanisms are available. select one to continue.</p><menu><input type="hidden" name="user" value="':ppush(username):lpush'">'
		if creds.trust()     then a:lpush '<button name="how" value="trust">trust</button>'    end
		if creds.pw()        then a:lpush '<button name="how" value="pw">password</button>'    end
		if creds.otp()       then a:lpush '<button name="how" value="otp">TOTP code</button>'  end
		if creds.challenge() then a:lpush '<button name="how" value="chlg">challenge</button>' end
		a:lpush '</menu></form>'
		doc.body = a:finalize()
	end

	co:stdpage(doc)
	--doc.body:free()
end

return login_form

Modified route.t from [6caa1366ba] to [cd7a14ae6e].

140
141
142
143
144
145
146
147
148

























149
150
151
152
153
154
155
156
...
339
340
341
342
343
344
345

346
347
348
349
350
351
352
353
354
355
356






357





358


























359
360
361
362
363
364
365
			-- pick an auth method
			lib.render.login(co, act.ptr, &cs, pstring.null())
		else var aid: uint64 = 0
			lib.dbg('authentication attempt beginning')
			-- attempt login with provided method
			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				aid = co.srv:actor_auth_pw(co.peer,
					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})

























			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				lib.dbg('using otp auth')
				-- ··· --
			else lib.dbg('invalid auth method') end

			-- error out
			if aid == 0 then
				lib.render.login(co, nil, nil,  'authentication failure')
................................................................................
		co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil))
		-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
		co:installkey('?',co.aid)
		return
	elseif act:cmp( 'newcred') then
		var cmt = co:ppostv('comment')
		var pw = co:ppostv('newpw')

		var aid: uint64 = 0
		if pw:ref() then
			var cpw = co:ppostv('rptpw')
			if not pw:cmp(cpw) then
				co:complain(400,'enrollment failure','the passwords you supplied do not match')
				return
			end
			aid = co.srv:auth_attach_pw(uid, false, pw, cmt)
		else
			var key = co:ppostv('newkey')
			if key:ref() then












			end


























		end
		if aid ~= 0 then
			lib.dbg('setting credential restrictions')
			var privs = [(function()
				local check = quote end
				local me = symbol(lib.store.privset)
				for i,v in ipairs(lib.store.privset.members) do







|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|







 







>








|
|
|
>
>
>
>
>
>

>
>
>
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
...
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
			-- pick an auth method
			lib.render.login(co, act.ptr, &cs, pstring.null())
		else var aid: uint64 = 0
			lib.dbg('authentication attempt beginning')
			-- attempt login with provided method
			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				aid = co.srv:actor_auth_pw(co.peer,
					pstring {ptr=usn,ct=usnl},
					pstring {ptr=chrs,ct=chrsl})
			elseif lib.str.ncmp('challenge', am, lib.math.biggest(9,aml)) == 0 and chrs ~= nil then
				lib.dbg('challenge attempt beginning')
				var s_time = co:ppostv('time')
				var s_vfy = co:ppostv('vfy')
				var token = co:ppostv('token')
				if s_time:ref() and s_vfy:ref() and token:ref() then 
					lib.dbg('checking hmac validity')
					var vftok = co:stra(128) vftok:ppush(token):ppush(s_time)
					var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), vftok:finalize())
					var vfy, vfyok = lib.math.shorthand.parse(s_vfy.ptr, s_vfy.ct)
					if vfyok and lib.math.truncate64(hmac.ptr,hmac.ct) == vfy then
						lib.dbg('checking expiration time')
						var time, timeok = lib.math.shorthand.parse(s_time.ptr, s_time.ct)
						if timeok and lib.osclock.time(nil) - time < [2 * 60] then -- two minutes
							lib.dbg('decoding base64')
							var bin = co.srv.pool:alloc(uint8, chrsl)
							var binlen: intptr
							if lib.b64.mbedtls_base64_decode(bin.ptr, bin.ct, &binlen, [&uint8](chrs), chrsl) == 0 then
								lib.dbg('running signature <',{chrs,chrsl},'> against challenge keys for token [', {token.ptr,token.ct}, ']')
								aid = co.srv:actor_auth_challenge(co.peer,
									pstring {usn,usnl}, binblob{bin.ptr,binlen}, token)
							end
						end
					end
				end
			elseif lib.str.ncmp('otp', am, lib.math.biggest(3,aml)) == 0 and chrs ~= nil then
				lib.dbg('using otp auth')
				-- ··· --
			else lib.dbg('invalid auth method') end

			-- error out
			if aid == 0 then
				lib.render.login(co, nil, nil,  'authentication failure')
................................................................................
		co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil))
		-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
		co:installkey('?',co.aid)
		return
	elseif act:cmp( 'newcred') then
		var cmt = co:ppostv('comment')
		var pw = co:ppostv('newpw')
		var rsapub = co:ppostv('newrsa'):blob()
		var aid: uint64 = 0
		if pw:ref() then
			var cpw = co:ppostv('rptpw')
			if not pw:cmp(cpw) then
				co:complain(400,'enrollment failure','the passwords you supplied do not match')
				return
			end
			aid = co.srv:auth_attach_pw(uid, false, pw, cmt)
		elseif rsapub:ref() then
			var sig = co:ppostv('sig')
			var nonce = co:ppostv('nonce')
			var s_noncevld = co:ppostv('noncevld')
			var noncevld, ok = lib.math.shorthand.parse(s_noncevld.ptr, s_noncevld.ct)
			if not ok then
				co:complain(403,'try harder next time','you call that cryptanalysis?')
				return
			end

			var fr = co.srv.pool:frame()
			var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce)
			if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then
				co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy')
				return
			end

			var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL
			if not pkres.ok then
				co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file')
				return
			end
			var pk = pkres.val
			defer pk:free()

			var decoded = co.srv.pool:alloc(uint8,sig.ct)
			var decoded_sz: intptr = 0
			if lib.b64.mbedtls_base64_decode(decoded.ptr,sig.ct,&decoded_sz,[&uint8](sig.ptr),sig.ct) ~= 0 then
				co:complain(400,'invalid signature','the signature you supplied is not encoded in valid base64')
				return
			end

			var vfy, secl = lib.crypt.verify(&pk, nonce.ptr, nonce.ct, decoded.ptr, decoded_sz)
			if not vfy then
				co:complain(403,'verification failed','the signature you supplied does not match the required nonce')
				return
			end

			var dbuf: uint8[lib.crypt.const.maxdersz]
			var derkey = lib.crypt.der(true, &pk, &dbuf[0])
			aid = co.srv:auth_attach_rsa(co.who.id, false, derkey, cmt)
			co.srv.pool:reset(fr)
		end
		if aid ~= 0 then
			lib.dbg('setting credential restrictions')
			var privs = [(function()
				local check = quote end
				local me = symbol(lib.store.privset)
				for i,v in ipairs(lib.store.privset.members) do

Modified srv.t from [2ff93305a1] to [826b7b2edb].

818
819
820
821
822
823
824



825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851

































852
853
854
855
856
857
858
			cs = cs + set
			ok = iok
		end
	end
	return cs, ok
end




terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64
	for i=0,self.sources.ct do
		if self.sources(i).backend ~= nil and
		   self.sources(i).backend.actor_auth_pw ~= nil then
			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
			if aid ~= 0 then
				if uid == 0 then
					lib.dbg('new user just logged in, creating account entry')
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var na = lib.store.actor.mk(&kbuf[0])
					na.handle = newhnd.ptr
					var newuid: uint64
					if self.sources(i).backend.actor_create ~= nil then
						newuid = self.sources(i):actor_create(&na)
					else newuid = self:actor_create(&na) end

					if self.sources(i).backend.actor_auth_register_uid ~= nil then
						self.sources(i):actor_auth_register_uid(aid,newuid)
					end
				end
				return aid
			end
		end
	end

	return 0
end


































terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end








>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|

|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
			cs = cs + set
			ok = iok
		end
	end
	return cs, ok
end

local function mk_auth_fn(suffix,...)
	local syms = {...}
	local name = 'actor_auth_' .. suffix
	srv.methods[name] = terra(self: &srv, ip: lib.store.inet, user: pstring, [syms]): uint64
		for i=0,self.sources.ct do
			if self.sources(i).backend ~= nil and
			   self.sources(i).backend.[name] ~= nil then
				var aid,uid,newhnd = self.sources(i):[name](ip,user, [syms])
				if aid ~= 0 then
					if uid == 0 then
						lib.dbg('new user just logged in, creating account entry')
						var kbuf: uint8[lib.crypt.const.maxdersz]
						var na = lib.store.actor.mk(&kbuf[0])
						na.handle = newhnd.ptr
						var newuid: uint64
						if self.sources(i).backend.actor_create ~= nil then
							newuid = self.sources(i):actor_create(&na)
						else newuid = self:actor_create(&na) end

						if self.sources(i).backend.actor_auth_register_uid ~= nil then
							self.sources(i):actor_auth_register_uid(aid,newuid)
						end
					end
					return aid
				end
			end
		end

		return 0
	end
	srv.methods[name].name = name
end

mk_auth_fn('pw',        symbol(pstring))
mk_auth_fn('challenge', symbol(lib.mem.ptr(uint8)), symbol(pstring))

--terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64
--	for i=0,self.sources.ct do
--		if self.sources(i).backend ~= nil and
--		   self.sources(i).backend.actor_auth_pw ~= nil then
--			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
--			if aid ~= 0 then
--				if uid == 0 then
--					lib.dbg('new user just logged in, creating account entry')
--					var kbuf: uint8[lib.crypt.const.maxdersz]
--					var na = lib.store.actor.mk(&kbuf[0])
--					na.handle = newhnd.ptr
--					var newuid: uint64
--					if self.sources(i).backend.actor_create ~= nil then
--						newuid = self.sources(i):actor_create(&na)
--					else newuid = self:actor_create(&na) end
--
--					if self.sources(i).backend.actor_auth_register_uid ~= nil then
--						self.sources(i):actor_auth_register_uid(aid,newuid)
--					end
--				end
--				return aid
--			end
--		end
--	end
--
--	return 0
--end

terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end

Modified static/style.scss from [e23a7affc6] to [048cf30682].

379
380
381
382
383
384
385














386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413

414
415
416
417
418
419
420
...
480
481
482
483
484
485
486

487
488
489
490
491
492
493
494
495
496
497
498
499
500
501


502





503
504
505
506
507
508
509
...
697
698
699
700
701
702
703

704
705
706
707
708
709
710
	margin:auto;
	padding: 0.5in;
	text-align: center;
	menu:first-of-type { margin-top: 0.3in; }
	img.icon { width: 1.875in; height: 1.875in; }
}















div.login {
	@extend %box;
	width: 4in;
	padding: 0.4in;
	> .msg {
		text-align: center;
		padding: 0.3in;
	}
	> .msg:first-child { padding-top: 0; }
	> .user {
		width: max-content; margin: auto;
		background: tone(-20%,-0.3);
		border: 1px solid black;
		color: tone(-50%);
		padding: 0.1in;
		> img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; }
		> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
	}
	>form {
		display: grid;
		grid-template-columns: 1fr 1fr;
		grid-template-rows: 1.2em 1fr 1fr;
		grid-gap: 5px;
		> label, input, button { display: block; }
		> label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold }
		> input { grid-column: 1 / 3; grid-row: 2/3; }
		> button { grid-column: 2 / 3; grid-row: 3/4; }
		> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }

	}
}

form.compose {
	@extend %box;
	display: grid;
	grid-template-columns: 1.1in 2fr min-content 1fr 1.5fr;
................................................................................
		position: absolute;
		top: -0.3in;
		right: 0.1in;
		margin: 0.1in;
		padding: 0.1in;
		&:hover { font-weight: bold; }
	}

}

code {
	@extend %teletype;
	background: tone(-55%);
	border: 1px inset tone(-20%);
	padding: 2px 6px;
	font-size: 1.5ex !important;
	letter-spacing: 1.3px;
	padding-bottom: 3px;
	border-radius: 2px;
	vertical-align: baseline;
	box-shadow: 1px 1px 1px black;
}



pre { @extend %teletype; white-space: pre-wrap; }






div.thread {
	margin-left: 0.3in;
	& + article.post { margin-top: 0.3in; }
}

a[href].username {
................................................................................
		> label,summary { display:block; font-weight: bold; padding: 0.03in 0; }
		> .txtbox {
			@extend %serif;
			box-sizing: border-box;
			padding: 0.08in 0.1in;
			border: 1px solid black;
			background: tone(-55%);

		}
		> input, textarea, .txtbox {
			display: block;
			width: 100%;
		}
		> textarea { resize: vertical; min-height: 2in; }
	}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>







 







>





|




|




>
>
|
>
>
>
>
>







 







>







379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
...
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
...
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
	margin:auto;
	padding: 0.5in;
	text-align: center;
	menu:first-of-type { margin-top: 0.3in; }
	img.icon { width: 1.875in; height: 1.875in; }
}

body.login {
	form.auth-select {
		@extend %box;
		width: 3in;
		padding: 0.4in;
		p { text-align: center; }
		menu {
			%button {
				display: block;
				width: 100%;
				& + %button { border-top: none; }
			}
		}
	}
	div.login {
		@extend %box;
		width: 4in;
		padding: 0.4in;
		> .msg {
			text-align: center;
			padding: 0.3in;
		}
		> .msg:first-child { padding-top: 0; }
		> .user {
			width: max-content; margin: auto;
			background: tone(-20%,-0.3);
			border: 1px solid black;
			color: tone(-50%);
			padding: 0.1in;
			> img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; }
			> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
		}
		>form {
			display: grid;
			grid-template-columns: 1fr 1fr;
			grid-template-rows: 1.2em max-content max-content;
			grid-gap: 5px;
			> label, input, button { display: block; }
			> label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold }
			> input, textarea  { grid-column: 1 / 3; grid-row: 2/3; }
			> button { grid-column: 2 / 3; grid-row: 3/4; }
			> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
		}
	}
}

form.compose {
	@extend %box;
	display: grid;
	grid-template-columns: 1.1in 2fr min-content 1fr 1.5fr;
................................................................................
		position: absolute;
		top: -0.3in;
		right: 0.1in;
		margin: 0.1in;
		padding: 0.1in;
		&:hover { font-weight: bold; }
	}
	p { text-align: justify; }
}

code {
	@extend %teletype;
	background: tone(-55%);
	// border: 1px inset tone(-20%);
	padding: 2px 6px;
	font-size: 1.5ex !important;
	letter-spacing: 1.3px;
	padding-bottom: 3px;
	border-radius: 4px;
	vertical-align: baseline;
	box-shadow: 1px 1px 1px black;
}

pre {
	@extend %teletype;
	white-space: pre-wrap;
	> code:only-child {
		display: block;
		padding: 0.1in;
	}
}

div.thread {
	margin-left: 0.3in;
	& + article.post { margin-top: 0.3in; }
}

a[href].username {
................................................................................
		> label,summary { display:block; font-weight: bold; padding: 0.03in 0; }
		> .txtbox {
			@extend %serif;
			box-sizing: border-box;
			padding: 0.08in 0.1in;
			border: 1px solid black;
			background: tone(-55%);
			user-select: all;
		}
		> input, textarea, .txtbox {
			display: block;
			width: 100%;
		}
		> textarea { resize: vertical; min-height: 2in; }
	}

Modified store.t from [9b6251fcb3] to [53eb63c414].

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
...
384
385
386
387
388
389
390






391
392
393
394
395
396
397
...
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
	end
	-- TODO validate fully
	return true
end

terra m.actor.methods.mk(kbuf: &uint8)
	var newkp = lib.crypt.genkp()
	var privsz = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
		knownsince = lib.osclock.time(nil);
		rights = m.rights_default();
		avatarid = 0;
		epithet = nil, key = [lib.mem.ptr(uint8)] {
			ptr = &kbuf[0], ct = privsz
		};
	}
end

struct m.actor_stats {
	posts: intptr
	follows: intptr
	followers: intptr
................................................................................
			-> {uint64, uint64, pstr}
	actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) }
			-> {uint64, uint64, pstr}
		-- handles password-based logins against hashed passwords
			-- origin: inet
			-- handle: rawstring
			-- token:  rawstring






	actor_auth_tls:    {&m.source, m.inet, rawstring}
			-> {uint64, uint64, pstr}
		-- handles implicit authentication performed as part of an TLS connection
			-- origin: inet
			-- fingerprint: rawstring
	actor_auth_api:    {&m.source, m.inet, rawstring, rawstring} -> uint64
			-> {uint64, uint64, pstr}
................................................................................
	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> uint64
	auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: pstring
	auth_privs_set: {&m.source, uint64, m.privset} -> {}
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}







|






|
<
<







 







>
>
>
>
>
>







 







|







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176


177
178
179
180
181
182
183
...
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
...
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
	end
	-- TODO validate fully
	return true
end

terra m.actor.methods.mk(kbuf: &uint8)
	var newkp = lib.crypt.genkp()
	var derkey = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
		knownsince = lib.osclock.time(nil);
		rights = m.rights_default();
		avatarid = 0;
		epithet = nil, key = derkey;


	}
end

struct m.actor_stats {
	posts: intptr
	follows: intptr
	followers: intptr
................................................................................
			-> {uint64, uint64, pstr}
	actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) }
			-> {uint64, uint64, pstr}
		-- handles password-based logins against hashed passwords
			-- origin: inet
			-- handle: rawstring
			-- token:  rawstring
	actor_auth_challenge: {&m.source, m.inet, pstr, lib.mem.ptr(uint8), pstr }
			-> {uint64, uint64, pstr}
			-- origin:          inet
			-- handle:          rawstring
			-- response:        rawstring
			-- challenge token: pstring
	actor_auth_tls:    {&m.source, m.inet, rawstring}
			-> {uint64, uint64, pstr}
		-- handles implicit authentication performed as part of an TLS connection
			-- origin: inet
			-- fingerprint: rawstring
	actor_auth_api:    {&m.source, m.inet, rawstring, rawstring} -> uint64
			-> {uint64, uint64, pstr}
................................................................................
	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> uint64
	auth_attach_rsa: {&m.source, uint64, bool, lib.mem.ptr(uint8), pstr} -> uint64
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: pstring
	auth_privs_set: {&m.source, uint64, m.privset} -> {}
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}

Modified str.t from [c25d641c64] to [2798df18ea].

58
59
60
61
62
63
64






65
66
67
68
69
70
71
			end
			return true
		end
		terra ty:ffw()
			var newp = m.ffw(self.ptr,self.ct)
			var newct = self.ct - (newp - self.ptr)
			return ty { ptr = newp, ct = newct }






		end
	end
	install_funcs(strptr)
	install_funcs(strref)

	--strptr.methods.cmpl = macro(function(self,other)
	--	return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] })







>
>
>
>
>
>







58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
			end
			return true
		end
		terra ty:ffw()
			var newp = m.ffw(self.ptr,self.ct)
			var newct = self.ct - (newp - self.ptr)
			return ty { ptr = newp, ct = newct }
		end
		terra ty:blob()
			return byteptr {
				ptr = [&uint8](self.ptr);
				ct = self.ct;
			}
		end
	end
	install_funcs(strptr)
	install_funcs(strref)

	--strptr.methods.cmpl = macro(function(self,other)
	--	return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] })

Modified view/conf-sec-credmg.tpl from [c30c9309b5] to [a2babc26c2].

3
4
5
6
7
8
9

10
11
12
13
14
15
16
..
25
26
27
28
29
30
31

32
33
34
35
36
	<p>this account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
	<select size="6" name="cred">
		@credlist
	</select>
	<menu class="horizontal choice">
		<button name="act" value="reset">reset</button>
		<button name="act" value="revoke">revoke</button>

	</menu>
</form>
<hr>
<form method="get">
	<p>you can associate extra credentials with this account. you can also limit how much of this account’s authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read the account timeline, but not post as the account owner or access any of his administrative powers. if you don't select a capability set, the credential will be able to wield the full scope of the associated account‘s powers.</p>
	<div class="check-panel">
		<label><input type="checkbox" name="allow-post"> post</label>
................................................................................
	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
	<div class="elem">
		<label for="netmask">netmask</label>
		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
	</div>
	<menu class="vertical choice">
		<button name="new" value="pw">new password</button>

		<button name="new" value="otp">new OTP key</button>
		<button name="new" value="api">new API token</button>
		<button name="new" value="challenge">new challenge key</button>
	</menu>
</form>







>







 







>


<


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
..
26
27
28
29
30
31
32
33
34
35

36
37
	<p>this account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
	<select size="6" name="cred">
		@credlist
	</select>
	<menu class="horizontal choice">
		<button name="act" value="reset">reset</button>
		<button name="act" value="revoke">revoke</button>
		@?auth
	</menu>
</form>
<hr>
<form method="get">
	<p>you can associate extra credentials with this account. you can also limit how much of this account’s authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read the account timeline, but not post as the account owner or access any of his administrative powers. if you don't select a capability set, the credential will be able to wield the full scope of the associated account‘s powers.</p>
	<div class="check-panel">
		<label><input type="checkbox" name="allow-post"> post</label>
................................................................................
	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
	<div class="elem">
		<label for="netmask">netmask</label>
		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
	</div>
	<menu class="vertical choice">
		<button name="new" value="pw">new password</button>
		<button name="new" value="rsa">new RSA key</button>
		<button name="new" value="otp">new OTP key</button>
		<button name="new" value="api">new API token</button>

	</menu>
</form>

Added view/conf-sec-keynew.tpl version [039cf710c4].





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<form method="post">
	<div class="elem">
		<label for="comment">comment</label>
		<input type="text" id="comment" name="comment" value="@comment" required>
	</div>
	<div class="elem">
		<label for="newkey">public key in PEM format</label>
		<textarea id="newkey" name="newrsa" required></textarea>
	</div>
	<p>to confirm your ownership of the private key, you'll need to sign the nonce provided below before it expires in 10 minutes. on unix-like OSes, you can usually use the openssl utility for this.</p>
	<code style="display:block; user-select: all">echo -n @nonce | openssl dgst -sha256 -sign privkey.pem | openssl base64</code>
	<div class="elem">
		<label>nonce</label>
		<div class="txtbox">@nonce</div>
		<input type="hidden" name="nonce" value="@nonce">
		<input type="hidden" name="noncevld" value="@noncevld">
	</div>
	<div class="elem">
		<label for="sig">nonce signature</label>
		<textarea id="sig" name="sig" required></textarea>
	</div>
	<menu class="choice horizontal">
		<button name="act" value="newcred">enroll</button>
		<a class="button" href="?">cancel</a>
	</menu>
</form>

Modified view/load.lua from [15344e4760] to [3d222d2b19].

20
21
22
23
24
25
26

27
28
29
30
31
32
33
	'login-challenge';

	'conf';
	'conf-profile';
	'conf-sec';
	'conf-sec-credmg';
	'conf-sec-pwnew';

	'conf-user-ctl';
}

local ingest = function(filename)
	local hnd = io.open(path..'/'..filename)
	local txt = hnd:read('*a')
	io.close(hnd)







>







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
	'login-challenge';

	'conf';
	'conf-profile';
	'conf-sec';
	'conf-sec-credmg';
	'conf-sec-pwnew';
	'conf-sec-keynew';
	'conf-user-ctl';
}

local ingest = function(filename)
	local hnd = io.open(path..'/'..filename)
	local txt = hnd:read('*a')
	io.close(hnd)

Modified view/login-challenge.tpl from [84fccbb367] to [cc4592543f].

3
4
5
6
7
8
9
10
11
12
13
14
		<img src="/avi/@handle">
		<div class="name">@!name</div>
	</div>
	<div class="msg">@challenge</div>
	<form action="/login" method="post">
		<label for="response">@label</label>
		<input type="hidden" name="user" value="@:handle">
		<input type="password" autocomplete="@auto" name="response" id="response" autofocus required>
		<button type="submit" name="authmethod" value="@method">authenticate</button>
		<a href="/login">cancel</a>
	</form>
</div>







|




3
4
5
6
7
8
9
10
11
12
13
14
		<img src="/avi/@handle">
		<div class="name">@!name</div>
	</div>
	<div class="msg">@challenge</div>
	<form action="/login" method="post">
		<label for="response">@label</label>
		<input type="hidden" name="user" value="@:handle">
		@inputfield
		<button type="submit" name="authmethod" value="@method">authenticate</button>
		<a href="/login">cancel</a>
	</form>
</div>

Modified view/login-username.tpl from [8c165f8ae9] to [c0dae1cbbc].

1
2
3
4
5
6
7
8
<div class="login">
	<div class="msg">@loginmsg</div>
	<form action="/login" method="post">
		<label for="user">local handle</label>
		<input type="text" name="user" id="user" autocomplete="username" autofocus required>
		<button type="submit">log on</button>
	</form>
</div>





|


1
2
3
4
5
6
7
8
<div class="login">
	<div class="msg">@loginmsg</div>
	<form action="/login" method="post">
		<label for="user">local handle</label>
		<input type="text" name="user" id="user" autocomplete="username" autofocus required>
		<button>log on</button>
	</form>
</div>