@yielded/crypto 0.1.0-beta.28 → 0.1.0-beta.30
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 +4 -4
- package/dist/Aead.d.mts +26 -1
- package/dist/Aead.mjs +16 -1
- package/dist/Aead.mjs.map +1 -1
- package/dist/Signature.d.mts +69 -1
- package/dist/Signature.mjs +25 -5
- package/dist/Signature.mjs.map +1 -1
- package/dist/internal/aead.mjs +112 -68
- package/dist/internal/aead.mjs.map +1 -1
- package/dist/internal/argon2.mjs +8 -3
- package/dist/internal/argon2.mjs.map +1 -1
- package/dist/internal/hmac.mjs +12 -26
- package/dist/internal/hmac.mjs.map +1 -1
- package/dist/internal/scoped-key.mjs +31 -0
- package/dist/internal/scoped-key.mjs.map +1 -0
- package/dist/internal/signature.mjs +116 -2
- package/dist/internal/signature.mjs.map +1 -1
- package/dist/internal/xchacha-core.mjs +4 -2
- package/dist/internal/xchacha-core.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Aead.ts +35 -1
- package/src/Signature.ts +61 -4
- package/src/internal/aead.ts +97 -31
- package/src/internal/argon2.ts +13 -4
- package/src/internal/hmac.ts +15 -56
- package/src/internal/scoped-key.ts +60 -0
- package/src/internal/signature.ts +140 -1
- package/src/internal/xchacha-core.ts +6 -2
package/src/internal/aead.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { Effect, Redacted, Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Aead,
|
|
5
|
+
DecryptInput,
|
|
6
|
+
EncryptInput,
|
|
7
|
+
type Key,
|
|
8
|
+
KeyDecryptInput,
|
|
9
|
+
KeyEncryptInput,
|
|
10
|
+
KeyInput,
|
|
11
|
+
} from "../Aead";
|
|
4
12
|
import { AuthenticationFailed, type OperationError, UnsupportedAlgorithm } from "../Errors";
|
|
5
13
|
import { copy, decode, importError, nativeError, nativeErrorName, withSecret } from "./common";
|
|
14
|
+
import { makeScopedKey } from "./scoped-key";
|
|
6
15
|
|
|
7
|
-
interface
|
|
8
|
-
readonly key: Uint8Array<ArrayBuffer>;
|
|
16
|
+
interface CipherData {
|
|
9
17
|
readonly nonce: Uint8Array<ArrayBuffer>;
|
|
10
18
|
readonly additionalData: Uint8Array<ArrayBuffer>;
|
|
11
19
|
readonly data: Uint8Array<ArrayBuffer>;
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
interface CipherInput extends CipherData {
|
|
23
|
+
readonly key: Uint8Array<ArrayBuffer>;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export interface XChaCha {
|
|
15
27
|
readonly encrypt: (input: CipherInput) => Effect.Effect<Uint8Array, OperationError>;
|
|
16
28
|
readonly decrypt: (
|
|
@@ -19,9 +31,86 @@ export interface XChaCha {
|
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
const keyBytes = Schema.Uint8Array.check(Schema.isBetweenLength(32, 32));
|
|
34
|
+
const aesNonce = Schema.Uint8Array.check(Schema.isBetweenLength(12, 12));
|
|
35
|
+
|
|
36
|
+
export const makeAead = (subtle: SubtleCrypto, xchacha?: XChaCha): Aead["Service"] => {
|
|
37
|
+
const encryptAes = (key: CryptoKey, input: CipherData) =>
|
|
38
|
+
Effect.tryPromise({
|
|
39
|
+
try: () =>
|
|
40
|
+
subtle.encrypt(
|
|
41
|
+
{
|
|
42
|
+
name: "AES-GCM",
|
|
43
|
+
iv: input.nonce,
|
|
44
|
+
additionalData: input.additionalData,
|
|
45
|
+
tagLength: 128,
|
|
46
|
+
},
|
|
47
|
+
key,
|
|
48
|
+
input.data,
|
|
49
|
+
),
|
|
50
|
+
catch: nativeError,
|
|
51
|
+
}).pipe(Effect.map((encrypted) => new Uint8Array(encrypted)));
|
|
52
|
+
|
|
53
|
+
const decryptAes = (key: CryptoKey, input: CipherData) =>
|
|
54
|
+
Effect.tryPromise({
|
|
55
|
+
try: () =>
|
|
56
|
+
subtle.decrypt(
|
|
57
|
+
{
|
|
58
|
+
name: "AES-GCM",
|
|
59
|
+
iv: input.nonce,
|
|
60
|
+
additionalData: input.additionalData,
|
|
61
|
+
tagLength: 128,
|
|
62
|
+
},
|
|
63
|
+
key,
|
|
64
|
+
input.data,
|
|
65
|
+
),
|
|
66
|
+
// WebCrypto specifies OperationError for a rejected GCM tag.
|
|
67
|
+
catch: (cause) =>
|
|
68
|
+
nativeErrorName(cause) === "OperationError"
|
|
69
|
+
? AuthenticationFailed.make({})
|
|
70
|
+
: nativeError(cause),
|
|
71
|
+
}).pipe(Effect.map((decrypted) => new Uint8Array(decrypted)));
|
|
72
|
+
|
|
73
|
+
return Aead.of({
|
|
74
|
+
importKey: Effect.fnUntraced(function* (input) {
|
|
75
|
+
const value = yield* decode(KeyInput, input, "key");
|
|
76
|
+
|
|
77
|
+
yield* decode(keyBytes, Redacted.value(value.key), "key");
|
|
78
|
+
|
|
79
|
+
const use = yield* makeScopedKey(
|
|
80
|
+
withSecret(value.key, (material) =>
|
|
81
|
+
Effect.tryPromise({
|
|
82
|
+
try: () => subtle.importKey("raw", material, "AES-GCM", false, ["encrypt", "decrypt"]),
|
|
83
|
+
catch: importError,
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
22
87
|
|
|
23
|
-
|
|
24
|
-
|
|
88
|
+
return {
|
|
89
|
+
encrypt: Effect.fnUntraced(function* (input) {
|
|
90
|
+
const value = yield* decode(KeyEncryptInput, input, "data");
|
|
91
|
+
const nonce = yield* decode(aesNonce, value.nonce, "nonce").pipe(Effect.flatMap(copy));
|
|
92
|
+
const additionalData = yield* copy(value.additionalData ?? new Uint8Array());
|
|
93
|
+
|
|
94
|
+
return yield* withSecret(value.plaintext, (data) =>
|
|
95
|
+
use((key) => encryptAes(key, { nonce, additionalData, data })),
|
|
96
|
+
);
|
|
97
|
+
}),
|
|
98
|
+
decrypt: Effect.fnUntraced(function* (input) {
|
|
99
|
+
const value = yield* decode(KeyDecryptInput, input, "data");
|
|
100
|
+
const nonce = yield* decode(aesNonce, value.nonce, "nonce").pipe(Effect.flatMap(copy));
|
|
101
|
+
const additionalData = yield* copy(value.additionalData ?? new Uint8Array());
|
|
102
|
+
const data = yield* copy(value.ciphertext);
|
|
103
|
+
|
|
104
|
+
const plaintext = yield* use((key) =>
|
|
105
|
+
data.length < 16
|
|
106
|
+
? Effect.fail(AuthenticationFailed.make({}))
|
|
107
|
+
: decryptAes(key, { nonce, additionalData, data }),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return Redacted.make(plaintext);
|
|
111
|
+
}),
|
|
112
|
+
} satisfies Key;
|
|
113
|
+
}),
|
|
25
114
|
encrypt: Effect.fnUntraced(function* (input) {
|
|
26
115
|
const value = yield* decode(EncryptInput, input, "data");
|
|
27
116
|
const nonceLength = value.algorithm === "AES-256-GCM" ? 12 : 24;
|
|
@@ -51,17 +140,7 @@ export const makeAead = (subtle: SubtleCrypto, xchacha?: XChaCha): Aead["Service
|
|
|
51
140
|
catch: importError,
|
|
52
141
|
});
|
|
53
142
|
|
|
54
|
-
|
|
55
|
-
try: () =>
|
|
56
|
-
subtle.encrypt(
|
|
57
|
-
{ name: "AES-GCM", iv: nonce, additionalData, tagLength: 128 },
|
|
58
|
-
imported,
|
|
59
|
-
data,
|
|
60
|
-
),
|
|
61
|
-
catch: nativeError,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return new Uint8Array(encrypted);
|
|
143
|
+
return yield* encryptAes(imported, { nonce, additionalData, data });
|
|
65
144
|
}).pipe(Effect.uninterruptible);
|
|
66
145
|
}),
|
|
67
146
|
);
|
|
@@ -96,24 +175,11 @@ export const makeAead = (subtle: SubtleCrypto, xchacha?: XChaCha): Aead["Service
|
|
|
96
175
|
catch: importError,
|
|
97
176
|
});
|
|
98
177
|
|
|
99
|
-
|
|
100
|
-
try: () =>
|
|
101
|
-
subtle.decrypt(
|
|
102
|
-
{ name: "AES-GCM", iv: nonce, additionalData, tagLength: 128 },
|
|
103
|
-
imported,
|
|
104
|
-
data,
|
|
105
|
-
),
|
|
106
|
-
// WebCrypto specifies OperationError for a rejected GCM tag.
|
|
107
|
-
catch: (cause) =>
|
|
108
|
-
nativeErrorName(cause) === "OperationError"
|
|
109
|
-
? AuthenticationFailed.make({})
|
|
110
|
-
: nativeError(cause),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return new Uint8Array(decrypted);
|
|
178
|
+
return yield* decryptAes(imported, { nonce, additionalData, data });
|
|
114
179
|
}).pipe(Effect.uninterruptible);
|
|
115
180
|
});
|
|
116
181
|
|
|
117
182
|
return Redacted.make(plaintext);
|
|
118
183
|
}),
|
|
119
184
|
});
|
|
185
|
+
};
|
package/src/internal/argon2.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
2
|
|
|
3
3
|
import { allocate, destroy, fill, initialize, output } from "./argon2-core";
|
|
4
4
|
import type { Argon2 } from "./kdf";
|
|
5
5
|
|
|
6
|
-
/** Effect schedules
|
|
6
|
+
/** Effect schedules time slices; admission remains held through buffer cleanup. */
|
|
7
7
|
export const argon2: Argon2 = (input) =>
|
|
8
8
|
Effect.acquireUseRelease(
|
|
9
9
|
Effect.sync(() => allocate(input)),
|
|
10
10
|
(state) =>
|
|
11
11
|
Effect.gen(function* () {
|
|
12
|
+
const clock = yield* Clock.Clock;
|
|
13
|
+
|
|
12
14
|
initialize(state, input);
|
|
13
15
|
const blocks = fill(state, input);
|
|
14
16
|
|
|
15
17
|
for (;;) {
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
|
18
27
|
}
|
|
19
28
|
yield* Effect.yieldNow;
|
|
20
29
|
}
|
package/src/internal/hmac.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { 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
|
-
|
|
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
|
-
})
|
|
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(
|
|
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) =>
|
|
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
|
+
});
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { Effect, Redacted, Schema } from "effect";
|
|
2
2
|
import { Base64Url } from "effect/encoding";
|
|
3
3
|
|
|
4
|
-
import { InvalidInput } from "../Errors";
|
|
4
|
+
import { CryptoUnavailable, InvalidInput } from "../Errors";
|
|
5
5
|
import {
|
|
6
6
|
type Algorithm,
|
|
7
|
+
type PrivateKey,
|
|
8
|
+
PrivateKeyInput,
|
|
9
|
+
GenerateKeyPairInput,
|
|
7
10
|
PrivateKeyParameters,
|
|
11
|
+
type PublicKey,
|
|
12
|
+
PublicKeyInput,
|
|
8
13
|
PublicKeyParameters,
|
|
9
14
|
SignInput,
|
|
10
15
|
Signature,
|
|
11
16
|
VerifyInput,
|
|
12
17
|
} from "../Signature";
|
|
13
18
|
import { copy, decode, importError, nativeError, withSecret } from "./common";
|
|
19
|
+
import { makeScopedKey } from "./scoped-key";
|
|
14
20
|
|
|
15
21
|
const derBytes = Schema.Uint8Array.check(Schema.isMinLength(1), Schema.isMaxLength(16384));
|
|
16
22
|
|
|
@@ -18,6 +24,21 @@ const RsaAlgorithm = Schema.Struct({
|
|
|
18
24
|
modulusLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(2048)),
|
|
19
25
|
});
|
|
20
26
|
|
|
27
|
+
const component = Schema.Uint8ArrayFromBase64Url;
|
|
28
|
+
const GeneratedEc = Schema.Struct({ x: component, y: component, d: component });
|
|
29
|
+
const GeneratedEd = Schema.Struct({ x: component, d: component });
|
|
30
|
+
|
|
31
|
+
const GeneratedRsa = Schema.Struct({
|
|
32
|
+
n: component,
|
|
33
|
+
e: component,
|
|
34
|
+
d: component,
|
|
35
|
+
p: component,
|
|
36
|
+
q: component,
|
|
37
|
+
dp: component,
|
|
38
|
+
dq: component,
|
|
39
|
+
qi: component,
|
|
40
|
+
});
|
|
41
|
+
|
|
21
42
|
const importAlgorithm = (algorithm: Algorithm) => {
|
|
22
43
|
switch (algorithm) {
|
|
23
44
|
case "ECDSA-P256-SHA256":
|
|
@@ -121,6 +142,124 @@ export const makeSignature = (subtle: SubtleCrypto): Signature["Service"] => {
|
|
|
121
142
|
});
|
|
122
143
|
|
|
123
144
|
return Signature.of({
|
|
145
|
+
importPrivateKey: Effect.fnUntraced(function* (input) {
|
|
146
|
+
const value = yield* decode(PrivateKeyInput, input, "key");
|
|
147
|
+
const algorithm = value.algorithm;
|
|
148
|
+
|
|
149
|
+
const use = yield* makeScopedKey(
|
|
150
|
+
withSecret(value.privateKey, (material) => importKey(algorithm, "pkcs8", material)),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
sign: Effect.fnUntraced(function* (input) {
|
|
155
|
+
const data = yield* decode(Schema.Uint8Array, input, "data").pipe(Effect.flatMap(copy));
|
|
156
|
+
|
|
157
|
+
const result = yield* use((key) =>
|
|
158
|
+
Effect.tryPromise({
|
|
159
|
+
try: () => subtle.sign(operationAlgorithm(algorithm), key, data),
|
|
160
|
+
catch: nativeError,
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return new Uint8Array(result);
|
|
165
|
+
}),
|
|
166
|
+
} satisfies PrivateKey;
|
|
167
|
+
}),
|
|
168
|
+
importPublicKey: Effect.fnUntraced(function* (input) {
|
|
169
|
+
const value = yield* decode(PublicKeyInput, input, "key");
|
|
170
|
+
const algorithm = value.algorithm;
|
|
171
|
+
const material = yield* copy(value.publicKey);
|
|
172
|
+
const use = yield* makeScopedKey(importKey(algorithm, "spki", material));
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
verify: Effect.fnUntraced(function* (input, signature) {
|
|
176
|
+
const data = yield* decode(Schema.Uint8Array, input, "data").pipe(Effect.flatMap(copy));
|
|
177
|
+
|
|
178
|
+
const tag = yield* decode(Schema.Uint8Array, signature, "data").pipe(
|
|
179
|
+
Effect.flatMap(copy),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return yield* use((key) => {
|
|
183
|
+
if ((algorithm === "ECDSA-P256-SHA256" || algorithm === "Ed25519") && tag.length !== 64)
|
|
184
|
+
return Effect.succeed(false);
|
|
185
|
+
|
|
186
|
+
return Effect.tryPromise({
|
|
187
|
+
try: () => subtle.verify(operationAlgorithm(algorithm), key, tag, data),
|
|
188
|
+
catch: nativeError,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}),
|
|
192
|
+
} satisfies PublicKey;
|
|
193
|
+
}),
|
|
194
|
+
generateKeyPair: Effect.fnUntraced(function* (input) {
|
|
195
|
+
const value = yield* decode(GenerateKeyPairInput, input, "parameters");
|
|
196
|
+
|
|
197
|
+
const pair = yield* Effect.tryPromise({
|
|
198
|
+
try: () => {
|
|
199
|
+
switch (value.algorithm) {
|
|
200
|
+
case "ECDSA-P256-SHA256":
|
|
201
|
+
return subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
202
|
+
"sign",
|
|
203
|
+
"verify",
|
|
204
|
+
]);
|
|
205
|
+
case "Ed25519":
|
|
206
|
+
return subtle.generateKey("Ed25519", true, ["sign", "verify"]);
|
|
207
|
+
case "RSASSA-PKCS1-v1_5-SHA256":
|
|
208
|
+
case "RSA-PSS-SHA256":
|
|
209
|
+
return subtle.generateKey(
|
|
210
|
+
{
|
|
211
|
+
name: value.algorithm === "RSA-PSS-SHA256" ? "RSA-PSS" : "RSASSA-PKCS1-v1_5",
|
|
212
|
+
hash: "SHA-256",
|
|
213
|
+
modulusLength: value.modulusLength ?? 2048,
|
|
214
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
215
|
+
},
|
|
216
|
+
true,
|
|
217
|
+
["sign", "verify"],
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
catch: nativeError,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const jwk = yield* Effect.tryPromise({
|
|
225
|
+
try: () => subtle.exportKey("jwk", pair.privateKey),
|
|
226
|
+
catch: nativeError,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// JsonWebKey does not guarantee algorithm-specific fields. Decode only at
|
|
230
|
+
// this native boundary; callers receive typed components, never loose JWKs.
|
|
231
|
+
const parameters: PrivateKeyParameters = yield* Effect.gen(function* () {
|
|
232
|
+
switch (value.algorithm) {
|
|
233
|
+
case "ECDSA-P256-SHA256":
|
|
234
|
+
return {
|
|
235
|
+
algorithm: value.algorithm,
|
|
236
|
+
...(yield* Schema.decodeUnknownEffect(GeneratedEc)(jwk, { reportInput: false })),
|
|
237
|
+
};
|
|
238
|
+
case "Ed25519":
|
|
239
|
+
return {
|
|
240
|
+
algorithm: value.algorithm,
|
|
241
|
+
...(yield* Schema.decodeUnknownEffect(GeneratedEd)(jwk, { reportInput: false })),
|
|
242
|
+
};
|
|
243
|
+
case "RSASSA-PKCS1-v1_5-SHA256":
|
|
244
|
+
case "RSA-PSS-SHA256":
|
|
245
|
+
return {
|
|
246
|
+
algorithm: value.algorithm,
|
|
247
|
+
...(yield* Schema.decodeUnknownEffect(GeneratedRsa)(jwk, { reportInput: false })),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}).pipe(Effect.mapError(() => CryptoUnavailable.make({})));
|
|
251
|
+
|
|
252
|
+
yield* decode(PrivateKeyParameters, parameters, "key");
|
|
253
|
+
|
|
254
|
+
const publicKey: PublicKeyParameters =
|
|
255
|
+
"n" in parameters
|
|
256
|
+
? { algorithm: parameters.algorithm, n: parameters.n.slice(), e: parameters.e.slice() }
|
|
257
|
+
: "y" in parameters
|
|
258
|
+
? { algorithm: parameters.algorithm, x: parameters.x.slice(), y: parameters.y.slice() }
|
|
259
|
+
: { algorithm: "Ed25519", x: parameters.x.slice() };
|
|
260
|
+
|
|
261
|
+
return { publicKey, privateKey: Redacted.make(parameters) };
|
|
262
|
+
}, Effect.uninterruptible),
|
|
124
263
|
encodePublicKey: Effect.fnUntraced(function* (input) {
|
|
125
264
|
const parameters = yield* decode(PublicKeyParameters, input, "key");
|
|
126
265
|
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
|
|
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 =
|
|
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
|
};
|