@yielded/crypto 0.0.0-stage → 0.1.0-beta.19
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/LICENSE +21 -0
- package/README.md +39 -2
- package/THIRD_PARTY_NOTICES.md +378 -0
- package/dist/Aead.d.mts +34 -0
- package/dist/Aead.mjs +28 -0
- package/dist/Aead.mjs.map +1 -0
- package/dist/Errors.d.mts +20 -0
- package/dist/Errors.mjs +19 -0
- package/dist/Errors.mjs.map +1 -0
- package/dist/Hmac.d.mts +39 -0
- package/dist/Hmac.mjs +26 -0
- package/dist/Hmac.mjs.map +1 -0
- package/dist/Kdf.d.mts +54 -0
- package/dist/Kdf.mjs +78 -0
- package/dist/Kdf.mjs.map +1 -0
- package/dist/KdfAdmission.d.mts +24 -0
- package/dist/KdfAdmission.mjs +48 -0
- package/dist/KdfAdmission.mjs.map +1 -0
- package/dist/Portable.d.mts +11 -0
- package/dist/Portable.mjs +13 -0
- package/dist/Portable.mjs.map +1 -0
- package/dist/Signature.d.mts +70 -0
- package/dist/Signature.mjs +73 -0
- package/dist/Signature.mjs.map +1 -0
- package/dist/WebCrypto.d.mts +20 -0
- package/dist/WebCrypto.mjs +33 -0
- package/dist/WebCrypto.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/internal/aead.mjs +81 -0
- package/dist/internal/aead.mjs.map +1 -0
- package/dist/internal/argon2-core.mjs +293 -0
- package/dist/internal/argon2-core.mjs.map +1 -0
- package/dist/internal/argon2.mjs +16 -0
- package/dist/internal/argon2.mjs.map +1 -0
- package/dist/internal/blake2b.mjs +359 -0
- package/dist/internal/blake2b.mjs.map +1 -0
- package/dist/internal/common.mjs +20 -0
- package/dist/internal/common.mjs.map +1 -0
- package/dist/internal/hmac.mjs +68 -0
- package/dist/internal/hmac.mjs.map +1 -0
- package/dist/internal/kdf.mjs +99 -0
- package/dist/internal/kdf.mjs.map +1 -0
- package/dist/internal/layer.mjs +26 -0
- package/dist/internal/layer.mjs.map +1 -0
- package/dist/internal/node.d.mts +11 -0
- package/dist/internal/node.mjs +34 -0
- package/dist/internal/node.mjs.map +1 -0
- package/dist/internal/poly1305.mjs +247 -0
- package/dist/internal/poly1305.mjs.map +1 -0
- package/dist/internal/signature.mjs +140 -0
- package/dist/internal/signature.mjs.map +1 -0
- package/dist/internal/word64.mjs +25 -0
- package/dist/internal/word64.mjs.map +1 -0
- package/dist/internal/xchacha-core.mjs +285 -0
- package/dist/internal/xchacha-core.mjs.map +1 -0
- package/dist/internal/xchacha.mjs +21 -0
- package/dist/internal/xchacha.mjs.map +1 -0
- package/dist/platform-bun.d.mts +2 -0
- package/dist/platform-bun.mjs +2 -0
- package/dist/platform-node.d.mts +2 -0
- package/dist/platform-node.mjs +2 -0
- package/package.json +1 -6
- package/src/Aead.ts +39 -0
- package/src/Errors.ts +27 -0
- package/src/Hmac.ts +41 -0
- package/src/Kdf.ts +87 -0
- package/src/KdfAdmission.ts +87 -0
- package/src/Portable.ts +8 -0
- package/src/Signature.ts +87 -0
- package/src/WebCrypto.ts +46 -0
- package/src/index.ts +6 -0
- package/src/internal/aead.ts +119 -0
- package/src/internal/argon2-core.ts +470 -0
- package/src/internal/argon2.ts +23 -0
- package/src/internal/blake2b.ts +230 -0
- package/src/internal/common.ts +44 -0
- package/src/internal/hmac.ts +111 -0
- package/src/internal/kdf.ts +150 -0
- package/src/internal/layer.ts +32 -0
- package/src/internal/node.ts +46 -0
- package/src/internal/poly1305.ts +289 -0
- package/src/internal/signature.ts +188 -0
- package/src/internal/word64.ts +48 -0
- package/src/internal/xchacha-core.ts +397 -0
- package/src/internal/xchacha.ts +26 -0
- package/src/platform-bun.ts +2 -0
- package/src/platform-node.ts +2 -0
package/src/Portable.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { argon2 } from "./internal/argon2";
|
|
2
|
+
import { makeLayer } from "./internal/layer";
|
|
3
|
+
import { xchacha } from "./internal/xchacha";
|
|
4
|
+
import type { Limits } from "./Kdf";
|
|
5
|
+
|
|
6
|
+
/** WebCrypto with owned, portable Argon2id and XChaCha20-Poly1305 implementations. */
|
|
7
|
+
export const layer = (subtle: SubtleCrypto, limits: Partial<Limits> = {}) =>
|
|
8
|
+
makeLayer(subtle, limits, { argon2, xchacha });
|
package/src/Signature.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Context, type Effect, type Redacted, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { OperationError } from "./Errors";
|
|
4
|
+
|
|
5
|
+
export const Algorithm = Schema.Literals([
|
|
6
|
+
"ECDSA-P256-SHA256",
|
|
7
|
+
"RSASSA-PKCS1-v1_5-SHA256",
|
|
8
|
+
"RSA-PSS-SHA256",
|
|
9
|
+
"Ed25519",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export type Algorithm = typeof Algorithm.Type;
|
|
13
|
+
|
|
14
|
+
const coordinate = Schema.Uint8Array.check(Schema.isMinLength(32), Schema.isMaxLength(32));
|
|
15
|
+
const integer = Schema.Uint8Array.check(Schema.isMinLength(1), Schema.isMaxLength(1024));
|
|
16
|
+
const ec = { algorithm: Schema.Literal("ECDSA-P256-SHA256"), x: coordinate, y: coordinate };
|
|
17
|
+
const ed = { algorithm: Schema.Literal("Ed25519"), x: coordinate };
|
|
18
|
+
|
|
19
|
+
const rsa = {
|
|
20
|
+
algorithm: Schema.Literals(["RSASSA-PKCS1-v1_5-SHA256", "RSA-PSS-SHA256"]),
|
|
21
|
+
n: integer,
|
|
22
|
+
e: integer,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Unsigned big-endian components: 32-byte curve values and RSA components up to
|
|
27
|
+
* 1024 bytes. Native import validates the actual key, including RSA's minimum.
|
|
28
|
+
*/
|
|
29
|
+
export const PublicKeyParameters = Schema.Union([
|
|
30
|
+
Schema.Struct(ec),
|
|
31
|
+
Schema.Struct(ed),
|
|
32
|
+
Schema.Struct(rsa),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export type PublicKeyParameters = typeof PublicKeyParameters.Type;
|
|
36
|
+
|
|
37
|
+
export const PrivateKeyParameters = Schema.Union([
|
|
38
|
+
Schema.Struct({ ...ec, d: coordinate }),
|
|
39
|
+
Schema.Struct({ ...ed, d: coordinate }),
|
|
40
|
+
Schema.Struct({
|
|
41
|
+
...rsa,
|
|
42
|
+
d: integer,
|
|
43
|
+
p: integer,
|
|
44
|
+
q: integer,
|
|
45
|
+
dp: integer,
|
|
46
|
+
dq: integer,
|
|
47
|
+
qi: integer,
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export type PrivateKeyParameters = typeof PrivateKeyParameters.Type;
|
|
52
|
+
|
|
53
|
+
export const SignInput = Schema.Struct({
|
|
54
|
+
algorithm: Algorithm,
|
|
55
|
+
privateKey: Schema.Redacted(Schema.Uint8Array, { disallowJsonEncode: true }),
|
|
56
|
+
data: Schema.Uint8Array,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type SignInput = typeof SignInput.Type;
|
|
60
|
+
|
|
61
|
+
export const VerifyInput = Schema.Struct({
|
|
62
|
+
algorithm: Algorithm,
|
|
63
|
+
publicKey: Schema.Uint8Array,
|
|
64
|
+
data: Schema.Uint8Array,
|
|
65
|
+
signature: Schema.Uint8Array,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type VerifyInput = typeof VerifyInput.Type;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Private keys are PKCS8 DER; public keys are SPKI DER. ECDSA signatures are
|
|
72
|
+
* 64-byte IEEE P1363 (r || s), not ASN.1 DER. RSA requires at least 2048 bits;
|
|
73
|
+
* PSS uses a 32-byte salt. Key serialization/selection and JOSE policy stay outside.
|
|
74
|
+
*/
|
|
75
|
+
export class Signature extends Context.Service<
|
|
76
|
+
Signature,
|
|
77
|
+
{
|
|
78
|
+
readonly encodePublicKey: (
|
|
79
|
+
input: PublicKeyParameters,
|
|
80
|
+
) => Effect.Effect<Uint8Array, OperationError>;
|
|
81
|
+
readonly encodePrivateKey: (
|
|
82
|
+
input: Redacted.Redacted<PrivateKeyParameters>,
|
|
83
|
+
) => Effect.Effect<Redacted.Redacted<Uint8Array>, OperationError>;
|
|
84
|
+
readonly sign: (input: SignInput) => Effect.Effect<Uint8Array, OperationError>;
|
|
85
|
+
readonly verify: (input: VerifyInput) => Effect.Effect<boolean, OperationError>;
|
|
86
|
+
}
|
|
87
|
+
>()("@yielded/crypto/Signature") {}
|
package/src/WebCrypto.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Crypto, Effect, Layer, PlatformError } from "effect";
|
|
2
|
+
|
|
3
|
+
import { Hmac } from "./Hmac";
|
|
4
|
+
import { makeHmac } from "./internal/hmac";
|
|
5
|
+
import { makeLayer } from "./internal/layer";
|
|
6
|
+
import type { Limits } from "./Kdf";
|
|
7
|
+
|
|
8
|
+
/** Supply the host capability explicitly; no implicit global backend selection. */
|
|
9
|
+
export const layer = (subtle: SubtleCrypto, limits: Partial<Limits> = {}) =>
|
|
10
|
+
makeLayer(subtle, limits);
|
|
11
|
+
|
|
12
|
+
/** HMAC alone has no KDF admission requirement. */
|
|
13
|
+
export const layerHmac = (subtle: SubtleCrypto): Layer.Layer<Hmac> =>
|
|
14
|
+
Layer.sync(Hmac)(() => makeHmac(subtle));
|
|
15
|
+
|
|
16
|
+
/** Effect entropy and SHA digests from the host's global WebCrypto implementation. */
|
|
17
|
+
export const layerCryptoWeb: Layer.Layer<Crypto.Crypto> = Layer.sync(Crypto.Crypto)(() =>
|
|
18
|
+
Crypto.make({
|
|
19
|
+
randomBytes: (size) => globalThis.crypto.getRandomValues(new Uint8Array(size)),
|
|
20
|
+
digest: (algorithm, data) =>
|
|
21
|
+
Effect.tryPromise({
|
|
22
|
+
try: () => globalThis.crypto.subtle.digest(algorithm, new Uint8Array(data)),
|
|
23
|
+
catch: (cause) =>
|
|
24
|
+
PlatformError.systemError({
|
|
25
|
+
_tag: "Unknown",
|
|
26
|
+
module: "Crypto",
|
|
27
|
+
method: "digest",
|
|
28
|
+
cause,
|
|
29
|
+
}),
|
|
30
|
+
}).pipe(Effect.map((bytes) => new Uint8Array(bytes))),
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/** HMAC from global WebCrypto. Missing WebCrypto is a platform defect at Layer construction. */
|
|
35
|
+
export const layerHmacWeb: Layer.Layer<Hmac> = Layer.effect(Hmac)(
|
|
36
|
+
Effect.suspend(() => {
|
|
37
|
+
const subtle = globalThis.crypto?.subtle;
|
|
38
|
+
|
|
39
|
+
return subtle === undefined
|
|
40
|
+
? Effect.die(new Error("WebCrypto is unavailable in this runtime"))
|
|
41
|
+
: Effect.succeed(makeHmac(subtle));
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Entropy, SHA digests and HMAC for sessions and proofs, without password derivation. */
|
|
46
|
+
export const layerWebCrypto = Layer.merge(layerCryptoWeb, layerHmacWeb);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import { Aead, DecryptInput, EncryptInput } from "../Aead";
|
|
4
|
+
import { AuthenticationFailed, type OperationError, UnsupportedAlgorithm } from "../Errors";
|
|
5
|
+
import { copy, decode, importError, nativeError, nativeErrorName, withSecret } from "./common";
|
|
6
|
+
|
|
7
|
+
interface CipherInput {
|
|
8
|
+
readonly key: Uint8Array<ArrayBuffer>;
|
|
9
|
+
readonly nonce: Uint8Array<ArrayBuffer>;
|
|
10
|
+
readonly additionalData: Uint8Array<ArrayBuffer>;
|
|
11
|
+
readonly data: Uint8Array<ArrayBuffer>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface XChaCha {
|
|
15
|
+
readonly encrypt: (input: CipherInput) => Effect.Effect<Uint8Array, OperationError>;
|
|
16
|
+
readonly decrypt: (
|
|
17
|
+
input: CipherInput,
|
|
18
|
+
) => Effect.Effect<Uint8Array, OperationError | AuthenticationFailed>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const keyBytes = Schema.Uint8Array.check(Schema.isBetweenLength(32, 32));
|
|
22
|
+
|
|
23
|
+
export const makeAead = (subtle: SubtleCrypto, xchacha?: XChaCha): Aead["Service"] =>
|
|
24
|
+
Aead.of({
|
|
25
|
+
encrypt: Effect.fnUntraced(function* (input) {
|
|
26
|
+
const value = yield* decode(EncryptInput, input, "data");
|
|
27
|
+
const nonceLength = value.algorithm === "AES-256-GCM" ? 12 : 24;
|
|
28
|
+
|
|
29
|
+
yield* decode(keyBytes, Redacted.value(value.key), "key");
|
|
30
|
+
yield* decode(
|
|
31
|
+
Schema.Uint8Array.check(Schema.isBetweenLength(nonceLength, nonceLength)),
|
|
32
|
+
value.nonce,
|
|
33
|
+
"nonce",
|
|
34
|
+
);
|
|
35
|
+
if (value.algorithm === "XChaCha20-Poly1305" && xchacha === undefined)
|
|
36
|
+
return yield* UnsupportedAlgorithm.make({});
|
|
37
|
+
const nonce = yield* copy(value.nonce);
|
|
38
|
+
const additionalData = yield* copy(value.additionalData ?? new Uint8Array());
|
|
39
|
+
|
|
40
|
+
return yield* withSecret(value.key, (key) =>
|
|
41
|
+
withSecret(value.plaintext, (data) => {
|
|
42
|
+
if (value.algorithm === "XChaCha20-Poly1305") {
|
|
43
|
+
return xchacha === undefined
|
|
44
|
+
? Effect.fail(UnsupportedAlgorithm.make({}))
|
|
45
|
+
: xchacha.encrypt({ key, nonce, additionalData, data });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Effect.gen(function* () {
|
|
49
|
+
const imported = yield* Effect.tryPromise({
|
|
50
|
+
try: () => subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]),
|
|
51
|
+
catch: importError,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const encrypted = yield* Effect.tryPromise({
|
|
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);
|
|
65
|
+
}).pipe(Effect.uninterruptible);
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}),
|
|
69
|
+
decrypt: Effect.fnUntraced(function* (input) {
|
|
70
|
+
const value = yield* decode(DecryptInput, input, "data");
|
|
71
|
+
const nonceLength = value.algorithm === "AES-256-GCM" ? 12 : 24;
|
|
72
|
+
|
|
73
|
+
yield* decode(keyBytes, Redacted.value(value.key), "key");
|
|
74
|
+
yield* decode(
|
|
75
|
+
Schema.Uint8Array.check(Schema.isBetweenLength(nonceLength, nonceLength)),
|
|
76
|
+
value.nonce,
|
|
77
|
+
"nonce",
|
|
78
|
+
);
|
|
79
|
+
if (value.algorithm === "XChaCha20-Poly1305" && xchacha === undefined)
|
|
80
|
+
return yield* UnsupportedAlgorithm.make({});
|
|
81
|
+
if (value.ciphertext.length < 16) return yield* AuthenticationFailed.make({});
|
|
82
|
+
const nonce = yield* copy(value.nonce);
|
|
83
|
+
const additionalData = yield* copy(value.additionalData ?? new Uint8Array());
|
|
84
|
+
const data = yield* copy(value.ciphertext);
|
|
85
|
+
|
|
86
|
+
const plaintext = yield* withSecret(value.key, (key) => {
|
|
87
|
+
if (value.algorithm === "XChaCha20-Poly1305") {
|
|
88
|
+
return xchacha === undefined
|
|
89
|
+
? Effect.fail(UnsupportedAlgorithm.make({}))
|
|
90
|
+
: xchacha.decrypt({ key, nonce, additionalData, data });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Effect.gen(function* () {
|
|
94
|
+
const imported = yield* Effect.tryPromise({
|
|
95
|
+
try: () => subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]),
|
|
96
|
+
catch: importError,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const decrypted = yield* Effect.tryPromise({
|
|
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);
|
|
114
|
+
}).pipe(Effect.uninterruptible);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return Redacted.make(plaintext);
|
|
118
|
+
}),
|
|
119
|
+
});
|