@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.14
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/README.md +52 -47
- package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
- package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
- package/dist/index.cjs +645 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -9
- package/dist/index.d.ts +27 -9
- package/dist/index.js +644 -150
- package/dist/index.js.map +1 -1
- package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
- package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/server.cjs +2880 -1667
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +810 -169
- package/dist/server.d.ts +810 -169
- package/dist/server.js +2848 -1646
- package/dist/server.js.map +1 -1
- package/dist/svelte.cjs +2 -2
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.d.cts +2 -2
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +2 -2
- package/dist/svelte.js.map +1 -1
- package/dist/vue.d.cts +1 -1
- package/dist/vue.d.ts +1 -1
- package/package.json +7 -7
- package/src/admin/admin-api.ts +327 -0
- package/src/admin/audit-log.ts +324 -0
- package/src/admin/webhooks.ts +576 -0
- package/src/bindings/create-auth-session.ts +184 -0
- package/src/bindings/create-org-session.ts +130 -0
- package/src/client/auth-client.ts +1592 -0
- package/src/client/auth-sync.ts +213 -0
- package/src/client/device-session.ts +104 -0
- package/src/client/org-client.ts +399 -0
- package/src/client/quickstart.ts +108 -0
- package/src/client/storage.ts +94 -0
- package/src/device/device-identity.ts +330 -0
- package/src/device/device-store.ts +379 -0
- package/src/encryption/auto-lock.ts +170 -0
- package/src/encryption/database-encryption.ts +265 -0
- package/src/encryption/key-derivation.ts +149 -0
- package/src/encryption/operation-encryptor.ts +361 -0
- package/src/index.ts +132 -0
- package/src/mfa/totp.ts +826 -0
- package/src/org/org-routes.ts +758 -0
- package/src/org/org-store.ts +490 -0
- package/src/org/org-types.ts +230 -0
- package/src/passkey/passkey-client.ts +597 -0
- package/src/passkey/passkey-server.ts +779 -0
- package/src/postgres/ensure-schema.ts +65 -0
- package/src/provider/adapter.ts +246 -0
- package/src/provider/built-in/auth-routes.ts +1313 -0
- package/src/provider/built-in/email-verification.ts +303 -0
- package/src/provider/built-in/password-hash.ts +118 -0
- package/src/provider/built-in/password-reset.ts +416 -0
- package/src/provider/built-in/postgres-user-store.ts +365 -0
- package/src/provider/built-in/quickstart-server.ts +760 -0
- package/src/provider/built-in/sqlite-user-store.ts +335 -0
- package/src/provider/built-in/sync-scopes.ts +85 -0
- package/src/provider/built-in/user-store.ts +465 -0
- package/src/provider/external/clerk-adapter.ts +157 -0
- package/src/provider/external/external-jwt-provider.ts +491 -0
- package/src/provider/external/supabase-adapter.ts +163 -0
- package/src/provider/oauth/linked-identity-store.ts +108 -0
- package/src/provider/oauth/oauth-flow.ts +550 -0
- package/src/provider/oauth/oauth-types.ts +184 -0
- package/src/provider/oauth/postgres-oauth-store.ts +296 -0
- package/src/provider/oauth/sqlite-oauth-store.ts +285 -0
- package/src/rbac/rbac-engine.ts +323 -0
- package/src/rbac/rbac-types.ts +210 -0
- package/src/rbac/scope-resolver.ts +140 -0
- package/src/react/AuthProvider.tsx +97 -0
- package/src/react/OrgProvider.tsx +41 -0
- package/src/react/auth-context.ts +26 -0
- package/src/react/hooks.ts +110 -0
- package/src/react/org-hooks.ts +214 -0
- package/src/react.ts +26 -0
- package/src/server.ts +338 -0
- package/src/session/session.ts +401 -0
- package/src/svelte/auth-context.ts +50 -0
- package/src/svelte/org-context.ts +32 -0
- package/src/svelte/org-hooks.ts +201 -0
- package/src/svelte/use-auth.ts +115 -0
- package/src/svelte.ts +25 -0
- package/src/tokens/encrypted-token-store.ts +360 -0
- package/src/tokens/jwt.ts +236 -0
- package/src/tokens/postgres-token-revocation-store.ts +140 -0
- package/src/tokens/sqlite-token-revocation-store.ts +121 -0
- package/src/tokens/token-manager.ts +821 -0
- package/src/tokens/token-store.ts +192 -0
- package/src/types.ts +394 -0
- package/src/vue/auth-context.ts +10 -0
- package/src/vue/auth-provider-types.ts +5 -0
- package/src/vue/auth-provider.ts +76 -0
- package/src/vue/org-hooks.ts +193 -0
- package/src/vue/org-provider.ts +49 -0
- package/src/vue/use-auth.ts +139 -0
- package/src/vue.ts +10 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { KoraError } from '@korajs/core'
|
|
2
|
+
import type { UserStore } from './user-store'
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Types
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A pending email verification token.
|
|
10
|
+
*/
|
|
11
|
+
export interface EmailVerificationToken {
|
|
12
|
+
/** Cryptographically random single-use token */
|
|
13
|
+
token: string
|
|
14
|
+
/** User ID the token was generated for */
|
|
15
|
+
userId: string
|
|
16
|
+
/** Email being verified */
|
|
17
|
+
email: string
|
|
18
|
+
/** When the token was created (ms since epoch) */
|
|
19
|
+
createdAt: number
|
|
20
|
+
/** When the token expires (ms since epoch) */
|
|
21
|
+
expiresAt: number
|
|
22
|
+
/** Whether the token has been consumed */
|
|
23
|
+
consumed: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Persistence interface for email verification tokens.
|
|
28
|
+
*/
|
|
29
|
+
export interface EmailVerificationStore {
|
|
30
|
+
/** Store a verification token. */
|
|
31
|
+
store(token: EmailVerificationToken): Promise<void>
|
|
32
|
+
|
|
33
|
+
/** Look up a token. Returns null if not found. */
|
|
34
|
+
get(token: string): Promise<EmailVerificationToken | null>
|
|
35
|
+
|
|
36
|
+
/** Mark a token as consumed. */
|
|
37
|
+
consume(token: string): Promise<void>
|
|
38
|
+
|
|
39
|
+
/** Count active (non-consumed, non-expired) tokens for a user. */
|
|
40
|
+
countActiveForUser(userId: string): Promise<number>
|
|
41
|
+
|
|
42
|
+
/** Remove expired tokens. */
|
|
43
|
+
cleanExpired(): Promise<number>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for the email verification flow.
|
|
48
|
+
*/
|
|
49
|
+
export interface EmailVerificationConfig {
|
|
50
|
+
/** User store for looking up users */
|
|
51
|
+
userStore: UserStore
|
|
52
|
+
/** Store for verification tokens. Defaults to InMemoryEmailVerificationStore. */
|
|
53
|
+
verificationStore?: EmailVerificationStore
|
|
54
|
+
/** Token TTL in milliseconds. Defaults to 24 hours. */
|
|
55
|
+
tokenTtlMs?: number
|
|
56
|
+
/** Max verification requests per user per TTL window. Defaults to 3. */
|
|
57
|
+
maxRequestsPerUser?: number
|
|
58
|
+
/**
|
|
59
|
+
* Callback invoked when a verification email should be sent.
|
|
60
|
+
* The developer must implement email sending.
|
|
61
|
+
* If not provided, the token is returned in the route response (development mode).
|
|
62
|
+
*/
|
|
63
|
+
onVerificationRequired?: (email: string, token: string, expiresAt: number) => void | Promise<void>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Errors
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
export class EmailVerificationError extends KoraError {
|
|
71
|
+
constructor(message: string, code: string, context?: Record<string, unknown>) {
|
|
72
|
+
super(message, code, context)
|
|
73
|
+
this.name = 'EmailVerificationError'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class VerificationTokenExpiredError extends EmailVerificationError {
|
|
78
|
+
constructor() {
|
|
79
|
+
super('Email verification token has expired.', 'VERIFICATION_TOKEN_EXPIRED')
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class VerificationTokenNotFoundError extends EmailVerificationError {
|
|
84
|
+
constructor() {
|
|
85
|
+
super('Email verification token not found or already used.', 'VERIFICATION_TOKEN_NOT_FOUND')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// InMemoryEmailVerificationStore
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
export class InMemoryEmailVerificationStore implements EmailVerificationStore {
|
|
94
|
+
private tokens = new Map<string, EmailVerificationToken>()
|
|
95
|
+
|
|
96
|
+
async store(token: EmailVerificationToken): Promise<void> {
|
|
97
|
+
this.tokens.set(token.token, token)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async get(token: string): Promise<EmailVerificationToken | null> {
|
|
101
|
+
return this.tokens.get(token) ?? null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async consume(token: string): Promise<void> {
|
|
105
|
+
const entry = this.tokens.get(token)
|
|
106
|
+
if (entry) {
|
|
107
|
+
this.tokens.set(token, { ...entry, consumed: true })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async countActiveForUser(userId: string): Promise<number> {
|
|
112
|
+
const now = Date.now()
|
|
113
|
+
let count = 0
|
|
114
|
+
for (const token of this.tokens.values()) {
|
|
115
|
+
if (token.userId === userId && !token.consumed && now < token.expiresAt) {
|
|
116
|
+
count++
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return count
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async cleanExpired(): Promise<number> {
|
|
123
|
+
const now = Date.now()
|
|
124
|
+
let count = 0
|
|
125
|
+
for (const [key, token] of this.tokens) {
|
|
126
|
+
if (now > token.expiresAt) {
|
|
127
|
+
this.tokens.delete(key)
|
|
128
|
+
count++
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return count
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// EmailVerificationManager
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
/** Default TTL: 24 hours */
|
|
140
|
+
const DEFAULT_TOKEN_TTL_MS = 24 * 60 * 60 * 1000
|
|
141
|
+
|
|
142
|
+
/** Default max requests per user per TTL window */
|
|
143
|
+
const DEFAULT_MAX_REQUESTS = 3
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Manages the email verification flow.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* const verifier = new EmailVerificationManager({
|
|
151
|
+
* userStore,
|
|
152
|
+
* onVerificationRequired: async (email, token, expiresAt) => {
|
|
153
|
+
* await sendEmail(email, `Verify: https://app.com/verify?token=${token}`)
|
|
154
|
+
* },
|
|
155
|
+
* })
|
|
156
|
+
*
|
|
157
|
+
* // Send verification email
|
|
158
|
+
* await verifier.sendVerification('user-1', 'user@example.com')
|
|
159
|
+
*
|
|
160
|
+
* // Verify email
|
|
161
|
+
* await verifier.verifyEmail(token)
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export class EmailVerificationManager {
|
|
165
|
+
private readonly userStore: UserStore
|
|
166
|
+
private readonly verificationStore: EmailVerificationStore
|
|
167
|
+
private readonly tokenTtlMs: number
|
|
168
|
+
private readonly maxRequestsPerUser: number
|
|
169
|
+
private readonly onVerificationRequired?: (
|
|
170
|
+
email: string,
|
|
171
|
+
token: string,
|
|
172
|
+
expiresAt: number,
|
|
173
|
+
) => void | Promise<void>
|
|
174
|
+
|
|
175
|
+
constructor(config: EmailVerificationConfig) {
|
|
176
|
+
this.userStore = config.userStore
|
|
177
|
+
this.verificationStore = config.verificationStore ?? new InMemoryEmailVerificationStore()
|
|
178
|
+
this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS
|
|
179
|
+
this.maxRequestsPerUser = config.maxRequestsPerUser ?? DEFAULT_MAX_REQUESTS
|
|
180
|
+
this.onVerificationRequired = config.onVerificationRequired
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Send a verification email for a user.
|
|
185
|
+
* Rate-limited to prevent abuse.
|
|
186
|
+
*/
|
|
187
|
+
async sendVerification(
|
|
188
|
+
userId: string,
|
|
189
|
+
email: string,
|
|
190
|
+
): Promise<{
|
|
191
|
+
status: number
|
|
192
|
+
body: { data: { message: string; token?: string } } | { error: string }
|
|
193
|
+
}> {
|
|
194
|
+
const normalizedEmail = email.toLowerCase().trim()
|
|
195
|
+
|
|
196
|
+
// Rate limit
|
|
197
|
+
const activeCount = await this.verificationStore.countActiveForUser(userId)
|
|
198
|
+
if (activeCount >= this.maxRequestsPerUser) {
|
|
199
|
+
return {
|
|
200
|
+
status: 429,
|
|
201
|
+
body: { error: 'Too many verification requests. Please try again later.' },
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Generate token
|
|
206
|
+
const token = generateSecureToken()
|
|
207
|
+
const now = Date.now()
|
|
208
|
+
const verificationToken: EmailVerificationToken = {
|
|
209
|
+
token,
|
|
210
|
+
userId,
|
|
211
|
+
email: normalizedEmail,
|
|
212
|
+
createdAt: now,
|
|
213
|
+
expiresAt: now + this.tokenTtlMs,
|
|
214
|
+
consumed: false,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await this.verificationStore.store(verificationToken)
|
|
218
|
+
|
|
219
|
+
// Invoke callback
|
|
220
|
+
if (this.onVerificationRequired) {
|
|
221
|
+
try {
|
|
222
|
+
await this.onVerificationRequired(normalizedEmail, token, verificationToken.expiresAt)
|
|
223
|
+
} catch {
|
|
224
|
+
// Don't fail if callback errors
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// In development mode (no callback), return the token
|
|
229
|
+
const responseData: { message: string; token?: string } = {
|
|
230
|
+
message: 'Verification email sent.',
|
|
231
|
+
}
|
|
232
|
+
if (!this.onVerificationRequired) {
|
|
233
|
+
responseData.token = token
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { status: 200, body: { data: responseData } }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Verify an email using a verification token.
|
|
241
|
+
*/
|
|
242
|
+
async verifyEmail(token: string): Promise<{
|
|
243
|
+
status: number
|
|
244
|
+
body: { data: { message: string; userId: string; email: string } } | { error: string }
|
|
245
|
+
}> {
|
|
246
|
+
const verificationToken = await this.verificationStore.get(token)
|
|
247
|
+
if (!verificationToken || verificationToken.consumed) {
|
|
248
|
+
return { status: 404, body: { error: 'Verification token not found or already used.' } }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (Date.now() > verificationToken.expiresAt) {
|
|
252
|
+
await this.verificationStore.consume(token)
|
|
253
|
+
return { status: 410, body: { error: 'Verification token has expired.' } }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Consume token
|
|
257
|
+
await this.verificationStore.consume(token)
|
|
258
|
+
|
|
259
|
+
// Mark user's email as verified
|
|
260
|
+
await this.userStore.setEmailVerified(verificationToken.userId, true)
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
status: 200,
|
|
264
|
+
body: {
|
|
265
|
+
data: {
|
|
266
|
+
message: 'Email verified successfully.',
|
|
267
|
+
userId: verificationToken.userId,
|
|
268
|
+
email: verificationToken.email,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Resend verification email for a user.
|
|
276
|
+
* Delegates to sendVerification with rate limiting.
|
|
277
|
+
*/
|
|
278
|
+
async resendVerification(userId: string): Promise<{
|
|
279
|
+
status: number
|
|
280
|
+
body: { data: { message: string; token?: string } } | { error: string }
|
|
281
|
+
}> {
|
|
282
|
+
const user = await this.userStore.findById(userId)
|
|
283
|
+
if (!user) {
|
|
284
|
+
return { status: 404, body: { error: 'User not found.' } }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return this.sendVerification(userId, user.email)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Helpers
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
function generateSecureToken(): string {
|
|
296
|
+
const bytes = new Uint8Array(32)
|
|
297
|
+
globalThis.crypto.getRandomValues(bytes)
|
|
298
|
+
let binary = ''
|
|
299
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
300
|
+
binary += String.fromCharCode(bytes[i] as number)
|
|
301
|
+
}
|
|
302
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
303
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { pbkdf2, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/** Number of PBKDF2 iterations. 600,000 per OWASP 2023 recommendations for SHA-512. */
|
|
4
|
+
const PBKDF2_ITERATIONS = 600_000
|
|
5
|
+
|
|
6
|
+
/** Digest algorithm used by PBKDF2. */
|
|
7
|
+
const PBKDF2_DIGEST = 'sha512'
|
|
8
|
+
|
|
9
|
+
/** Length of the derived key in bytes. */
|
|
10
|
+
const KEY_LENGTH = 64
|
|
11
|
+
|
|
12
|
+
/** Length of the random salt in bytes. */
|
|
13
|
+
const SALT_LENGTH = 32
|
|
14
|
+
|
|
15
|
+
interface HashResult {
|
|
16
|
+
/** Hex-encoded PBKDF2 derived key. */
|
|
17
|
+
hash: string
|
|
18
|
+
/** Hex-encoded random salt used during hashing. */
|
|
19
|
+
salt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hashes a password using PBKDF2 with a cryptographically random salt.
|
|
24
|
+
*
|
|
25
|
+
* Uses SHA-512 with 600,000 iterations and a 32-byte random salt,
|
|
26
|
+
* producing a 64-byte derived key. Both the hash and salt are returned
|
|
27
|
+
* as hex-encoded strings for storage.
|
|
28
|
+
*
|
|
29
|
+
* @param password - The plaintext password to hash
|
|
30
|
+
* @returns The hex-encoded hash and salt
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const { hash, salt } = await hashPassword('my-secret-password')
|
|
35
|
+
* // Store hash and salt in the database
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function hashPassword(password: string): Promise<HashResult> {
|
|
39
|
+
const salt = randomBytes(SALT_LENGTH)
|
|
40
|
+
|
|
41
|
+
const derivedKey = await pbkdf2Async(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
hash: derivedKey.toString('hex'),
|
|
45
|
+
salt: salt.toString('hex'),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verifies a plaintext password against a stored hash and salt using
|
|
51
|
+
* timing-safe comparison to prevent timing attacks.
|
|
52
|
+
*
|
|
53
|
+
* Re-derives the key from the password and salt using the same PBKDF2
|
|
54
|
+
* parameters, then compares the result against the stored hash using
|
|
55
|
+
* `crypto.timingSafeEqual` to avoid leaking information through
|
|
56
|
+
* response timing.
|
|
57
|
+
*
|
|
58
|
+
* @param password - The plaintext password to verify
|
|
59
|
+
* @param hash - The hex-encoded hash to compare against
|
|
60
|
+
* @param salt - The hex-encoded salt that was used to produce the hash
|
|
61
|
+
* @returns `true` if the password matches, `false` otherwise
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const isValid = await verifyPassword('my-secret-password', storedHash, storedSalt)
|
|
66
|
+
* if (isValid) {
|
|
67
|
+
* // Grant access
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export async function verifyPassword(
|
|
72
|
+
password: string,
|
|
73
|
+
hash: string,
|
|
74
|
+
salt: string,
|
|
75
|
+
): Promise<boolean> {
|
|
76
|
+
const saltBuffer = Buffer.from(salt, 'hex')
|
|
77
|
+
|
|
78
|
+
const derivedKey = await pbkdf2Async(
|
|
79
|
+
password,
|
|
80
|
+
saltBuffer,
|
|
81
|
+
PBKDF2_ITERATIONS,
|
|
82
|
+
KEY_LENGTH,
|
|
83
|
+
PBKDF2_DIGEST,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const hashBuffer = Buffer.from(hash, 'hex')
|
|
87
|
+
|
|
88
|
+
// Both buffers must be the same length for timingSafeEqual.
|
|
89
|
+
// If the stored hash has an unexpected length, reject rather than throw.
|
|
90
|
+
if (derivedKey.length !== hashBuffer.length) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return timingSafeEqual(derivedKey, hashBuffer)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Promisified wrapper around Node.js `crypto.pbkdf2`.
|
|
99
|
+
* The callback-based API delegates hashing to libuv's thread pool,
|
|
100
|
+
* avoiding blocking the event loop during the 600,000 iterations.
|
|
101
|
+
*/
|
|
102
|
+
function pbkdf2Async(
|
|
103
|
+
password: string,
|
|
104
|
+
salt: Buffer,
|
|
105
|
+
iterations: number,
|
|
106
|
+
keyLength: number,
|
|
107
|
+
digest: string,
|
|
108
|
+
): Promise<Buffer> {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
pbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {
|
|
111
|
+
if (err) {
|
|
112
|
+
reject(err)
|
|
113
|
+
} else {
|
|
114
|
+
resolve(derivedKey)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
}
|