-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroom-settings.ts
More file actions
166 lines (149 loc) · 4.47 KB
/
room-settings.ts
File metadata and controls
166 lines (149 loc) · 4.47 KB
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
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
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
131
132
133
134
135
136
137
138
139
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
import { fromBase64Url, toBase64Url } from '@/crypto'
const SETTINGS_VERSION = 1
const DEFAULT_SAFEWORD_ITERATIONS = 120_000
export interface SafeWordSettings {
saltB64: string
hashB64: string
iterations: number
}
export interface RoomSettings {
usernameModeEnabled: boolean
safeWord: SafeWordSettings | null
maxParticipants: number
}
interface RoomSettingsPayloadV1 {
v: number
u?: 1
s?: {
s: string
h: string
i: number
}
m?: number // maxParticipants
}
export const DEFAULT_ROOM_SETTINGS: RoomSettings = {
usernameModeEnabled: false,
safeWord: null,
maxParticipants: 2,
}
function isSafeWordSettings(input: unknown): input is SafeWordSettings {
if (typeof input !== 'object' || input === null) return false
const obj = input as Record<string, unknown>
return (
typeof obj.saltB64 === 'string' &&
typeof obj.hashB64 === 'string' &&
typeof obj.iterations === 'number' &&
Number.isInteger(obj.iterations) &&
obj.iterations > 0
)
}
export function normalizeRoomSettings(input?: Partial<RoomSettings> | null): RoomSettings {
const maxParticipants = typeof input?.maxParticipants === 'number'
&& Number.isInteger(input.maxParticipants)
&& input.maxParticipants >= 2
&& input.maxParticipants <= 200
? input.maxParticipants
: 2
return {
usernameModeEnabled: Boolean(input?.usernameModeEnabled),
safeWord: isSafeWordSettings(input?.safeWord) ? input.safeWord : null,
maxParticipants,
}
}
export function encodeRoomSettings(settings: RoomSettings): string | null {
const normalized = normalizeRoomSettings(settings)
if (!normalized.usernameModeEnabled && !normalized.safeWord && normalized.maxParticipants === 2) {
return null
}
const payload: RoomSettingsPayloadV1 = { v: SETTINGS_VERSION }
if (normalized.usernameModeEnabled) {
payload.u = 1
}
if (normalized.safeWord) {
payload.s = {
s: normalized.safeWord.saltB64,
h: normalized.safeWord.hashB64,
i: normalized.safeWord.iterations,
}
}
if (normalized.maxParticipants !== 2) {
payload.m = normalized.maxParticipants
}
const bytes = new TextEncoder().encode(JSON.stringify(payload))
return toBase64Url(bytes)
}
export function decodeRoomSettings(encoded: string | null | undefined): RoomSettings | null {
if (!encoded) return null
try {
const bytes = fromBase64Url(encoded)
const parsed: unknown = JSON.parse(new TextDecoder().decode(bytes))
if (typeof parsed !== 'object' || parsed === null) return null
const payload = parsed as RoomSettingsPayloadV1
if (payload.v !== SETTINGS_VERSION) return null
const safeWord = payload.s && typeof payload.s === 'object'
? {
saltB64: payload.s.s,
hashB64: payload.s.h,
iterations: payload.s.i,
}
: null
return normalizeRoomSettings({
usernameModeEnabled: payload.u === 1,
safeWord,
maxParticipants: typeof payload.m === 'number' ? payload.m : 2,
})
} catch {
return null
}
}
function trimSafeWord(input: string): string {
return input.trim()
}
async function deriveSafeWordHash(input: string, saltB64: string, iterations: number): Promise<string> {
const password = trimSafeWord(input)
const saltSource = fromBase64Url(saltB64)
const salt = new Uint8Array(saltSource.length)
salt.set(saltSource)
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits'],
)
const bits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
hash: 'SHA-256',
salt,
iterations,
},
keyMaterial,
256,
)
return toBase64Url(new Uint8Array(bits))
}
export async function createSafeWordSettings(
safeWord: string,
iterations = DEFAULT_SAFEWORD_ITERATIONS,
): Promise<SafeWordSettings> {
const salt = crypto.getRandomValues(new Uint8Array(16))
const saltB64 = toBase64Url(salt)
const hashB64 = await deriveSafeWordHash(safeWord, saltB64, iterations)
return { saltB64, hashB64, iterations }
}
export async function verifySafeWord(
candidate: string,
safeWord: SafeWordSettings,
): Promise<boolean> {
const expected = fromBase64Url(safeWord.hashB64)
const actual = fromBase64Url(
await deriveSafeWordHash(candidate, safeWord.saltB64, safeWord.iterations),
)
if (expected.length !== actual.length) return false
let diff = 0
for (let i = 0; i < expected.length; i++) {
diff |= (expected[i] ?? 0) ^ (actual[i] ?? 0)
}
return diff === 0
}