@furystack/security 7.0.9 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +9 -3
  3. package/esm/crypto-password-hasher.d.ts +10 -12
  4. package/esm/crypto-password-hasher.d.ts.map +1 -1
  5. package/esm/crypto-password-hasher.js +43 -36
  6. package/esm/crypto-password-hasher.js.map +1 -1
  7. package/esm/helpers.d.ts +20 -7
  8. package/esm/helpers.d.ts.map +1 -1
  9. package/esm/helpers.js +27 -13
  10. package/esm/helpers.js.map +1 -1
  11. package/esm/index.d.ts +3 -1
  12. package/esm/index.d.ts.map +1 -1
  13. package/esm/index.js +3 -1
  14. package/esm/index.js.map +1 -1
  15. package/esm/password-authenticator.d.ts +26 -15
  16. package/esm/password-authenticator.d.ts.map +1 -1
  17. package/esm/password-authenticator.js +73 -109
  18. package/esm/password-authenticator.js.map +1 -1
  19. package/esm/password-authenticator.spec.js +122 -212
  20. package/esm/password-authenticator.spec.js.map +1 -1
  21. package/esm/password-credential-store.d.ts +50 -0
  22. package/esm/password-credential-store.d.ts.map +1 -0
  23. package/esm/password-credential-store.js +73 -0
  24. package/esm/password-credential-store.js.map +1 -0
  25. package/esm/password-credential-store.spec.d.ts +2 -0
  26. package/esm/password-credential-store.spec.d.ts.map +1 -0
  27. package/esm/password-credential-store.spec.js +64 -0
  28. package/esm/password-credential-store.spec.js.map +1 -0
  29. package/esm/password-hasher.d.ts +21 -3
  30. package/esm/password-hasher.d.ts.map +1 -1
  31. package/esm/password-hasher.js +1 -19
  32. package/esm/password-hasher.js.map +1 -1
  33. package/esm/security-policy-manager.d.ts +46 -14
  34. package/esm/security-policy-manager.d.ts.map +1 -1
  35. package/esm/security-policy-manager.js +48 -52
  36. package/esm/security-policy-manager.js.map +1 -1
  37. package/esm/security-policy-manager.spec.js +58 -7
  38. package/esm/security-policy-manager.spec.js.map +1 -1
  39. package/esm/security-policy.d.ts +35 -9
  40. package/esm/security-policy.d.ts.map +1 -1
  41. package/esm/security-policy.js +23 -33
  42. package/esm/security-policy.js.map +1 -1
  43. package/package.json +5 -5
  44. package/src/crypto-password-hasher.ts +46 -39
  45. package/src/helpers.ts +27 -15
  46. package/src/index.ts +3 -2
  47. package/src/password-authenticator.spec.ts +134 -254
  48. package/src/password-authenticator.ts +92 -86
  49. package/src/password-credential-store.spec.ts +77 -0
  50. package/src/password-credential-store.ts +79 -0
  51. package/src/password-hasher.ts +21 -9
  52. package/src/security-policy-manager.spec.ts +63 -7
  53. package/src/security-policy-manager.ts +91 -42
  54. package/src/security-policy.ts +50 -18
@@ -1,44 +1,51 @@
1
- import { Injectable } from '@furystack/inject'
1
+ import { defineService, type Token } from '@furystack/inject'
2
+ import { pbkdf2, randomBytes } from 'crypto'
3
+ import type { PasswordCheckResult } from './models/index.js'
2
4
  import type { PasswordHasher } from './password-hasher.js'
3
- import { randomBytes, pbkdf2 } from 'crypto'
4
- import type { PasswordCheckResult, PasswordCredential } from './models/index.js'
5
5
 
6
- @Injectable({ lifetime: 'singleton' })
7
- export class CryptoPasswordHasher implements PasswordHasher {
8
- private readonly iterations = 1000
9
- private readonly keylen = 64
6
+ const ITERATIONS = 1000
7
+ const KEY_LENGTH = 64
8
+ const SALT_BYTES = 16
10
9
 
11
- public async verifyCredential(
12
- password: string,
13
- { passwordHash, salt }: PasswordCredential,
14
- ): Promise<PasswordCheckResult> {
15
- const newHash = await new Promise((resolve, reject) =>
16
- pbkdf2(password, salt, this.iterations, this.keylen, 'sha512', (err, key) =>
17
- err ? reject(err) : resolve(key.toString('hex')),
18
- ),
19
- )
20
- if (newHash === passwordHash) {
10
+ const deriveHash = (password: string, salt: string): Promise<string> =>
11
+ new Promise<string>((resolve, reject) => {
12
+ pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, 'sha512', (err, key) => {
13
+ if (err) {
14
+ reject(err)
15
+ return
16
+ }
17
+ resolve(key.toString('hex'))
18
+ })
19
+ })
20
+
21
+ /**
22
+ * Default PBKDF2-based {@link PasswordHasher} implementation. Uses SHA-512
23
+ * with 1000 iterations and a 64-byte derived key length.
24
+ *
25
+ * Swap this token in {@link SecurityPolicy.hasher} to replace the hashing
26
+ * algorithm — for example with a stronger Argon2 implementation or a
27
+ * delegated hashing service.
28
+ */
29
+ export const CryptoPasswordHasher: Token<PasswordHasher, 'singleton'> = defineService({
30
+ name: 'furystack/security/CryptoPasswordHasher',
31
+ lifetime: 'singleton',
32
+ factory: (): PasswordHasher => ({
33
+ verifyCredential: async (password, { passwordHash, salt }): Promise<PasswordCheckResult> => {
34
+ const candidate = await deriveHash(password, salt)
35
+ if (candidate === passwordHash) {
36
+ return { isValid: true }
37
+ }
38
+ return { isValid: false, reason: 'badUsernameOrPassword' }
39
+ },
40
+ createCredential: async (userName, password) => {
41
+ const salt = randomBytes(SALT_BYTES).toString('hex')
42
+ const passwordHash = await deriveHash(password, salt)
21
43
  return {
22
- isValid: true,
44
+ userName,
45
+ salt,
46
+ passwordHash,
47
+ creationDate: new Date().toISOString(),
23
48
  }
24
- }
25
- return {
26
- isValid: false,
27
- reason: 'badUsernameOrPassword',
28
- }
29
- }
30
- public async createCredential(userName: string, password: string) {
31
- const salt = randomBytes(16).toString('hex')
32
- const passwordHash = await new Promise<string>((resolve, reject) =>
33
- pbkdf2(password, salt, this.iterations, this.keylen, 'sha512', (err, key) =>
34
- err ? reject(err) : resolve(key.toString('hex')),
35
- ),
36
- )
37
- return {
38
- userName,
39
- salt,
40
- passwordHash,
41
- creationDate: new Date().toISOString(),
42
- }
43
- }
44
- }
49
+ },
50
+ }),
51
+ })
package/src/helpers.ts CHANGED
@@ -1,22 +1,34 @@
1
1
  import type { Injector } from '@furystack/inject'
2
- import { SecurityPolicy } from './security-policy.js'
2
+ import { PasswordAuthenticator } from './password-authenticator.js'
3
+ import { SecurityPolicyManager } from './security-policy-manager.js'
4
+ import { defaultSecurityPolicy, SecurityPolicy } from './security-policy.js'
3
5
 
4
6
  /**
5
- * Sets up the @furystack/security with the provided settings.
7
+ * Rebinds the {@link SecurityPolicy} on the given injector with the default
8
+ * policy merged with `overrides`.
6
9
  *
7
- * **Prerequisite:** DataSets for `PasswordCredential` and `PasswordResetToken` must be registered
8
- * via `getRepository(injector).createDataSet()` before `PasswordAuthenticator` is instantiated,
9
- * as it resolves these DataSets through `@Injected`.
10
+ * Apply this at application setup before resolving any service that reads
11
+ * the policy — {@link PasswordAuthenticator}, {@link SecurityPolicyManager}
12
+ * or direct `injector.get(SecurityPolicy)` callers. Because those services
13
+ * capture the policy at resolve time, calling `usePasswordPolicy` after
14
+ * they have been resolved has no effect unless you also invalidate them.
10
15
  *
11
- * @param injector The Injector instance
12
- * @param policy The security policy to use
16
+ * **Prerequisite:** bind persistent implementations of
17
+ * `PasswordCredentialStore` and `PasswordResetTokenStore` before resolving
18
+ * {@link PasswordAuthenticator}. Their default factories throw on purpose
19
+ * so password data is never silently kept in-memory.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * usePasswordPolicy(injector, {
24
+ * passwordExpirationDays: 90,
25
+ * passwordComplexityRules: [createMinLengthComplexityRule(8)],
26
+ * })
27
+ * ```
13
28
  */
14
- export const usePasswordPolicy = (injector: Injector, policy?: Partial<SecurityPolicy>) => {
15
- const plainPolicy = new SecurityPolicy()
16
-
17
- if (policy) {
18
- Object.assign(plainPolicy, policy)
19
- }
20
-
21
- injector.setExplicitInstance(plainPolicy, SecurityPolicy)
29
+ export const usePasswordPolicy = (injector: Injector, overrides?: Partial<SecurityPolicy>): void => {
30
+ injector.bind(SecurityPolicy, () => ({ ...defaultSecurityPolicy(), ...overrides }))
31
+ injector.invalidate(SecurityPolicy)
32
+ injector.invalidate(SecurityPolicyManager)
33
+ injector.invalidate(PasswordAuthenticator)
22
34
  }
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export * from './crypto-password-hasher.js'
2
2
  export * from './errors/index.js'
3
+ export * from './helpers.js'
3
4
  export * from './models/index.js'
4
5
  export * from './password-authenticator.js'
5
6
  export * from './password-complexity-rules/index.js'
7
+ export * from './password-credential-store.js'
6
8
  export * from './password-hasher.js'
7
9
  export * from './security-policy-manager.js'
8
-
9
- export * from './helpers.js'
10
+ export * from './security-policy.js'