@yielded/crypto 0.1.0-beta.27 → 0.1.0-beta.29

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.
@@ -1,8 +1,8 @@
1
- import { Effect, Exit, Fiber, Redacted, Schema, Scope } from "effect";
1
+ import { Effect, Redacted, Schema } from "effect";
2
2
 
3
- import { CryptoUnavailable } from "../Errors";
4
3
  import { Hmac, Input, type Key, KeyInput, VerifyInput } from "../Hmac";
5
4
  import { copy, decode, importError, nativeError, withSecret } from "./common";
5
+ import { makeScopedKey } from "./scoped-key";
6
6
 
7
7
  const keyBytes = Schema.Uint8Array.check(Schema.isMinLength(1));
8
8
 
@@ -12,37 +12,7 @@ export const makeHmac = (subtle: SubtleCrypto): Hmac["Service"] => {
12
12
 
13
13
  yield* decode(keyBytes, Redacted.value(value.key), "key");
14
14
 
15
- // A sequential child joins every native call before dropping the key, even
16
- // when the caller's Scope runs its other finalizers in parallel.
17
- const scope = yield* Scope.fork(yield* Effect.scope, "sequential");
18
- let imported: CryptoKey | undefined;
19
-
20
- yield* Scope.addFinalizer(
21
- scope,
22
- Effect.sync(() => {
23
- imported = undefined;
24
- }),
25
- );
26
-
27
- const available = Effect.suspend(() =>
28
- scope.state._tag === "Closed" ? Effect.fail(CryptoUnavailable.make({})) : Effect.void,
29
- );
30
-
31
- const run = <A, E>(operation: Effect.Effect<A, E>) =>
32
- Effect.acquireUseRelease(
33
- available.pipe(Effect.andThen(Effect.forkIn(operation, scope, { uninterruptible: false }))),
34
- (fiber) =>
35
- Effect.gen(function* () {
36
- const exit = yield* Fiber.await(fiber);
37
-
38
- yield* available;
39
-
40
- return yield* exit;
41
- }),
42
- Fiber.interrupt,
43
- );
44
-
45
- yield* run(
15
+ const use = yield* makeScopedKey(
46
16
  withSecret(value.key, (material) =>
47
17
  Effect.tryPromise({
48
18
  try: () =>
@@ -51,41 +21,30 @@ export const makeHmac = (subtle: SubtleCrypto): Hmac["Service"] => {
51
21
  "verify",
52
22
  ]),
53
23
  catch: importError,
54
- }).pipe(
55
- Effect.tap((key) =>
56
- Effect.sync(() => {
57
- imported = key;
58
- }),
59
- ),
60
- Effect.uninterruptible,
61
- ),
24
+ }),
62
25
  ),
63
- ).pipe(
64
- Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void)),
65
26
  );
66
27
 
67
- const use = <A>(operation: (key: CryptoKey) => Promise<A>) =>
68
- run(
69
- Effect.suspend(() => {
70
- const key = imported;
71
-
72
- return key === undefined
73
- ? Effect.fail(CryptoUnavailable.make({}))
74
- : Effect.tryPromise({ try: () => operation(key), catch: nativeError });
75
- }).pipe(Effect.uninterruptible),
76
- );
77
-
78
28
  return {
79
29
  sign: Effect.fnUntraced(function* (input) {
80
30
  const data = yield* decode(Schema.Uint8Array, input, "data").pipe(Effect.flatMap(copy));
81
31
 
82
- return new Uint8Array(yield* use((key) => subtle.sign("HMAC", key, data)));
32
+ return new Uint8Array(
33
+ yield* use((key) =>
34
+ Effect.tryPromise({ try: () => subtle.sign("HMAC", key, data), catch: nativeError }),
35
+ ),
36
+ );
83
37
  }),
84
38
  verify: Effect.fnUntraced(function* (input, signature) {
85
39
  const data = yield* decode(Schema.Uint8Array, input, "data").pipe(Effect.flatMap(copy));
86
40
  const tag = yield* decode(Schema.Uint8Array, signature, "data").pipe(Effect.flatMap(copy));
87
41
 
88
- return yield* use((key) => subtle.verify("HMAC", key, tag, data));
42
+ return yield* use((key) =>
43
+ Effect.tryPromise({
44
+ try: () => subtle.verify("HMAC", key, tag, data),
45
+ catch: nativeError,
46
+ }),
47
+ );
89
48
  }),
90
49
  } satisfies Key;
91
50
  });
@@ -0,0 +1,60 @@
1
+ import { Effect, Exit, Fiber, Scope } from "effect";
2
+
3
+ import { CryptoUnavailable, type OperationError } from "../Errors";
4
+
5
+ /** Join native calls before releasing a key, including parallel parent closure. */
6
+ export const makeScopedKey = Effect.fnUntraced(function* (
7
+ acquire: Effect.Effect<CryptoKey, OperationError>,
8
+ ) {
9
+ const scope = yield* Scope.fork(yield* Effect.scope, "sequential");
10
+ let imported: CryptoKey | undefined;
11
+
12
+ yield* Scope.addFinalizer(
13
+ scope,
14
+ Effect.sync(() => {
15
+ imported = undefined;
16
+ }),
17
+ );
18
+
19
+ const available = Effect.suspend(() =>
20
+ scope.state._tag === "Closed" ? Effect.fail(CryptoUnavailable.make({})) : Effect.void,
21
+ );
22
+
23
+ const run = <A, E>(operation: Effect.Effect<A, E>) =>
24
+ Effect.acquireUseRelease(
25
+ available.pipe(
26
+ Effect.andThen(
27
+ Effect.forkIn(operation, scope, { uninterruptible: false, startImmediately: true }),
28
+ ),
29
+ ),
30
+ (fiber) =>
31
+ Effect.gen(function* () {
32
+ const exit = yield* Fiber.await(fiber);
33
+
34
+ yield* available;
35
+
36
+ return yield* exit;
37
+ }),
38
+ Fiber.interrupt,
39
+ );
40
+
41
+ yield* run(
42
+ acquire.pipe(
43
+ Effect.tap((key) =>
44
+ Effect.sync(() => {
45
+ imported = key;
46
+ }),
47
+ ),
48
+ Effect.uninterruptible,
49
+ ),
50
+ ).pipe(Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void)));
51
+
52
+ return <A, E>(operation: (key: CryptoKey) => Effect.Effect<A, E>) =>
53
+ run(
54
+ Effect.suspend((): Effect.Effect<A, E | CryptoUnavailable> => {
55
+ const key = imported;
56
+
57
+ return key === undefined ? Effect.fail(CryptoUnavailable.make({})) : operation(key);
58
+ }).pipe(Effect.uninterruptible),
59
+ );
60
+ });
@@ -4,13 +4,18 @@ import { Base64Url } from "effect/encoding";
4
4
  import { InvalidInput } from "../Errors";
5
5
  import {
6
6
  type Algorithm,
7
+ type PrivateKey,
8
+ PrivateKeyInput,
7
9
  PrivateKeyParameters,
10
+ type PublicKey,
11
+ PublicKeyInput,
8
12
  PublicKeyParameters,
9
13
  SignInput,
10
14
  Signature,
11
15
  VerifyInput,
12
16
  } from "../Signature";
13
17
  import { copy, decode, importError, nativeError, withSecret } from "./common";
18
+ import { makeScopedKey } from "./scoped-key";
14
19
 
15
20
  const derBytes = Schema.Uint8Array.check(Schema.isMinLength(1), Schema.isMaxLength(16384));
16
21
 
@@ -121,6 +126,55 @@ export const makeSignature = (subtle: SubtleCrypto): Signature["Service"] => {
121
126
  });
122
127
 
123
128
  return Signature.of({
129
+ importPrivateKey: Effect.fnUntraced(function* (input) {
130
+ const value = yield* decode(PrivateKeyInput, input, "key");
131
+ const algorithm = value.algorithm;
132
+
133
+ const use = yield* makeScopedKey(
134
+ withSecret(value.privateKey, (material) => importKey(algorithm, "pkcs8", material)),
135
+ );
136
+
137
+ return {
138
+ sign: Effect.fnUntraced(function* (input) {
139
+ const data = yield* decode(Schema.Uint8Array, input, "data").pipe(Effect.flatMap(copy));
140
+
141
+ const result = yield* use((key) =>
142
+ Effect.tryPromise({
143
+ try: () => subtle.sign(operationAlgorithm(algorithm), key, data),
144
+ catch: nativeError,
145
+ }),
146
+ );
147
+
148
+ return new Uint8Array(result);
149
+ }),
150
+ } satisfies PrivateKey;
151
+ }),
152
+ importPublicKey: Effect.fnUntraced(function* (input) {
153
+ const value = yield* decode(PublicKeyInput, input, "key");
154
+ const algorithm = value.algorithm;
155
+ const material = yield* copy(value.publicKey);
156
+ const use = yield* makeScopedKey(importKey(algorithm, "spki", material));
157
+
158
+ return {
159
+ verify: Effect.fnUntraced(function* (input, signature) {
160
+ const data = yield* decode(Schema.Uint8Array, input, "data").pipe(Effect.flatMap(copy));
161
+
162
+ const tag = yield* decode(Schema.Uint8Array, signature, "data").pipe(
163
+ Effect.flatMap(copy),
164
+ );
165
+
166
+ return yield* use((key) => {
167
+ if ((algorithm === "ECDSA-P256-SHA256" || algorithm === "Ed25519") && tag.length !== 64)
168
+ return Effect.succeed(false);
169
+
170
+ return Effect.tryPromise({
171
+ try: () => subtle.verify(operationAlgorithm(algorithm), key, tag, data),
172
+ catch: nativeError,
173
+ });
174
+ });
175
+ }),
176
+ } satisfies PublicKey;
177
+ }),
124
178
  encodePublicKey: Effect.fnUntraced(function* (input) {
125
179
  const parameters = yield* decode(PublicKeyParameters, input, "key");
126
180
  const key = yield* importComponents(parameters, "verify");
@@ -258,6 +258,7 @@ interface State {
258
258
  readonly nonce: Uint32Array;
259
259
  readonly words: Uint32Array;
260
260
  readonly stream: Uint8Array;
261
+ readonly streamView: DataView;
261
262
  }
262
263
 
263
264
  const readWords = (data: Uint8Array, words: Uint32Array): void => {
@@ -270,11 +271,14 @@ function withState<A>(key: Uint8Array, nonce: Uint8Array, operation: (state: Sta
270
271
  const original = new Uint32Array(8);
271
272
  const extendedNonce = new Uint32Array(4);
272
273
 
274
+ const stream = new Uint8Array(64);
275
+
273
276
  const state: State = {
274
277
  key: new Uint32Array(8),
275
278
  nonce: new Uint32Array(3),
276
279
  words: new Uint32Array(16),
277
- stream: new Uint8Array(64),
280
+ stream,
281
+ streamView: new DataView(stream.buffer),
278
282
  };
279
283
 
280
284
  try {
@@ -300,7 +304,7 @@ function withState<A>(key: Uint8Array, nonce: Uint8Array, operation: (state: Sta
300
304
 
301
305
  const blockAt = (state: State, counter: number): void => {
302
306
  chachaCore(sigma, state.key, state.nonce, state.words, counter);
303
- const view = new DataView(state.stream.buffer, state.stream.byteOffset, state.stream.byteLength);
307
+ const view = state.streamView;
304
308
 
305
309
  for (let i = 0; i < 16; i++) view.setUint32(i * 4, state.words[i], true);
306
310
  };