@yielded/crypto 0.1.0-beta.30 → 0.1.0-beta.32

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 (46) hide show
  1. package/README.md +8 -9
  2. package/THIRD_PARTY_NOTICES.md +3 -0
  3. package/dist/Kdf.d.mts +16 -2
  4. package/dist/Kdf.mjs +21 -3
  5. package/dist/Kdf.mjs.map +1 -1
  6. package/dist/Portable.d.mts +2 -1
  7. package/dist/Portable.mjs +3 -1
  8. package/dist/Portable.mjs.map +1 -1
  9. package/dist/WebCrypto.mjs +2 -1
  10. package/dist/WebCrypto.mjs.map +1 -1
  11. package/dist/internal/argon2-core.mjs +19 -12
  12. package/dist/internal/argon2-core.mjs.map +1 -1
  13. package/dist/internal/argon2-wasm.mjs +33 -0
  14. package/dist/internal/argon2-wasm.mjs.map +1 -0
  15. package/dist/internal/argon2.mjs +6 -5
  16. package/dist/internal/argon2.mjs.map +1 -1
  17. package/dist/internal/argon2.wasm +0 -0
  18. package/dist/internal/kdf.mjs +21 -1
  19. package/dist/internal/kdf.mjs.map +1 -1
  20. package/dist/internal/layer.mjs.map +1 -1
  21. package/dist/internal/node.d.mts +3 -2
  22. package/dist/internal/node.mjs +4 -3
  23. package/dist/internal/node.mjs.map +1 -1
  24. package/dist/internal/scrypt-backend.mjs +10 -0
  25. package/dist/internal/scrypt-backend.mjs.map +1 -0
  26. package/dist/internal/scrypt.mjs +25 -0
  27. package/dist/internal/scrypt.mjs.map +1 -0
  28. package/dist/platform-workerd.d.mts +14 -0
  29. package/dist/platform-workerd.mjs +18 -0
  30. package/dist/platform-workerd.mjs.map +1 -0
  31. package/package.json +1 -1
  32. package/src/Kdf.ts +20 -2
  33. package/src/Portable.ts +4 -1
  34. package/src/WebCrypto.ts +2 -1
  35. package/src/internal/argon2-block.c +76 -0
  36. package/src/internal/argon2-core.ts +36 -12
  37. package/src/internal/argon2-wasm.ts +59 -0
  38. package/src/internal/argon2.ts +28 -23
  39. package/src/internal/argon2.wasm +0 -0
  40. package/src/internal/kdf.ts +39 -1
  41. package/src/internal/layer.ts +4 -1
  42. package/src/internal/node.ts +6 -3
  43. package/src/internal/scrypt-backend.ts +25 -0
  44. package/src/internal/scrypt.ts +38 -0
  45. package/src/internal/wasm.d.ts +5 -0
  46. package/src/platform-workerd.ts +16 -0
@@ -1,7 +1,8 @@
1
1
  import { CryptoUnavailable, UnsupportedAlgorithm } from "../Errors.mjs";
2
2
  import { makeLayer } from "./layer.mjs";
3
+ import { layer as layer$1 } from "./scrypt.mjs";
3
4
  import { xchacha } from "./xchacha.mjs";
4
- import { Effect, Result } from "effect";
5
+ import { Effect, Layer, Result } from "effect";
5
6
  import * as crypto from "node:crypto";
6
7
  //#region src/internal/node.ts
7
8
  const argon2 = Effect.fnUntraced(function* (input) {
@@ -23,11 +24,11 @@ const argon2 = Effect.fnUntraced(function* (input) {
23
24
  if (Result.isFailure(registration)) resume(Effect.fail(registration.failure));
24
25
  }).pipe(Effect.uninterruptible);
25
26
  });
26
- /** Node-compatible WebCrypto/Argon2id with the portable XChaCha20-Poly1305 extension. */
27
+ /** Node-compatible WebCrypto and native password KDFs with portable XChaCha20-Poly1305. */
27
28
  const layer = (limits = {}) => makeLayer(crypto.webcrypto.subtle, limits, {
28
29
  argon2,
29
30
  xchacha
30
- });
31
+ }).pipe(Layer.provide(layer$1));
31
32
  //#endregion
32
33
  export { layer };
33
34
 
@@ -1 +1 @@
1
- {"version":3,"file":"node.mjs","names":[],"sources":["../../src/internal/node.ts"],"sourcesContent":["// @effect-diagnostics-next-line nodeBuiltinImport:off -- This platform adapter needs native Argon2 and WebCrypto, which Effect Crypto does not expose.\nimport * as crypto from \"node:crypto\";\n\nimport { Effect, Result } from \"effect\";\n\nimport { CryptoUnavailable, UnsupportedAlgorithm } from \"../Errors\";\nimport type { Limits } from \"../Kdf\";\nimport type { Argon2, Argon2Parameters } from \"./kdf\";\nimport { makeLayer } from \"./layer\";\nimport { xchacha } from \"./xchacha\";\n\nconst argon2: Argon2 = Effect.fnUntraced(function* (input: Argon2Parameters) {\n // Namespace import keeps this module loadable on Node versions without Argon2.\n if (typeof crypto.argon2 !== \"function\") return yield* UnsupportedAlgorithm.make({});\n\n // Node cannot cancel this work. Keep borrowed buffers and admission until the\n // callback completes, including when its caller has requested interruption.\n return yield* Effect.callback<Uint8Array, CryptoUnavailable>((resume) => {\n const registration = Result.try({\n try: () =>\n crypto.argon2(\n \"argon2id\",\n {\n message: input.password,\n nonce: input.salt,\n parallelism: input.parallelism,\n memory: input.memoryKiB,\n passes: input.passes,\n tagLength: input.length,\n secret: input.secret,\n associatedData: input.associatedData,\n },\n (error, key) =>\n resume(error === null ? Effect.succeed(key) : Effect.fail(CryptoUnavailable.make({}))),\n ),\n catch: () => CryptoUnavailable.make({}),\n });\n\n if (Result.isFailure(registration)) resume(Effect.fail(registration.failure));\n }).pipe(Effect.uninterruptible);\n});\n\n/** Node-compatible WebCrypto/Argon2id with the portable XChaCha20-Poly1305 extension. */\nexport const layer = (limits: Partial<Limits> = {}) =>\n // Node's declarations include additional key usages absent from lib.dom. The\n // standard operations used here share the WebCrypto ABI; no payload is cast.\n makeLayer(crypto.webcrypto.subtle as SubtleCrypto, limits, { argon2, xchacha });\n"],"mappings":";;;;;;AAWA,MAAM,SAAiB,OAAO,WAAW,WAAW,OAAyB;CAE3E,IAAI,OAAO,OAAO,WAAW,YAAY,OAAO,OAAO,qBAAqB,KAAK,CAAC,CAAC;CAInF,OAAO,OAAO,OAAO,UAAyC,WAAW;EACvE,MAAM,eAAe,OAAO,IAAI;GAC9B,WACE,OAAO,OACL,YACA;IACE,SAAS,MAAM;IACf,OAAO,MAAM;IACb,aAAa,MAAM;IACnB,QAAQ,MAAM;IACd,QAAQ,MAAM;IACd,WAAW,MAAM;IACjB,QAAQ,MAAM;IACd,gBAAgB,MAAM;GACxB,IACC,OAAO,QACN,OAAO,UAAU,OAAO,OAAO,QAAQ,GAAG,IAAI,OAAO,KAAK,kBAAkB,KAAK,CAAC,CAAC,CAAC,CAAC,CACzF;GACF,aAAa,kBAAkB,KAAK,CAAC,CAAC;EACxC,CAAC;EAED,IAAI,OAAO,UAAU,YAAY,GAAG,OAAO,OAAO,KAAK,aAAa,OAAO,CAAC;CAC9E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe;AAChC,CAAC;;AAGD,MAAa,SAAS,SAA0B,CAAC,MAG/C,UAAU,OAAO,UAAU,QAAwB,QAAQ;CAAE;CAAQ;AAAQ,CAAC"}
1
+ {"version":3,"file":"node.mjs","names":["Scrypt.layer"],"sources":["../../src/internal/node.ts"],"sourcesContent":["// @effect-diagnostics-next-line nodeBuiltinImport:off -- This platform adapter needs native Argon2 and WebCrypto, which Effect Crypto does not expose.\nimport * as crypto from \"node:crypto\";\n\nimport { Effect, Layer, Result } from \"effect\";\n\nimport { CryptoUnavailable, UnsupportedAlgorithm } from \"../Errors\";\nimport type { Limits } from \"../Kdf\";\nimport type { Argon2, Argon2Parameters } from \"./kdf\";\nimport { makeLayer } from \"./layer\";\nimport * as Scrypt from \"./scrypt\";\nimport { xchacha } from \"./xchacha\";\n\nconst argon2: Argon2 = Effect.fnUntraced(function* (input: Argon2Parameters) {\n // Namespace import keeps this module loadable on Node versions without Argon2.\n if (typeof crypto.argon2 !== \"function\") return yield* UnsupportedAlgorithm.make({});\n\n // Node cannot cancel this work. Keep borrowed buffers and admission until the\n // callback completes, including when its caller has requested interruption.\n return yield* Effect.callback<Uint8Array, CryptoUnavailable>((resume) => {\n const registration = Result.try({\n try: () =>\n crypto.argon2(\n \"argon2id\",\n {\n message: input.password,\n nonce: input.salt,\n parallelism: input.parallelism,\n memory: input.memoryKiB,\n passes: input.passes,\n tagLength: input.length,\n secret: input.secret,\n associatedData: input.associatedData,\n },\n (error, key) =>\n resume(error === null ? Effect.succeed(key) : Effect.fail(CryptoUnavailable.make({}))),\n ),\n catch: () => CryptoUnavailable.make({}),\n });\n\n if (Result.isFailure(registration)) resume(Effect.fail(registration.failure));\n }).pipe(Effect.uninterruptible);\n});\n\n/** Node-compatible WebCrypto and native password KDFs with portable XChaCha20-Poly1305. */\nexport const layer = (limits: Partial<Limits> = {}) =>\n // Node's declarations include additional key usages absent from lib.dom. The\n // standard operations used here share the WebCrypto ABI; no payload is cast.\n makeLayer(crypto.webcrypto.subtle as SubtleCrypto, limits, { argon2, xchacha }).pipe(\n Layer.provide(Scrypt.layer),\n );\n"],"mappings":";;;;;;;AAYA,MAAM,SAAiB,OAAO,WAAW,WAAW,OAAyB;CAE3E,IAAI,OAAO,OAAO,WAAW,YAAY,OAAO,OAAO,qBAAqB,KAAK,CAAC,CAAC;CAInF,OAAO,OAAO,OAAO,UAAyC,WAAW;EACvE,MAAM,eAAe,OAAO,IAAI;GAC9B,WACE,OAAO,OACL,YACA;IACE,SAAS,MAAM;IACf,OAAO,MAAM;IACb,aAAa,MAAM;IACnB,QAAQ,MAAM;IACd,QAAQ,MAAM;IACd,WAAW,MAAM;IACjB,QAAQ,MAAM;IACd,gBAAgB,MAAM;GACxB,IACC,OAAO,QACN,OAAO,UAAU,OAAO,OAAO,QAAQ,GAAG,IAAI,OAAO,KAAK,kBAAkB,KAAK,CAAC,CAAC,CAAC,CAAC,CACzF;GACF,aAAa,kBAAkB,KAAK,CAAC,CAAC;EACxC,CAAC;EAED,IAAI,OAAO,UAAU,YAAY,GAAG,OAAO,OAAO,KAAK,aAAa,OAAO,CAAC;CAC9E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe;AAChC,CAAC;;AAGD,MAAa,SAAS,SAA0B,CAAC,MAG/C,UAAU,OAAO,UAAU,QAAwB,QAAQ;CAAE;CAAQ;AAAQ,CAAC,CAAC,CAAC,KAC9E,MAAM,QAAQA,OAAY,CAC5B"}
@@ -0,0 +1,10 @@
1
+ import { UnsupportedAlgorithm } from "../Errors.mjs";
2
+ import { Context, Effect, Layer } from "effect";
3
+ //#region src/internal/scrypt-backend.ts
4
+ /** Raw derivation selected by a platform Layer; Kdf owns validation and admission. */
5
+ var ScryptBackend = class extends Context.Service()("@yielded/crypto/internal/scrypt-backend") {};
6
+ const layerUnsupported = Layer.succeed(ScryptBackend, { derive: () => Effect.fail(UnsupportedAlgorithm.make({})) });
7
+ //#endregion
8
+ export { ScryptBackend, layerUnsupported };
9
+
10
+ //# sourceMappingURL=scrypt-backend.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrypt-backend.mjs","names":[],"sources":["../../src/internal/scrypt-backend.ts"],"sourcesContent":["import { Context, Effect, Layer } from \"effect\";\n\nimport { type OperationError, UnsupportedAlgorithm } from \"../Errors\";\n\nexport interface ScryptParameters {\n readonly password: Uint8Array<ArrayBuffer>;\n readonly salt: Uint8Array<ArrayBuffer>;\n readonly cost: number;\n readonly blockSize: number;\n readonly parallelism: number;\n readonly length: number;\n readonly maximumMemoryBytes: number;\n}\n\n/** Raw derivation selected by a platform Layer; Kdf owns validation and admission. */\nexport class ScryptBackend extends Context.Service<\n ScryptBackend,\n {\n readonly derive: (input: ScryptParameters) => Effect.Effect<Uint8Array, OperationError>;\n }\n>()(\"@yielded/crypto/internal/scrypt-backend\") {}\n\nexport const layerUnsupported = Layer.succeed(ScryptBackend, {\n derive: () => Effect.fail(UnsupportedAlgorithm.make({})),\n});\n"],"mappings":";;;;AAeA,IAAa,gBAAb,cAAmC,QAAQ,QAKzC,CAAC,CAAC,yCAAyC,CAAC,CAAC,CAAC;AAEhD,MAAa,mBAAmB,MAAM,QAAQ,eAAe,EAC3D,cAAc,OAAO,KAAK,qBAAqB,KAAK,CAAC,CAAC,CAAC,EACzD,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { CryptoUnavailable, UnsupportedAlgorithm } from "../Errors.mjs";
2
+ import { ScryptBackend } from "./scrypt-backend.mjs";
3
+ import { Effect, Layer, Result } from "effect";
4
+ import * as crypto from "node:crypto";
5
+ //#region src/internal/scrypt.ts
6
+ const derive = Effect.fnUntraced(function* (input) {
7
+ if (typeof crypto.scrypt !== "function") return yield* UnsupportedAlgorithm.make({});
8
+ return yield* Effect.callback((resume) => {
9
+ const registration = Result.try({
10
+ try: () => crypto.scrypt(input.password, input.salt, input.length, {
11
+ N: input.cost,
12
+ r: input.blockSize,
13
+ p: input.parallelism,
14
+ maxmem: input.maximumMemoryBytes
15
+ }, (error, key) => resume(error ? Effect.fail(CryptoUnavailable.make({})) : Effect.succeed(key))),
16
+ catch: () => CryptoUnavailable.make({})
17
+ });
18
+ if (Result.isFailure(registration)) resume(Effect.fail(registration.failure));
19
+ }).pipe(Effect.uninterruptible);
20
+ });
21
+ const layer = Layer.succeed(ScryptBackend, { derive });
22
+ //#endregion
23
+ export { layer };
24
+
25
+ //# sourceMappingURL=scrypt.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrypt.mjs","names":[],"sources":["../../src/internal/scrypt.ts"],"sourcesContent":["// @effect-diagnostics-next-line nodeBuiltinImport:off -- Native scrypt is not exposed by Effect Crypto or WebCrypto.\nimport * as crypto from \"node:crypto\";\n\nimport { Effect, Layer, Result } from \"effect\";\n\nimport { CryptoUnavailable, UnsupportedAlgorithm } from \"../Errors\";\nimport { ScryptBackend, type ScryptParameters } from \"./scrypt-backend\";\n\nconst derive = Effect.fnUntraced(function* (input: ScryptParameters) {\n if (typeof crypto.scrypt !== \"function\") return yield* UnsupportedAlgorithm.make({});\n\n // Native work cannot be cancelled. Keep admission and borrowed buffers until\n // its callback finishes, including when the caller requests interruption.\n return yield* Effect.callback<Uint8Array, CryptoUnavailable>((resume) => {\n const registration = Result.try({\n try: () =>\n crypto.scrypt(\n input.password,\n input.salt,\n input.length,\n {\n N: input.cost,\n r: input.blockSize,\n p: input.parallelism,\n maxmem: input.maximumMemoryBytes,\n },\n // Bun supplies undefined on success; Node supplies null.\n (error, key) =>\n resume(error ? Effect.fail(CryptoUnavailable.make({})) : Effect.succeed(key)),\n ),\n catch: () => CryptoUnavailable.make({}),\n });\n\n if (Result.isFailure(registration)) resume(Effect.fail(registration.failure));\n }).pipe(Effect.uninterruptible);\n});\n\nexport const layer = Layer.succeed(ScryptBackend, { derive });\n"],"mappings":";;;;;AAQA,MAAM,SAAS,OAAO,WAAW,WAAW,OAAyB;CACnE,IAAI,OAAO,OAAO,WAAW,YAAY,OAAO,OAAO,qBAAqB,KAAK,CAAC,CAAC;CAInF,OAAO,OAAO,OAAO,UAAyC,WAAW;EACvE,MAAM,eAAe,OAAO,IAAI;GAC9B,WACE,OAAO,OACL,MAAM,UACN,MAAM,MACN,MAAM,QACN;IACE,GAAG,MAAM;IACT,GAAG,MAAM;IACT,GAAG,MAAM;IACT,QAAQ,MAAM;GAChB,IAEC,OAAO,QACN,OAAO,QAAQ,OAAO,KAAK,kBAAkB,KAAK,CAAC,CAAC,CAAC,IAAI,OAAO,QAAQ,GAAG,CAAC,CAChF;GACF,aAAa,kBAAkB,KAAK,CAAC,CAAC;EACxC,CAAC;EAED,IAAI,OAAO,UAAU,YAAY,GAAG,OAAO,OAAO,KAAK,aAAa,OAAO,CAAC;CAC9E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe;AAChC,CAAC;AAED,MAAa,QAAQ,MAAM,QAAQ,eAAe,EAAE,OAAO,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { InvalidInput } from "./Errors.mjs";
2
+ import { Aead } from "./Aead.mjs";
3
+ import { Hmac } from "./Hmac.mjs";
4
+ import { Kdf, Limits } from "./Kdf.mjs";
5
+ import { KdfAdmission } from "./KdfAdmission.mjs";
6
+ import { Signature } from "./Signature.mjs";
7
+ import { Layer } from "effect";
8
+ //#region src/platform-workerd.d.ts
9
+ /** Workers WebCrypto, Wasm Argon2id and native scrypt.
10
+ * Bundle with Wrangler and enable Node.js compatibility.
11
+ */
12
+ export declare const layer: (subtle: SubtleCrypto, limits?: Partial<Limits>) => Layer.Layer<Aead | Hmac | Kdf | Signature, InvalidInput, KdfAdmission>;
13
+ //#endregion
14
+ //# sourceMappingURL=platform-workerd.d.mts.map
@@ -0,0 +1,18 @@
1
+ import { makeLayer } from "./internal/layer.mjs";
2
+ import { layer as layer$1 } from "./internal/scrypt.mjs";
3
+ import { xchacha } from "./internal/xchacha.mjs";
4
+ import { makeArgon2 } from "./internal/argon2-wasm.mjs";
5
+ import { Layer } from "effect";
6
+ import module from "./internal/argon2.wasm";
7
+ //#region src/platform-workerd.ts
8
+ /** Workers WebCrypto, Wasm Argon2id and native scrypt.
9
+ * Bundle with Wrangler and enable Node.js compatibility.
10
+ */
11
+ const layer = (subtle, limits = {}) => makeLayer(subtle, limits, {
12
+ argon2: makeArgon2(module),
13
+ xchacha
14
+ }).pipe(Layer.provide(layer$1));
15
+ //#endregion
16
+ export { layer };
17
+
18
+ //# sourceMappingURL=platform-workerd.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-workerd.mjs","names":["Scrypt.layer"],"sources":["../src/platform-workerd.ts"],"sourcesContent":["import { Layer } from \"effect\";\n\nimport { makeArgon2 } from \"./internal/argon2-wasm\";\nimport module from \"./internal/argon2.wasm\";\nimport { makeLayer } from \"./internal/layer\";\nimport * as Scrypt from \"./internal/scrypt\";\nimport { xchacha } from \"./internal/xchacha\";\nimport type { Limits } from \"./Kdf\";\n\n/** Workers WebCrypto, Wasm Argon2id and native scrypt.\n * Bundle with Wrangler and enable Node.js compatibility.\n */\nexport const layer = (subtle: SubtleCrypto, limits: Partial<Limits> = {}) =>\n makeLayer(subtle, limits, { argon2: makeArgon2(module), xchacha }).pipe(\n Layer.provide(Scrypt.layer),\n );\n"],"mappings":";;;;;;;;;;AAYA,MAAa,SAAS,QAAsB,SAA0B,CAAC,MACrE,UAAU,QAAQ,QAAQ;CAAE,QAAQ,WAAW,MAAM;CAAG;AAAQ,CAAC,CAAC,CAAC,KACjE,MAAM,QAAQA,OAAY,CAC5B"}
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@yielded/crypto","version":"0.1.0-beta.30","devDependencies":{"@effect/vitest":"4.0.0","@types/node":"26.1.2","effect":"4.0.0","typescript":"7.0.2","vite-plus":"0.3.3"},"peerDependencies":{"effect":"^4.0.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./Aead":{"types":"./dist/Aead.d.mts","default":"./dist/Aead.mjs"},"./Errors":{"types":"./dist/Errors.d.mts","default":"./dist/Errors.mjs"},"./Hmac":{"types":"./dist/Hmac.d.mts","default":"./dist/Hmac.mjs"},"./Kdf":{"types":"./dist/Kdf.d.mts","default":"./dist/Kdf.mjs"},"./KdfAdmission":{"types":"./dist/KdfAdmission.d.mts","default":"./dist/KdfAdmission.mjs"},"./Signature":{"types":"./dist/Signature.d.mts","default":"./dist/Signature.mjs"},"./WebCrypto":{"types":"./dist/WebCrypto.d.mts","default":"./dist/WebCrypto.mjs"},"./platform-node":{"types":"./dist/platform-node.d.mts","default":"./dist/platform-node.mjs"},"./platform-bun":{"types":"./dist/platform-bun.d.mts","default":"./dist/platform-bun.mjs"},"./Portable":{"types":"./dist/Portable.d.mts","default":"./dist/Portable.mjs"}},"description":"Effect cryptography services with explicit native and portable backends.","homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/crypto"},"files":["dist","src","THIRD_PARTY_NOTICES.md"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test"}}
1
+ {"name":"@yielded/crypto","version":"0.1.0-beta.32","devDependencies":{"@effect/vitest":"4.0.0","@types/node":"26.1.2","effect":"4.0.0","typescript":"7.0.2","vite":"npm:@voidzero-dev/vite-plus-core@1.1.0","vite-plus":"1.1.0","vitest":"5.0.3"},"peerDependencies":{"effect":"^4.0.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./Aead":{"types":"./dist/Aead.d.mts","default":"./dist/Aead.mjs"},"./Errors":{"types":"./dist/Errors.d.mts","default":"./dist/Errors.mjs"},"./Hmac":{"types":"./dist/Hmac.d.mts","default":"./dist/Hmac.mjs"},"./Kdf":{"types":"./dist/Kdf.d.mts","default":"./dist/Kdf.mjs"},"./KdfAdmission":{"types":"./dist/KdfAdmission.d.mts","default":"./dist/KdfAdmission.mjs"},"./Signature":{"types":"./dist/Signature.d.mts","default":"./dist/Signature.mjs"},"./WebCrypto":{"types":"./dist/WebCrypto.d.mts","default":"./dist/WebCrypto.mjs"},"./platform-node":{"types":"./dist/platform-node.d.mts","default":"./dist/platform-node.mjs"},"./platform-bun":{"types":"./dist/platform-bun.d.mts","default":"./dist/platform-bun.mjs"},"./platform-workerd":{"types":"./dist/platform-workerd.d.mts","default":"./dist/platform-workerd.mjs"},"./Portable":{"types":"./dist/Portable.d.mts","default":"./dist/Portable.mjs"}},"description":"Effect cryptography services with explicit native and portable backends.","homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/crypto"},"files":["dist","src","THIRD_PARTY_NOTICES.md"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","build:wasm":"clang --target=wasm32 -O3 -msimd128 -ffreestanding -fno-builtin -nostdlib -Wl,--no-entry -Wl,--import-memory -Wl,--export=argon2_block -Wl,--export=__heap_base -Wl,--strip-all src/internal/argon2-block.c -o src/internal/argon2.wasm","check":"tsc --noEmit -p tsconfig.json","test":"vp test"}}
package/src/Kdf.ts CHANGED
@@ -22,6 +22,17 @@ export const Argon2idInput = Schema.Struct({
22
22
 
23
23
  export type Argon2idInput = typeof Argon2idInput.Type;
24
24
 
25
+ export const ScryptInput = Schema.Struct({
26
+ password: secret,
27
+ salt: bytes,
28
+ cost: Schema.Int.check(Schema.isBetween({ minimum: 2, maximum: 0x80000000 })),
29
+ blockSize: uint32,
30
+ parallelism: uint32,
31
+ length: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 0x7fffffff })),
32
+ });
33
+
34
+ export type ScryptInput = typeof ScryptInput.Type;
35
+
25
36
  export const Pbkdf2Input = Schema.Struct({
26
37
  password: secret,
27
38
  salt: bytes,
@@ -42,7 +53,10 @@ export const HkdfInput = Schema.Struct({
42
53
 
43
54
  export type HkdfInput = typeof HkdfInput.Type;
44
55
 
45
- /** Resource ceilings, not a password-strength policy. Limits apply per permit. */
56
+ /** Resource ceilings, not a password-strength policy. Limits apply per permit.
57
+ * Scrypt bounds its allocation by maximumMemoryKiB and N*r*p/8 by
58
+ * maximumMemoryPasses. maximumPasses/maximumParallelism apply to Argon2id.
59
+ */
46
60
  export const Limits = Schema.Struct({
47
61
  maximumMemoryKiB: positive,
48
62
  maximumPasses: positive,
@@ -67,7 +81,8 @@ export const defaultLimits: Limits = Object.freeze({
67
81
 
68
82
  /**
69
83
  * Byte-preserving derivation; no text normalization, PHC parsing or password policy.
70
- * Argon2id uses version 19; PBKDF2 and HKDF use SHA-256. A backend captures shared
84
+ * Argon2id uses version 19; PBKDF2 and HKDF use SHA-256. Scrypt names N/r/p as
85
+ * cost/blockSize/parallelism. A backend captures shared
71
86
  * KdfAdmission when its Layer is built and retains admission until work and cleanup
72
87
  * finish, including when a native operation cannot be cancelled.
73
88
  */
@@ -77,6 +92,9 @@ export class Kdf extends Context.Service<
77
92
  readonly argon2id: (
78
93
  input: Argon2idInput,
79
94
  ) => Effect.Effect<Redacted.Redacted<Uint8Array>, OperationError | KdfBusy>;
95
+ readonly scrypt: (
96
+ input: ScryptInput,
97
+ ) => Effect.Effect<Redacted.Redacted<Uint8Array>, OperationError | KdfBusy>;
80
98
  readonly pbkdf2: (
81
99
  input: Pbkdf2Input,
82
100
  ) => Effect.Effect<Redacted.Redacted<Uint8Array>, OperationError | KdfBusy>;
package/src/Portable.ts CHANGED
@@ -1,8 +1,11 @@
1
+ import { Layer } from "effect";
2
+
1
3
  import { argon2 } from "./internal/argon2";
2
4
  import { makeLayer } from "./internal/layer";
5
+ import { layerUnsupported } from "./internal/scrypt-backend";
3
6
  import { xchacha } from "./internal/xchacha";
4
7
  import type { Limits } from "./Kdf";
5
8
 
6
9
  /** WebCrypto with owned, portable Argon2id and XChaCha20-Poly1305 implementations. */
7
10
  export const layer = (subtle: SubtleCrypto, limits: Partial<Limits> = {}) =>
8
- makeLayer(subtle, limits, { argon2, xchacha });
11
+ makeLayer(subtle, limits, { argon2, xchacha }).pipe(Layer.provide(layerUnsupported));
package/src/WebCrypto.ts CHANGED
@@ -3,11 +3,12 @@ import { Crypto, Effect, Layer, PlatformError } from "effect";
3
3
  import { Hmac } from "./Hmac";
4
4
  import { makeHmac } from "./internal/hmac";
5
5
  import { makeLayer } from "./internal/layer";
6
+ import { layerUnsupported } from "./internal/scrypt-backend";
6
7
  import type { Limits } from "./Kdf";
7
8
 
8
9
  /** Supply the host capability explicitly; no implicit global backend selection. */
9
10
  export const layer = (subtle: SubtleCrypto, limits: Partial<Limits> = {}) =>
10
- makeLayer(subtle, limits);
11
+ makeLayer(subtle, limits).pipe(Layer.provide(layerUnsupported));
11
12
 
12
13
  /** HMAC alone has no KDF admission requirement. */
13
14
  export const layerHmac = (subtle: SubtleCrypto): Layer.Layer<Hmac> =>
@@ -0,0 +1,76 @@
1
+ #include <stdint.h>
2
+ #include <wasm_simd128.h>
3
+
4
+ /* Argon2id v19 block compression; see ../../THIRD_PARTY_NOTICES.md.
5
+ * Build with vp run @yielded/crypto#build:wasm. Imports only linear memory.
6
+ */
7
+ #define INLINE static inline __attribute__((always_inline))
8
+
9
+ /* Two independent RFC 9106 BlaMka operations, with unsigned 32-bit products. */
10
+ INLINE v128_t blamka(v128_t a, v128_t b) {
11
+ v128_t low_a = wasm_i32x4_shuffle(a, a, 0, 2, 0, 2);
12
+ v128_t low_b = wasm_i32x4_shuffle(b, b, 0, 2, 0, 2);
13
+ v128_t product = wasm_u64x2_extmul_low_u32x4(low_a, low_b);
14
+ return wasm_i64x2_add(wasm_i64x2_add(a, b), wasm_i64x2_shl(product, 1));
15
+ }
16
+
17
+ INLINE void g(v128_t *a, v128_t *b, v128_t *c, v128_t *d) {
18
+ *a = blamka(*a, *b);
19
+ v128_t x = wasm_v128_xor(*d, *a);
20
+ *d = wasm_i32x4_shuffle(x, x, 1, 0, 3, 2);
21
+ *c = blamka(*c, *d);
22
+ x = wasm_v128_xor(*b, *c);
23
+ *b = wasm_i8x16_shuffle(x, x, 3, 4, 5, 6, 7, 0, 1, 2, 11, 12, 13, 14, 15, 8, 9, 10);
24
+ *a = blamka(*a, *b);
25
+ x = wasm_v128_xor(*d, *a);
26
+ *d = wasm_i8x16_shuffle(x, x, 2, 3, 4, 5, 6, 7, 0, 1, 10, 11, 12, 13, 14, 15, 8, 9);
27
+ *c = blamka(*c, *d);
28
+ x = wasm_v128_xor(*b, *c);
29
+ *b = wasm_v128_or(wasm_i64x2_shl(x, 1), wasm_u64x2_shr(x, 63));
30
+ }
31
+
32
+ INLINE void p(uint64_t *v,
33
+ unsigned a0, unsigned a1, unsigned b0, unsigned b1,
34
+ unsigned c0, unsigned c1, unsigned d0, unsigned d1) {
35
+ v128_t a = wasm_v128_load(v + a0), aa = wasm_v128_load(v + a1);
36
+ v128_t b = wasm_v128_load(v + b0), bb = wasm_v128_load(v + b1);
37
+ v128_t c = wasm_v128_load(v + c0), cc = wasm_v128_load(v + c1);
38
+ v128_t d = wasm_v128_load(v + d0), dd = wasm_v128_load(v + d1);
39
+ g(&a, &b, &c, &d);
40
+ g(&aa, &bb, &cc, &dd);
41
+ v128_t diagonal_b = wasm_i64x2_shuffle(b, bb, 1, 2);
42
+ v128_t diagonal_bb = wasm_i64x2_shuffle(bb, b, 1, 2);
43
+ v128_t diagonal_d = wasm_i64x2_shuffle(dd, d, 1, 2);
44
+ v128_t diagonal_dd = wasm_i64x2_shuffle(d, dd, 1, 2);
45
+ g(&a, &diagonal_b, &cc, &diagonal_d);
46
+ g(&aa, &diagonal_bb, &c, &diagonal_dd);
47
+ wasm_v128_store(v + a0, a);
48
+ wasm_v128_store(v + a1, aa);
49
+ wasm_v128_store(v + b0, wasm_i64x2_shuffle(diagonal_bb, diagonal_b, 1, 2));
50
+ wasm_v128_store(v + b1, wasm_i64x2_shuffle(diagonal_b, diagonal_bb, 1, 2));
51
+ wasm_v128_store(v + c0, c);
52
+ wasm_v128_store(v + c1, cc);
53
+ wasm_v128_store(v + d0, wasm_i64x2_shuffle(diagonal_d, diagonal_dd, 1, 2));
54
+ wasm_v128_store(v + d1, wasm_i64x2_shuffle(diagonal_dd, diagonal_d, 1, 2));
55
+ }
56
+
57
+ void argon2_block(uint64_t *scratch, uint32_t *memory,
58
+ unsigned x_offset, unsigned y_offset, unsigned out_offset, unsigned xor_old) {
59
+ uint64_t *x = (uint64_t *)(memory + x_offset);
60
+ uint64_t *y = (uint64_t *)(memory + y_offset);
61
+ uint64_t *out = (uint64_t *)(memory + out_offset);
62
+ for (unsigned i = 0; i < 128; i++) scratch[i] = x[i] ^ y[i];
63
+ /* Preserve R (and the previous destination on later passes) before permuting. */
64
+ if (xor_old)
65
+ for (unsigned i = 0; i < 128; i++) out[i] ^= scratch[i];
66
+ else
67
+ for (unsigned i = 0; i < 128; i++) out[i] = scratch[i];
68
+ for (unsigned i = 0; i < 128; i += 16)
69
+ p(scratch, i, i+2, i+4, i+6, i+8, i+10, i+12, i+14);
70
+ for (unsigned i = 0; i < 16; i += 2)
71
+ p(scratch, i, i+16, i+32, i+48, i+64, i+80, i+96, i+112);
72
+ for (unsigned i = 0; i < 128; i++) {
73
+ out[i] ^= scratch[i];
74
+ scratch[i] = 0;
75
+ }
76
+ }
@@ -317,16 +317,29 @@ export interface State {
317
317
  }
318
318
 
319
319
  // Allocate before absorbing secrets. Kdf has already applied Schema/resource limits.
320
- export const allocate = (input: Argon2Parameters): State => {
320
+ export const allocate = (
321
+ input: Argon2Parameters,
322
+ backing?: { readonly buffer: ArrayBuffer; readonly byteOffset: number },
323
+ ): State => {
321
324
  const blocks = 4 * input.parallelism * Math.floor(input.memoryKiB / (4 * input.parallelism));
322
325
  const laneLength = blocks / input.parallelism;
326
+ let offset = backing?.byteOffset ?? 0;
327
+
328
+ const words = (length: number) => {
329
+ if (backing === undefined) return new Uint32Array(length);
330
+ const view = new Uint32Array(backing.buffer, offset, length);
331
+
332
+ offset += view.byteLength;
333
+
334
+ return view;
335
+ };
323
336
 
324
337
  return {
325
- memory: new Uint32Array(blocks * 256),
326
- scratch: new Uint32Array(256),
327
- address: new Uint32Array(3 * 256),
328
- initial: new Uint32Array(18),
329
- final: new Uint32Array(256),
338
+ memory: words(blocks * 256),
339
+ scratch: words(256),
340
+ address: words(3 * 256),
341
+ initial: words(18),
342
+ final: words(256),
330
343
  blocks,
331
344
  laneLength,
332
345
  segmentLength: laneLength / 4,
@@ -402,7 +415,11 @@ export const initialize = (state: State, input: Argon2Parameters): void => {
402
415
  };
403
416
 
404
417
  /** Yield block boundaries to the Effect driver; all scratch belongs to this derivation. */
405
- export function* fill(state: State, input: Argon2Parameters): Generator<void, void> {
418
+ export function* fill(
419
+ state: State,
420
+ input: Argon2Parameters,
421
+ compress: typeof block = block,
422
+ ): Generator<void, void> {
406
423
  const { memory: B, scratch, address, laneLength: laneLen, segmentLength: segmentLen } = state;
407
424
  const p = input.parallelism;
408
425
 
@@ -424,8 +441,8 @@ export function* fill(state: State, input: Argon2Parameters): Generator<void, vo
424
441
 
425
442
  if (start === 2 && dataIndependent) {
426
443
  address[256 + 12]++;
427
- block(scratch, address, 256, 512, 0, false);
428
- block(scratch, address, 0, 512, 0, false);
444
+ compress(scratch, address, 256, 512, 0, false);
445
+ compress(scratch, address, 0, 512, 0, false);
429
446
  }
430
447
  let offset = lane * laneLen + s * segmentLen + start;
431
448
 
@@ -439,8 +456,8 @@ export function* fill(state: State, input: Argon2Parameters): Generator<void, vo
439
456
 
440
457
  if (addressIndex === 0) {
441
458
  address[256 + 12]++;
442
- block(scratch, address, 256, 512, 0, false);
443
- block(scratch, address, 0, 512, 0, false);
459
+ compress(scratch, address, 256, 512, 0, false);
460
+ compress(scratch, address, 0, 512, 0, false);
444
461
  }
445
462
  randL = address[2 * addressIndex];
446
463
  randH = address[2 * addressIndex + 1];
@@ -451,7 +468,14 @@ export function* fill(state: State, input: Argon2Parameters): Generator<void, vo
451
468
  const refLane = r === 0 && s === 0 ? lane : randH % p;
452
469
  const refPos = indexAlpha(r, s, laneLen, segmentLen, index, randL, refLane === lane);
453
470
 
454
- block(scratch, B, 256 * prev, 256 * (laneLen * refLane + refPos), offset * 256, needXor);
471
+ compress(
472
+ scratch,
473
+ B,
474
+ 256 * prev,
475
+ 256 * (laneLen * refLane + refPos),
476
+ offset * 256,
477
+ needXor,
478
+ );
455
479
  yield;
456
480
  }
457
481
  }
@@ -0,0 +1,59 @@
1
+ import { Effect } from "effect";
2
+
3
+ import { derive } from "./argon2";
4
+ import { allocate, type fill } from "./argon2-core";
5
+ import { nativeError } from "./common";
6
+ import type { Argon2 } from "./kdf";
7
+
8
+ /** ABI of the package's owned argon2.wasm, built from argon2-block.c. */
9
+ interface Exports extends WebAssembly.Exports {
10
+ readonly __heap_base: WebAssembly.Global;
11
+ readonly argon2_block: (
12
+ scratch: number,
13
+ memory: number,
14
+ x: number,
15
+ y: number,
16
+ out: number,
17
+ xor: number,
18
+ ) => void;
19
+ }
20
+
21
+ export const makeArgon2 =
22
+ (module: WebAssembly.Module): Argon2 =>
23
+ (input) =>
24
+ Effect.acquireUseRelease(
25
+ Effect.try({
26
+ try: () => {
27
+ // Each admitted derivation owns an instance. Two extra pages cover the
28
+ // module's 64 KiB stack, scratch, address, initial and final buffers.
29
+ const memory = new WebAssembly.Memory({
30
+ initial: 2,
31
+ maximum: Math.ceil(input.memoryKiB / 64) + 2,
32
+ });
33
+
34
+ const exports = new WebAssembly.Instance(module, { env: { memory } }).exports as Exports;
35
+ const byteOffset = Number(exports.__heap_base.value);
36
+ const block = exports.argon2_block;
37
+ const bytes = byteOffset + (input.memoryKiB + 5) * 1024 + 72;
38
+
39
+ memory.grow(Math.max(0, Math.ceil(bytes / 65536) - 2));
40
+ const state = allocate(input, { buffer: memory.buffer, byteOffset });
41
+
42
+ const compress: NonNullable<Parameters<typeof fill>[2]> = (
43
+ scratch,
44
+ words,
45
+ x,
46
+ y,
47
+ out,
48
+ xor,
49
+ ) => block(scratch.byteOffset, words.byteOffset, x, y, out, Number(xor));
50
+
51
+ return { memory, state, compress };
52
+ },
53
+ catch: nativeError,
54
+ }),
55
+ ({ state, compress }) => derive(input, state, compress),
56
+ // Clear the entire linear memory, including any compiler stack spills,
57
+ // before Kdf releases admission on completion, failure or interruption.
58
+ ({ memory }) => Effect.sync(() => new Uint8Array(memory.buffer).fill(0)),
59
+ );
@@ -1,32 +1,37 @@
1
1
  import { Clock, Effect } from "effect";
2
2
 
3
- import { allocate, destroy, fill, initialize, output } from "./argon2-core";
4
- import type { Argon2 } from "./kdf";
3
+ import { allocate, destroy, fill, initialize, output, type State } from "./argon2-core";
4
+ import type { Argon2, Argon2Parameters } from "./kdf";
5
+
6
+ export const derive = Effect.fnUntraced(function* (
7
+ input: Argon2Parameters,
8
+ state: State,
9
+ compress?: Parameters<typeof fill>[2],
10
+ ) {
11
+ const clock = yield* Clock.Clock;
12
+
13
+ initialize(state, input);
14
+ const blocks = fill(state, input, compress);
15
+
16
+ for (;;) {
17
+ const started = clock.monotonicTimeNanosUnsafe();
18
+
19
+ // Workers freeze clocks during CPU work. Cap each slice as well so
20
+ // interruption and other fibers still get a turn on those runtimes.
21
+ for (let processed = 0; processed < 2048; processed += 256) {
22
+ for (let batch = 0; batch < 256; batch++) {
23
+ if (blocks.next().done) return output(state, input);
24
+ }
25
+ if (clock.monotonicTimeNanosUnsafe() - started >= 8_000_000n) break;
26
+ }
27
+ yield* Effect.yieldNow;
28
+ }
29
+ });
5
30
 
6
31
  /** Effect schedules time slices; admission remains held through buffer cleanup. */
7
32
  export const argon2: Argon2 = (input) =>
8
33
  Effect.acquireUseRelease(
9
34
  Effect.sync(() => allocate(input)),
10
- (state) =>
11
- Effect.gen(function* () {
12
- const clock = yield* Clock.Clock;
13
-
14
- initialize(state, input);
15
- const blocks = fill(state, input);
16
-
17
- for (;;) {
18
- const started = clock.monotonicTimeNanosUnsafe();
19
-
20
- // Workers freeze clocks during CPU work. Cap each slice as well so
21
- // interruption and other fibers still get a turn on those runtimes.
22
- for (let processed = 0; processed < 2048; processed += 256) {
23
- for (let batch = 0; batch < 256; batch++) {
24
- if (blocks.next().done) return output(state, input);
25
- }
26
- if (clock.monotonicTimeNanosUnsafe() - started >= 8_000_000n) break;
27
- }
28
- yield* Effect.yieldNow;
29
- }
30
- }),
35
+ (state) => derive(input, state),
31
36
  (state) => Effect.sync(() => destroy(state)),
32
37
  );
Binary file
@@ -1,9 +1,10 @@
1
1
  import { Effect, Redacted, Schema } from "effect";
2
2
 
3
3
  import { InvalidInput, type OperationError, UnsupportedAlgorithm } from "../Errors";
4
- import { Argon2idInput, HkdfInput, Kdf, type Limits, Pbkdf2Input } from "../Kdf";
4
+ import { Argon2idInput, HkdfInput, Kdf, type Limits, Pbkdf2Input, ScryptInput } from "../Kdf";
5
5
  import { KdfAdmission } from "../KdfAdmission";
6
6
  import { copy, decode, importError, nativeError, withSecret } from "./common";
7
+ import { ScryptBackend } from "./scrypt-backend";
7
8
 
8
9
  export interface Argon2Parameters {
9
10
  readonly password: Uint8Array<ArrayBuffer>;
@@ -24,6 +25,7 @@ export const makeKdf = Effect.fnUntraced(function* (
24
25
  argon2?: Argon2,
25
26
  ) {
26
27
  const admission = yield* KdfAdmission;
28
+ const scrypt = yield* ScryptBackend;
27
29
  const boundedBytes = Schema.Uint8Array.check(Schema.isMaxLength(limits.maximumInputBytes));
28
30
 
29
31
  const output = Schema.Int.check(
@@ -34,6 +36,42 @@ export const makeKdf = Effect.fnUntraced(function* (
34
36
  const outputLength = (input: number) => decode(output, input, "parameters");
35
37
 
36
38
  return Kdf.of({
39
+ scrypt: (input) =>
40
+ admission.run(
41
+ Effect.gen(function* () {
42
+ const value = yield* decode(ScryptInput, input, "parameters");
43
+
44
+ yield* outputLength(value.length);
45
+ yield* inputBytes(Redacted.value(value.password));
46
+ yield* inputBytes(value.salt);
47
+
48
+ const memoryBytes = 128 * value.blockSize * (value.cost + value.parallelism + 2);
49
+
50
+ if (
51
+ !Number.isInteger(Math.log2(value.cost)) ||
52
+ Math.log2(value.cost) >= 16 * value.blockSize ||
53
+ value.blockSize * value.parallelism >= 2 ** 30 ||
54
+ memoryBytes > limits.maximumMemoryKiB * 1024 ||
55
+ (value.cost * value.blockSize * value.parallelism) / 8 > limits.maximumMemoryPasses
56
+ )
57
+ return yield* InvalidInput.make({ reason: "parameters" });
58
+ const salt = yield* copy(value.salt);
59
+
60
+ return yield* withSecret(value.password, (password) =>
61
+ scrypt
62
+ .derive({
63
+ password,
64
+ salt,
65
+ cost: value.cost,
66
+ blockSize: value.blockSize,
67
+ parallelism: value.parallelism,
68
+ length: value.length,
69
+ maximumMemoryBytes: limits.maximumMemoryKiB * 1024,
70
+ })
71
+ .pipe(Effect.map(Redacted.make)),
72
+ );
73
+ }),
74
+ ),
37
75
  pbkdf2: (input) =>
38
76
  admission.run(
39
77
  Effect.gen(function* () {
@@ -13,7 +13,10 @@ import { makeSignature } from "./signature";
13
13
  export const makeLayer = (
14
14
  subtle: SubtleCrypto,
15
15
  input: Partial<Limits>,
16
- extensions: { readonly argon2?: Argon2; readonly xchacha?: XChaCha } = {},
16
+ extensions: {
17
+ readonly argon2?: Argon2;
18
+ readonly xchacha?: XChaCha;
19
+ } = {},
17
20
  ) => {
18
21
  const snapshot = { ...defaultLimits, ...input };
19
22
 
@@ -1,12 +1,13 @@
1
1
  // @effect-diagnostics-next-line nodeBuiltinImport:off -- This platform adapter needs native Argon2 and WebCrypto, which Effect Crypto does not expose.
2
2
  import * as crypto from "node:crypto";
3
3
 
4
- import { Effect, Result } from "effect";
4
+ import { Effect, Layer, Result } from "effect";
5
5
 
6
6
  import { CryptoUnavailable, UnsupportedAlgorithm } from "../Errors";
7
7
  import type { Limits } from "../Kdf";
8
8
  import type { Argon2, Argon2Parameters } from "./kdf";
9
9
  import { makeLayer } from "./layer";
10
+ import * as Scrypt from "./scrypt";
10
11
  import { xchacha } from "./xchacha";
11
12
 
12
13
  const argon2: Argon2 = Effect.fnUntraced(function* (input: Argon2Parameters) {
@@ -40,8 +41,10 @@ const argon2: Argon2 = Effect.fnUntraced(function* (input: Argon2Parameters) {
40
41
  }).pipe(Effect.uninterruptible);
41
42
  });
42
43
 
43
- /** Node-compatible WebCrypto/Argon2id with the portable XChaCha20-Poly1305 extension. */
44
+ /** Node-compatible WebCrypto and native password KDFs with portable XChaCha20-Poly1305. */
44
45
  export const layer = (limits: Partial<Limits> = {}) =>
45
46
  // Node's declarations include additional key usages absent from lib.dom. The
46
47
  // standard operations used here share the WebCrypto ABI; no payload is cast.
47
- makeLayer(crypto.webcrypto.subtle as SubtleCrypto, limits, { argon2, xchacha });
48
+ makeLayer(crypto.webcrypto.subtle as SubtleCrypto, limits, { argon2, xchacha }).pipe(
49
+ Layer.provide(Scrypt.layer),
50
+ );
@@ -0,0 +1,25 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+
3
+ import { type OperationError, UnsupportedAlgorithm } from "../Errors";
4
+
5
+ export interface ScryptParameters {
6
+ readonly password: Uint8Array<ArrayBuffer>;
7
+ readonly salt: Uint8Array<ArrayBuffer>;
8
+ readonly cost: number;
9
+ readonly blockSize: number;
10
+ readonly parallelism: number;
11
+ readonly length: number;
12
+ readonly maximumMemoryBytes: number;
13
+ }
14
+
15
+ /** Raw derivation selected by a platform Layer; Kdf owns validation and admission. */
16
+ export class ScryptBackend extends Context.Service<
17
+ ScryptBackend,
18
+ {
19
+ readonly derive: (input: ScryptParameters) => Effect.Effect<Uint8Array, OperationError>;
20
+ }
21
+ >()("@yielded/crypto/internal/scrypt-backend") {}
22
+
23
+ export const layerUnsupported = Layer.succeed(ScryptBackend, {
24
+ derive: () => Effect.fail(UnsupportedAlgorithm.make({})),
25
+ });