main
ts 81 lines 3.59 KB
Raw
1 import { Account, getAccount, normalizeUsername, updateAccount } from './perm'
2 import { ALLOW_SESSION_IP_CHANGE, HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './cross-const'
3 import * as srp from 'tssrp6a'
4 import { Context } from 'koa'
5 import { srpClientPart } from './srp'
6 import { DAY } from './cross'
7 import { expiringCache } from './expiringCache'
8 import { createHash } from 'node:crypto'
9 import events from './events'
10
11 const srp6aNimbusRoutines = new srp.SRPRoutines(new srp.SRPParameters())
12
13 export async function srpServerStep1(account: Account) {
14 if (!account.srp)
15 throw HTTP_NOT_ACCEPTABLE
16 const [salt, verifier] = account.srp.split('|')
17 if (!salt || !verifier)
18 throw Error("malformed account")
19 const srpSession = new srp.SRPServerSession(srp6aNimbusRoutines)
20 const srpServer = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
21 return { srpServer, salt, pubKey: String(srpServer.B) } // cast to string cause bigint can't be jsonized
22 }
23
24 const cache = expiringCache<Promise<boolean>>(60_000)
25 export async function srpCheck(username: string, password: string) {
26 const account = getAccount(username)
27 if (!account?.srp || !password) return
28 const k = createHash('sha256').update(username + password + account.srp).digest("hex")
29 const good = await cache.try(k, async () => {
30 const { srpServer, salt, pubKey } = await srpServerStep1(account)
31 const client = await srpClientPart(srp, username, password, salt, pubKey)
32 return srpServer.step2(client.A, client.M1).then(() => true, () => false)
33 })
34 return good ? account : undefined
35 }
36
37 export function getCurrentUsername(ctx: Context): string {
38 return ctx.state.account?.username || ''
39 }
40
41 export async function clearTextLogin(ctx: Context, u: string, p: string, via: string) {
42 if (!p) return
43 if ((await events.emitAsync('attemptingLogin', { ctx, username: u, via }))?.isDefaultPrevented()) return
44 const plugins = await events.emitAsync('clearTextLogin', { ctx, username: u, password: p, via }) // provide clear password to plugins
45 const a = plugins?.some(x => x === true) ? getAccount(u) : await srpCheck(u, p)
46 if (!a && u)
47 events.emit('failedLogin', { ctx, username: u, via })
48 return a
49 }
50
51 // centralized log-in state
52 export async function setLoggedIn(ctx: Context, username: string | false) {
53 const s = ctx.session
54 if (!s)
55 return ctx.throw(HTTP_SERVER_ERROR,'session')
56 delete ctx.state.usernames
57 if (username === false) {
58 if (s.username)
59 events.emit('logout', ctx)
60 delete ctx.state.account
61 ctx.session = null
62 return
63 }
64 delete s.loggingIn // clear pending SRP handshake state
65 const a = ctx.state.account = getAccount(username)
66 if (!a) return
67 await events.emitAsync('finalizingLogin', { ctx, username, inputs: { ...ctx.state.params, ...ctx.query } })
68 const normalized = normalizeUsername(username)
69 if (s.username !== normalized)
70 delete s.allowNet // discard restrictions cached for another identity before replacing the session account
71 s.username = normalized
72 s.ts = Date.now()
73 const k = ALLOW_SESSION_IP_CHANGE
74 s[k] = k in ctx.query || Boolean(ctx.state.params?.[k]) || undefined // login APIs will get ctx.state.params, others can rely on ctx.query
75 if (!a.expire && a.days_to_live)
76 updateAccount(a, { expire: new Date(Date.now() + a.days_to_live! * DAY) })
77 await events.emitAsync('login', ctx)
78 }
79
80 // since session are currently stored in cookies, we need to store this information
81 export const invalidateSessionBefore = new Map<string, number>()