@venn-lang/crypto 0.1.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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +444 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/actions/encoding-actions.ts +34 -0
- package/src/actions/hash-actions.ts +70 -0
- package/src/actions/index.ts +13 -0
- package/src/actions/jwt-actions.ts +77 -0
- package/src/actions/password-actions.ts +66 -0
- package/src/bytes/bytes.ts +70 -0
- package/src/bytes/index.ts +12 -0
- package/src/engines/fake-engine.ts +43 -0
- package/src/engines/index.ts +2 -0
- package/src/engines/web-crypto-engine.ts +45 -0
- package/src/index.ts +20 -0
- package/src/jwt/decode.ts +37 -0
- package/src/jwt/index.ts +2 -0
- package/src/jwt/jwt.types.ts +9 -0
- package/src/plugin.ts +18 -0
- package/src/port/crypto-engine.port.ts +15 -0
- package/src/port/crypto-engine.types.ts +23 -0
- package/src/port/index.ts +2 -0
- package/src/types.ts +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { VennError } from "@venn-lang/contracts";
|
|
2
|
+
import { arg, defineAction, definePlugin, z } from "@venn-lang/sdk";
|
|
3
|
+
import { t } from "@venn-lang/types";
|
|
4
|
+
//#region src/bytes/bytes.ts
|
|
5
|
+
/** UTF-8 encode a string. */
|
|
6
|
+
function toBytes(text) {
|
|
7
|
+
return new TextEncoder().encode(text);
|
|
8
|
+
}
|
|
9
|
+
/** UTF-8 decode bytes back to a string. Invalid sequences become the replacement character. */
|
|
10
|
+
function fromBytes(bytes) {
|
|
11
|
+
return new TextDecoder().decode(bytes);
|
|
12
|
+
}
|
|
13
|
+
/** Lowercase hex, two characters per byte. */
|
|
14
|
+
function toHex(bytes) {
|
|
15
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
16
|
+
}
|
|
17
|
+
/** Read a hex string back into bytes. A trailing odd character is dropped. */
|
|
18
|
+
function fromHex(hex) {
|
|
19
|
+
const pairs = hex.match(/../g) ?? [];
|
|
20
|
+
return Uint8Array.from(pairs.map((pair) => Number.parseInt(pair, 16)));
|
|
21
|
+
}
|
|
22
|
+
/** Standard base64, padded. */
|
|
23
|
+
function toBase64(bytes) {
|
|
24
|
+
return btoa(String.fromCharCode(...bytes));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read standard base64 back into bytes.
|
|
28
|
+
*
|
|
29
|
+
* @throws DOMException when the text is not valid base64.
|
|
30
|
+
*/
|
|
31
|
+
function fromBase64(text) {
|
|
32
|
+
return Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
|
|
33
|
+
}
|
|
34
|
+
/** Base64url, the flavour JWT uses: `+/` become `-_` and padding is dropped. */
|
|
35
|
+
function toBase64Url(bytes) {
|
|
36
|
+
return toBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read base64url back into bytes, restoring the padding first.
|
|
40
|
+
*
|
|
41
|
+
* @throws DOMException when the text is not valid base64url.
|
|
42
|
+
*/
|
|
43
|
+
function fromBase64Url(text) {
|
|
44
|
+
const padded = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
45
|
+
return fromBase64(padded.padEnd(Math.ceil(padded.length / 4) * 4, "="));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compare two strings in time that does not depend on where they differ.
|
|
49
|
+
*
|
|
50
|
+
* Use this for every signature and digest check: `===` returns early on the
|
|
51
|
+
* first mismatched byte, which leaks the correct prefix to a patient attacker.
|
|
52
|
+
*/
|
|
53
|
+
function equals(left, right) {
|
|
54
|
+
if (left.length !== right.length) return false;
|
|
55
|
+
let diff = 0;
|
|
56
|
+
for (let index = 0; index < left.length; index++) diff |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
57
|
+
return diff === 0;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/engines/fake-engine.ts
|
|
61
|
+
/**
|
|
62
|
+
* A deterministic stand-in for tests: same input, same output, no platform crypto.
|
|
63
|
+
*
|
|
64
|
+
* Not secure and not meant to be. It exists so assertions over a digest, an HMAC
|
|
65
|
+
* or a random token stay reproducible from run to run.
|
|
66
|
+
*/
|
|
67
|
+
function createFakeCryptoEngine() {
|
|
68
|
+
let counter = 0;
|
|
69
|
+
return {
|
|
70
|
+
digest: ({ algorithm, data }) => Promise.resolve(stable(`${algorithm}|${data}`)),
|
|
71
|
+
hmac: ({ algorithm, key, data }) => Promise.resolve(stable(`${algorithm}|${key}|${data}`)),
|
|
72
|
+
derive: (args) => Promise.resolve(stable(`${args.algorithm}|${args.password}|${args.salt}|${args.iterations}`)),
|
|
73
|
+
randomBytes: (size) => {
|
|
74
|
+
counter += 1;
|
|
75
|
+
return sized(stable(`bytes|${counter}`), size);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function stable(seed) {
|
|
80
|
+
let out = "";
|
|
81
|
+
for (let round = 0; out.length < 64; round++) out += fnv1a(`${seed}|${round}`).toString(16).padStart(8, "0");
|
|
82
|
+
return out.slice(0, 64);
|
|
83
|
+
}
|
|
84
|
+
function fnv1a(text) {
|
|
85
|
+
let hash = 2166136261;
|
|
86
|
+
for (let index = 0; index < text.length; index++) {
|
|
87
|
+
hash ^= text.charCodeAt(index);
|
|
88
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
89
|
+
}
|
|
90
|
+
return hash >>> 0;
|
|
91
|
+
}
|
|
92
|
+
function sized(hex, size) {
|
|
93
|
+
return hex.repeat(Math.ceil(size * 2 / hex.length)).slice(0, size * 2);
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/engines/web-crypto-engine.ts
|
|
97
|
+
const NAMES = {
|
|
98
|
+
sha1: "SHA-1",
|
|
99
|
+
sha256: "SHA-256",
|
|
100
|
+
sha384: "SHA-384",
|
|
101
|
+
sha512: "SHA-512"
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The real engine, backed by the platform's global WebCrypto.
|
|
105
|
+
*
|
|
106
|
+
* WebCrypto exists in Node 24 and in browsers alike, so nothing here imports
|
|
107
|
+
* `node:crypto` and the package stays platform-neutral.
|
|
108
|
+
*/
|
|
109
|
+
function createWebCryptoEngine() {
|
|
110
|
+
return {
|
|
111
|
+
digest: async ({ algorithm, data }) => toHex(new Uint8Array(await crypto.subtle.digest(NAMES[algorithm], toBytes(data)))),
|
|
112
|
+
hmac: async ({ algorithm, key, data }) => toHex(new Uint8Array(await sign$1(algorithm, key, data))),
|
|
113
|
+
derive: async (args) => toHex(new Uint8Array(await deriveBits(args))),
|
|
114
|
+
randomBytes: (size) => toHex(crypto.getRandomValues(new Uint8Array(size)))
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function sign$1(algorithm, key, data) {
|
|
118
|
+
const spec = {
|
|
119
|
+
name: "HMAC",
|
|
120
|
+
hash: NAMES[algorithm]
|
|
121
|
+
};
|
|
122
|
+
const material = await crypto.subtle.importKey("raw", toBytes(key), spec, false, ["sign"]);
|
|
123
|
+
return crypto.subtle.sign("HMAC", material, toBytes(data));
|
|
124
|
+
}
|
|
125
|
+
async function deriveBits(args) {
|
|
126
|
+
const material = await crypto.subtle.importKey("raw", toBytes(args.password), "PBKDF2", false, ["deriveBits"]);
|
|
127
|
+
const params = {
|
|
128
|
+
name: "PBKDF2",
|
|
129
|
+
salt: toBytes(args.salt),
|
|
130
|
+
iterations: args.iterations,
|
|
131
|
+
hash: NAMES[args.algorithm]
|
|
132
|
+
};
|
|
133
|
+
return crypto.subtle.deriveBits(params, material, 256);
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/jwt/decode.ts
|
|
137
|
+
/**
|
|
138
|
+
* Split a token and decode its header and payload, without verifying anything.
|
|
139
|
+
*
|
|
140
|
+
* Reading a token's claims and trusting them are separate acts. This is the
|
|
141
|
+
* first; `crypto.jwt.verify` is the second.
|
|
142
|
+
*
|
|
143
|
+
* @param token A compact JWS, `header.payload.signature`.
|
|
144
|
+
* @returns The decoded sections plus the `signingInput` a signature covers.
|
|
145
|
+
* @throws VennError `VN7003` when the token has fewer than two sections, or when
|
|
146
|
+
* a section is not base64url-encoded JSON.
|
|
147
|
+
*/
|
|
148
|
+
function decodeJwt(token) {
|
|
149
|
+
const parts = token.split(".");
|
|
150
|
+
if (parts.length < 2) throw malformed("expected header.payload.signature");
|
|
151
|
+
return {
|
|
152
|
+
header: section(parts[0], "header"),
|
|
153
|
+
payload: section(parts[1], "payload"),
|
|
154
|
+
signature: parts[2] ?? "",
|
|
155
|
+
signingInput: `${parts[0]}.${parts[1]}`
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function section(part, what) {
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(fromBytes(fromBase64Url(part ?? "")));
|
|
161
|
+
} catch {
|
|
162
|
+
throw malformed(`its ${what} is not base64url-encoded JSON`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function malformed(detail) {
|
|
166
|
+
return new VennError({
|
|
167
|
+
code: "VN7003",
|
|
168
|
+
message: `Not a JWT — ${detail}.`
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/actions/encoding-actions.ts
|
|
173
|
+
/** The `crypto.base64.*` and `crypto.base64url.*` verbs, both directions. */
|
|
174
|
+
const encodingActions = [
|
|
175
|
+
text("base64.encode", "Encode a string as base64.", (value) => toBase64(toBytes(value))),
|
|
176
|
+
text("base64.decode", "Decode base64 back to a string.", (value) => fromBytes(fromBase64(value))),
|
|
177
|
+
text("base64url.encode", "Encode a string as base64url (JWT's flavour).", (value) => toBase64Url(toBytes(value))),
|
|
178
|
+
text("base64url.decode", "Decode base64url back to a string.", (value) => fromBytes(fromBase64Url(value)))
|
|
179
|
+
];
|
|
180
|
+
function text(name, doc, transform) {
|
|
181
|
+
return defineAction({
|
|
182
|
+
name,
|
|
183
|
+
doc,
|
|
184
|
+
args: [arg("text", t.string, "What to encode or decode.")],
|
|
185
|
+
result: t.string,
|
|
186
|
+
run: (_ctx, input) => transform(String(input.args[0] ?? ""))
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/port/crypto-engine.port.ts
|
|
191
|
+
/**
|
|
192
|
+
* The port every `crypto` verb reaches its primitives through.
|
|
193
|
+
*
|
|
194
|
+
* Bound by the host to `createWebCryptoEngine` or `createFakeCryptoEngine`.
|
|
195
|
+
* Requires no capability, because WebCrypto is present on every target.
|
|
196
|
+
*/
|
|
197
|
+
const CryptoEnginePort = {
|
|
198
|
+
id: "venn.port.crypto-engine",
|
|
199
|
+
version: 1,
|
|
200
|
+
requires: [],
|
|
201
|
+
methods: [
|
|
202
|
+
"digest",
|
|
203
|
+
"hmac",
|
|
204
|
+
"derive",
|
|
205
|
+
"randomBytes"
|
|
206
|
+
]
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/actions/hash-actions.ts
|
|
210
|
+
const algorithm = z.enum([
|
|
211
|
+
"sha1",
|
|
212
|
+
"sha256",
|
|
213
|
+
"sha384",
|
|
214
|
+
"sha512"
|
|
215
|
+
]).default("sha256");
|
|
216
|
+
const hashParams$1 = z.object({ algorithm }).optional();
|
|
217
|
+
const hmacParams = z.object({
|
|
218
|
+
key: z.string(),
|
|
219
|
+
algorithm
|
|
220
|
+
});
|
|
221
|
+
const bytesParams = z.object({ size: z.number().int().positive().default(16) }).optional();
|
|
222
|
+
/** The `crypto.hash`, `crypto.hmac`, `crypto.randomBytes` and `crypto.uuid` verbs. */
|
|
223
|
+
const hashActions = [
|
|
224
|
+
defineAction({
|
|
225
|
+
name: "hash",
|
|
226
|
+
doc: "Digest a string, hex-encoded. Defaults to sha256.",
|
|
227
|
+
params: hashParams$1,
|
|
228
|
+
args: [arg("data", t.string, "What to digest.")],
|
|
229
|
+
result: t.string,
|
|
230
|
+
run: (ctx, input) => ctx.port(CryptoEnginePort).digest({
|
|
231
|
+
algorithm: algorithmOf(input.params),
|
|
232
|
+
data: String(input.args[0] ?? "")
|
|
233
|
+
})
|
|
234
|
+
}),
|
|
235
|
+
defineAction({
|
|
236
|
+
name: "hmac",
|
|
237
|
+
doc: "Keyed digest of a string, hex-encoded.",
|
|
238
|
+
params: hmacParams,
|
|
239
|
+
args: [arg("data", t.string, "What to sign. The key goes in the options.")],
|
|
240
|
+
result: t.string,
|
|
241
|
+
run: (ctx, input) => {
|
|
242
|
+
const params = input.params;
|
|
243
|
+
const data = String(input.args[0] ?? "");
|
|
244
|
+
return ctx.port(CryptoEnginePort).hmac({
|
|
245
|
+
algorithm: params.algorithm,
|
|
246
|
+
key: params.key,
|
|
247
|
+
data
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}),
|
|
251
|
+
defineAction({
|
|
252
|
+
name: "randomBytes",
|
|
253
|
+
doc: "Random bytes, hex-encoded.",
|
|
254
|
+
params: bytesParams,
|
|
255
|
+
result: t.string,
|
|
256
|
+
run: (ctx, input) => {
|
|
257
|
+
const size = input.params?.size ?? 16;
|
|
258
|
+
return ctx.port(CryptoEnginePort).randomBytes(size);
|
|
259
|
+
}
|
|
260
|
+
}),
|
|
261
|
+
defineAction({
|
|
262
|
+
name: "uuid",
|
|
263
|
+
doc: "A random UUID v4.",
|
|
264
|
+
result: t.string,
|
|
265
|
+
run: (ctx) => uuidFrom(ctx.port(CryptoEnginePort).randomBytes(16))
|
|
266
|
+
})
|
|
267
|
+
];
|
|
268
|
+
function algorithmOf(params) {
|
|
269
|
+
return params?.algorithm ?? "sha256";
|
|
270
|
+
}
|
|
271
|
+
function uuidFrom(hex) {
|
|
272
|
+
const bytes = fromHex(hex);
|
|
273
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
274
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
275
|
+
const text = toHex(bytes);
|
|
276
|
+
return [...[
|
|
277
|
+
text.slice(0, 8),
|
|
278
|
+
text.slice(8, 12),
|
|
279
|
+
text.slice(12, 16),
|
|
280
|
+
text.slice(16, 20)
|
|
281
|
+
], text.slice(20, 32)].join("-");
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/actions/jwt-actions.ts
|
|
285
|
+
const ALGORITHMS = {
|
|
286
|
+
HS256: "sha256",
|
|
287
|
+
HS384: "sha384",
|
|
288
|
+
HS512: "sha512"
|
|
289
|
+
};
|
|
290
|
+
const signParams = z.object({
|
|
291
|
+
payload: z.record(z.string(), z.unknown()),
|
|
292
|
+
secret: z.string(),
|
|
293
|
+
algorithm: z.enum([
|
|
294
|
+
"HS256",
|
|
295
|
+
"HS384",
|
|
296
|
+
"HS512"
|
|
297
|
+
]).default("HS256")
|
|
298
|
+
});
|
|
299
|
+
const verifyParams$1 = z.object({ secret: z.string() });
|
|
300
|
+
/** The `crypto.jwt.*` verbs: read a token, mint one, check one. */
|
|
301
|
+
const jwtActions = [
|
|
302
|
+
defineAction({
|
|
303
|
+
name: "jwt.decode",
|
|
304
|
+
doc: "Take a token apart without verifying it: header, payload, signature.",
|
|
305
|
+
args: [arg("token", t.string, "The token to take apart. Nothing is verified.")],
|
|
306
|
+
result: t.ref("crypto.Jwt"),
|
|
307
|
+
run: (_ctx, input) => decodeJwt(String(input.args[0] ?? ""))
|
|
308
|
+
}),
|
|
309
|
+
defineAction({
|
|
310
|
+
name: "jwt.sign",
|
|
311
|
+
doc: "Mint an HMAC-signed token.",
|
|
312
|
+
params: signParams,
|
|
313
|
+
result: t.string,
|
|
314
|
+
run: (ctx, input) => sign(ctx, input.params)
|
|
315
|
+
}),
|
|
316
|
+
defineAction({
|
|
317
|
+
name: "jwt.verify",
|
|
318
|
+
doc: "True when the token's signature matches the secret.",
|
|
319
|
+
params: verifyParams$1,
|
|
320
|
+
args: [arg("token", t.string, "The token to check against the secret in the options.")],
|
|
321
|
+
result: t.bool,
|
|
322
|
+
run: (ctx, input) => verify$1(ctx, String(input.args[0] ?? ""), input.params)
|
|
323
|
+
})
|
|
324
|
+
];
|
|
325
|
+
async function sign(ctx, params) {
|
|
326
|
+
const input = `${encode({
|
|
327
|
+
alg: params.algorithm,
|
|
328
|
+
typ: "JWT"
|
|
329
|
+
})}.${encode(params.payload)}`;
|
|
330
|
+
return `${input}.${toBase64Url(fromHex(await ctx.port(CryptoEnginePort).hmac({
|
|
331
|
+
algorithm: ALGORITHMS[params.algorithm],
|
|
332
|
+
key: params.secret,
|
|
333
|
+
data: input
|
|
334
|
+
})))}`;
|
|
335
|
+
}
|
|
336
|
+
async function verify$1(ctx, token, params) {
|
|
337
|
+
const decoded = decodeJwt(token);
|
|
338
|
+
const algorithm = ALGORITHMS[String(decoded.header.alg ?? "")];
|
|
339
|
+
if (!algorithm) return false;
|
|
340
|
+
return equals(toBase64Url(fromHex(await ctx.port(CryptoEnginePort).hmac({
|
|
341
|
+
algorithm,
|
|
342
|
+
key: params.secret,
|
|
343
|
+
data: decoded.signingInput
|
|
344
|
+
}))), decoded.signature);
|
|
345
|
+
}
|
|
346
|
+
function encode(value) {
|
|
347
|
+
return toBase64Url(toBytes(JSON.stringify(value)));
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/actions/password-actions.ts
|
|
351
|
+
const SCHEME = "pbkdf2";
|
|
352
|
+
const DEFAULT_ITERATIONS = 1e5;
|
|
353
|
+
const hashParams = z.object({
|
|
354
|
+
iterations: z.number().int().positive().default(DEFAULT_ITERATIONS),
|
|
355
|
+
algorithm: z.enum(["sha256", "sha512"]).default("sha256")
|
|
356
|
+
}).optional();
|
|
357
|
+
const verifyParams = z.object({ hash: z.string() });
|
|
358
|
+
/**
|
|
359
|
+
* The `crypto.password.hash` and `crypto.password.verify` verbs.
|
|
360
|
+
*
|
|
361
|
+
* PBKDF2 rather than bcrypt, because WebCrypto offers PBKDF2 and so the package
|
|
362
|
+
* needs no native module. The encoded form carries its own cost parameters,
|
|
363
|
+
* `pbkdf2$sha256$iterations$salt$derived`, so a stored hash stays verifiable
|
|
364
|
+
* after the defaults here change.
|
|
365
|
+
*/
|
|
366
|
+
const passwordActions = [defineAction({
|
|
367
|
+
name: "password.hash",
|
|
368
|
+
doc: "Hash a password with PBKDF2, returning a self-describing string.",
|
|
369
|
+
params: hashParams,
|
|
370
|
+
args: [arg("password", t.string, "The password in the clear.")],
|
|
371
|
+
result: t.string,
|
|
372
|
+
run: (ctx, input) => hash(ctx, String(input.args[0] ?? ""), input.params)
|
|
373
|
+
}), defineAction({
|
|
374
|
+
name: "password.verify",
|
|
375
|
+
doc: "True when the password matches a hash produced by `password.hash`.",
|
|
376
|
+
params: verifyParams,
|
|
377
|
+
args: [arg("password", t.string, "The password in the clear, to check against the hash.")],
|
|
378
|
+
result: t.bool,
|
|
379
|
+
run: (ctx, input) => verify(ctx, String(input.args[0] ?? ""), input.params.hash)
|
|
380
|
+
})];
|
|
381
|
+
async function hash(ctx, password, params) {
|
|
382
|
+
const options = params ?? {};
|
|
383
|
+
const iterations = options.iterations ?? DEFAULT_ITERATIONS;
|
|
384
|
+
const algorithm = options.algorithm ?? "sha256";
|
|
385
|
+
const salt = ctx.port(CryptoEnginePort).randomBytes(16);
|
|
386
|
+
const derived = await ctx.port(CryptoEnginePort).derive({
|
|
387
|
+
password,
|
|
388
|
+
salt,
|
|
389
|
+
iterations,
|
|
390
|
+
algorithm
|
|
391
|
+
});
|
|
392
|
+
return [
|
|
393
|
+
SCHEME,
|
|
394
|
+
algorithm,
|
|
395
|
+
iterations,
|
|
396
|
+
salt,
|
|
397
|
+
derived
|
|
398
|
+
].join("$");
|
|
399
|
+
}
|
|
400
|
+
async function verify(ctx, password, encoded) {
|
|
401
|
+
const [scheme, algorithm, iterations, salt, derived] = encoded.split("$");
|
|
402
|
+
if (scheme !== SCHEME || !algorithm || !salt || !derived) return false;
|
|
403
|
+
return equals(await ctx.port(CryptoEnginePort).derive({
|
|
404
|
+
password,
|
|
405
|
+
salt,
|
|
406
|
+
iterations: Number(iterations),
|
|
407
|
+
algorithm
|
|
408
|
+
}), derived);
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/plugin.ts
|
|
412
|
+
/**
|
|
413
|
+
* The `crypto` plugin: digests, encodings, password hashing and JSON Web Tokens.
|
|
414
|
+
*
|
|
415
|
+
* Every primitive comes from `CryptoEnginePort`, so a host can swap WebCrypto for
|
|
416
|
+
* the deterministic fake and keep assertions reproducible. No capability is
|
|
417
|
+
* required: WebCrypto is present in Node and in the browser alike.
|
|
418
|
+
*/
|
|
419
|
+
const cryptoPlugin = definePlugin({
|
|
420
|
+
name: "venn/crypto",
|
|
421
|
+
version: "0.1.0",
|
|
422
|
+
namespace: "crypto",
|
|
423
|
+
actions: [
|
|
424
|
+
...hashActions,
|
|
425
|
+
...encodingActions,
|
|
426
|
+
...jwtActions,
|
|
427
|
+
...passwordActions
|
|
428
|
+
],
|
|
429
|
+
typeDefs: {
|
|
430
|
+
/**
|
|
431
|
+
* A token taken apart. The claims are whatever the issuer put there, so both
|
|
432
|
+
* sections are maps of dynamic rather than a shape nobody can promise.
|
|
433
|
+
*/
|
|
434
|
+
Jwt: t.record({
|
|
435
|
+
header: t.map(t.dynamic),
|
|
436
|
+
payload: t.map(t.dynamic),
|
|
437
|
+
signature: t.string,
|
|
438
|
+
signingInput: t.string
|
|
439
|
+
}) }
|
|
440
|
+
});
|
|
441
|
+
//#endregion
|
|
442
|
+
export { CryptoEnginePort, createFakeCryptoEngine, createWebCryptoEngine, cryptoPlugin, decodeJwt, equals, fromBase64, fromBase64Url, fromBytes, fromHex, toBase64, toBase64Url, toBytes, toHex };
|
|
443
|
+
|
|
444
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["sign","hashParams","verifyParams","verify"],"sources":["../src/bytes/bytes.ts","../src/engines/fake-engine.ts","../src/engines/web-crypto-engine.ts","../src/jwt/decode.ts","../src/actions/encoding-actions.ts","../src/port/crypto-engine.port.ts","../src/actions/hash-actions.ts","../src/actions/jwt-actions.ts","../src/actions/password-actions.ts","../src/actions/index.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["// Conversions between the shapes crypto work needs: text, bytes, hex, base64.\n// All pure, all platform-neutral.\n\n/** Bytes backed by a plain `ArrayBuffer`, the only shape WebCrypto accepts. */\nexport type Bytes = Uint8Array<ArrayBuffer>;\n\n/** UTF-8 encode a string. */\nexport function toBytes(text: string): Bytes {\n return new TextEncoder().encode(text) as Bytes;\n}\n\n/** UTF-8 decode bytes back to a string. Invalid sequences become the replacement character. */\nexport function fromBytes(bytes: Uint8Array): string {\n return new TextDecoder().decode(bytes);\n}\n\n/** Lowercase hex, two characters per byte. */\nexport function toHex(bytes: Uint8Array): string {\n return [...bytes].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** Read a hex string back into bytes. A trailing odd character is dropped. */\nexport function fromHex(hex: string): Bytes {\n const pairs = hex.match(/../g) ?? [];\n return Uint8Array.from(pairs.map((pair) => Number.parseInt(pair, 16))) as Bytes;\n}\n\n/** Standard base64, padded. */\nexport function toBase64(bytes: Uint8Array): string {\n return btoa(String.fromCharCode(...bytes));\n}\n\n/**\n * Read standard base64 back into bytes.\n *\n * @throws DOMException when the text is not valid base64.\n */\nexport function fromBase64(text: string): Bytes {\n return Uint8Array.from(atob(text), (char) => char.charCodeAt(0)) as Bytes;\n}\n\n/** Base64url, the flavour JWT uses: `+/` become `-_` and padding is dropped. */\nexport function toBase64Url(bytes: Uint8Array): string {\n return toBase64(bytes).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Read base64url back into bytes, restoring the padding first.\n *\n * @throws DOMException when the text is not valid base64url.\n */\nexport function fromBase64Url(text: string): Bytes {\n const padded = text.replace(/-/g, \"+\").replace(/_/g, \"/\");\n return fromBase64(padded.padEnd(Math.ceil(padded.length / 4) * 4, \"=\"));\n}\n\n/**\n * Compare two strings in time that does not depend on where they differ.\n *\n * Use this for every signature and digest check: `===` returns early on the\n * first mismatched byte, which leaks the correct prefix to a patient attacker.\n */\nexport function equals(left: string, right: string): boolean {\n if (left.length !== right.length) return false;\n let diff = 0;\n for (let index = 0; index < left.length; index++) {\n diff |= left.charCodeAt(index) ^ right.charCodeAt(index);\n }\n return diff === 0;\n}\n","import type { CryptoEngine } from \"../port/index.js\";\n\n/**\n * A deterministic stand-in for tests: same input, same output, no platform crypto.\n *\n * Not secure and not meant to be. It exists so assertions over a digest, an HMAC\n * or a random token stay reproducible from run to run.\n */\nexport function createFakeCryptoEngine(): CryptoEngine {\n let counter = 0;\n return {\n digest: ({ algorithm, data }) => Promise.resolve(stable(`${algorithm}|${data}`)),\n hmac: ({ algorithm, key, data }) => Promise.resolve(stable(`${algorithm}|${key}|${data}`)),\n derive: (args) =>\n Promise.resolve(stable(`${args.algorithm}|${args.password}|${args.salt}|${args.iterations}`)),\n randomBytes: (size) => {\n counter += 1;\n return sized(stable(`bytes|${counter}`), size);\n },\n };\n}\n\n// FNV-1a, run over the seed with a changing suffix until 64 hex digits are filled.\nfunction stable(seed: string): string {\n let out = \"\";\n for (let round = 0; out.length < 64; round++) {\n out += fnv1a(`${seed}|${round}`).toString(16).padStart(8, \"0\");\n }\n return out.slice(0, 64);\n}\n\nfunction fnv1a(text: string): number {\n let hash = 0x811c9dc5;\n for (let index = 0; index < text.length; index++) {\n hash ^= text.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193) >>> 0;\n }\n return hash >>> 0;\n}\n\nfunction sized(hex: string, size: number): string {\n return hex.repeat(Math.ceil((size * 2) / hex.length)).slice(0, size * 2);\n}\n","import { toBytes, toHex } from \"../bytes/index.js\";\nimport type { CryptoEngine, DeriveArgs, HashAlgorithm } from \"../port/index.js\";\n\nconst NAMES: Record<HashAlgorithm, string> = {\n sha1: \"SHA-1\",\n sha256: \"SHA-256\",\n sha384: \"SHA-384\",\n sha512: \"SHA-512\",\n};\n\n/**\n * The real engine, backed by the platform's global WebCrypto.\n *\n * WebCrypto exists in Node 24 and in browsers alike, so nothing here imports\n * `node:crypto` and the package stays platform-neutral.\n */\nexport function createWebCryptoEngine(): CryptoEngine {\n return {\n digest: async ({ algorithm, data }) =>\n toHex(new Uint8Array(await crypto.subtle.digest(NAMES[algorithm], toBytes(data)))),\n hmac: async ({ algorithm, key, data }) =>\n toHex(new Uint8Array(await sign(algorithm, key, data))),\n derive: async (args) => toHex(new Uint8Array(await deriveBits(args))),\n randomBytes: (size) => toHex(crypto.getRandomValues(new Uint8Array(size))),\n };\n}\n\nasync function sign(algorithm: HashAlgorithm, key: string, data: string): Promise<ArrayBuffer> {\n const spec = { name: \"HMAC\", hash: NAMES[algorithm] };\n const material = await crypto.subtle.importKey(\"raw\", toBytes(key), spec, false, [\"sign\"]);\n return crypto.subtle.sign(\"HMAC\", material, toBytes(data));\n}\n\nasync function deriveBits(args: DeriveArgs): Promise<ArrayBuffer> {\n const material = await crypto.subtle.importKey(\"raw\", toBytes(args.password), \"PBKDF2\", false, [\n \"deriveBits\",\n ]);\n const params = {\n name: \"PBKDF2\",\n salt: toBytes(args.salt),\n iterations: args.iterations,\n hash: NAMES[args.algorithm],\n };\n return crypto.subtle.deriveBits(params, material, 256);\n}\n","import { VennError } from \"@venn-lang/contracts\";\nimport { fromBase64Url, fromBytes } from \"../bytes/index.js\";\nimport type { DecodedJwt } from \"./jwt.types.js\";\n\n/**\n * Split a token and decode its header and payload, without verifying anything.\n *\n * Reading a token's claims and trusting them are separate acts. This is the\n * first; `crypto.jwt.verify` is the second.\n *\n * @param token A compact JWS, `header.payload.signature`.\n * @returns The decoded sections plus the `signingInput` a signature covers.\n * @throws VennError `VN7003` when the token has fewer than two sections, or when\n * a section is not base64url-encoded JSON.\n */\nexport function decodeJwt(token: string): DecodedJwt {\n const parts = token.split(\".\");\n if (parts.length < 2) throw malformed(\"expected header.payload.signature\");\n return {\n header: section(parts[0], \"header\"),\n payload: section(parts[1], \"payload\"),\n signature: parts[2] ?? \"\",\n signingInput: `${parts[0]}.${parts[1]}`,\n };\n}\n\nfunction section(part: string | undefined, what: string): Record<string, unknown> {\n try {\n return JSON.parse(fromBytes(fromBase64Url(part ?? \"\")));\n } catch {\n throw malformed(`its ${what} is not base64url-encoded JSON`);\n }\n}\n\nfunction malformed(detail: string): VennError {\n return new VennError({ code: \"VN7003\", message: `Not a JWT — ${detail}.` });\n}\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport {\n fromBase64,\n fromBase64Url,\n fromBytes,\n toBase64,\n toBase64Url,\n toBytes,\n} from \"../bytes/index.js\";\n\n/** The `crypto.base64.*` and `crypto.base64url.*` verbs, both directions. */\nexport const encodingActions: ActionDefinition[] = [\n text(\"base64.encode\", \"Encode a string as base64.\", (value) => toBase64(toBytes(value))),\n text(\"base64.decode\", \"Decode base64 back to a string.\", (value) => fromBytes(fromBase64(value))),\n text(\"base64url.encode\", \"Encode a string as base64url (JWT's flavour).\", (value) =>\n toBase64Url(toBytes(value)),\n ),\n text(\"base64url.decode\", \"Decode base64url back to a string.\", (value) =>\n fromBytes(fromBase64Url(value)),\n ),\n];\n\nfunction text(name: string, doc: string, transform: (value: string) => string): ActionDefinition {\n return defineAction({\n name,\n doc,\n // All four share one shape, a string in and a string out, so the signature\n // lives here instead of being repeated at each call above.\n args: [arg(\"text\", t.string, \"What to encode or decode.\")],\n result: t.string,\n run: (_ctx, input) => transform(String(input.args[0] ?? \"\")),\n });\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { CryptoEngine } from \"./crypto-engine.types.js\";\n\n/**\n * The port every `crypto` verb reaches its primitives through.\n *\n * Bound by the host to `createWebCryptoEngine` or `createFakeCryptoEngine`.\n * Requires no capability, because WebCrypto is present on every target.\n */\nexport const CryptoEnginePort: Port<CryptoEngine> = {\n id: \"venn.port.crypto-engine\",\n version: 1,\n requires: [],\n methods: [\"digest\", \"hmac\", \"derive\", \"randomBytes\"],\n};\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { fromHex, toHex } from \"../bytes/index.js\";\nimport { CryptoEnginePort, type HashAlgorithm } from \"../port/index.js\";\n\nconst algorithm = z.enum([\"sha1\", \"sha256\", \"sha384\", \"sha512\"]).default(\"sha256\");\nconst hashParams = z.object({ algorithm }).optional();\nconst hmacParams = z.object({ key: z.string(), algorithm });\nconst bytesParams = z.object({ size: z.number().int().positive().default(16) }).optional();\n\n/** The `crypto.hash`, `crypto.hmac`, `crypto.randomBytes` and `crypto.uuid` verbs. */\nexport const hashActions: ActionDefinition[] = [\n defineAction({\n name: \"hash\",\n doc: \"Digest a string, hex-encoded. Defaults to sha256.\",\n params: hashParams,\n args: [arg(\"data\", t.string, \"What to digest.\")],\n result: t.string,\n run: (ctx, input) =>\n ctx.port(CryptoEnginePort).digest({\n algorithm: algorithmOf(input.params),\n data: String(input.args[0] ?? \"\"),\n }),\n }),\n defineAction({\n name: \"hmac\",\n doc: \"Keyed digest of a string, hex-encoded.\",\n params: hmacParams,\n args: [arg(\"data\", t.string, \"What to sign. The key goes in the options.\")],\n result: t.string,\n run: (ctx, input) => {\n const params = input.params as { key: string; algorithm: HashAlgorithm };\n const data = String(input.args[0] ?? \"\");\n return ctx\n .port(CryptoEnginePort)\n .hmac({ algorithm: params.algorithm, key: params.key, data });\n },\n }),\n defineAction({\n name: \"randomBytes\",\n doc: \"Random bytes, hex-encoded.\",\n params: bytesParams,\n // `size` is an option, so the verb takes nothing positionally.\n result: t.string,\n run: (ctx, input) => {\n const size = (input.params as { size?: number } | undefined)?.size ?? 16;\n return ctx.port(CryptoEnginePort).randomBytes(size);\n },\n }),\n defineAction({\n name: \"uuid\",\n doc: \"A random UUID v4.\",\n result: t.string,\n run: (ctx) => uuidFrom(ctx.port(CryptoEnginePort).randomBytes(16)),\n }),\n];\n\nfunction algorithmOf(params: unknown): HashAlgorithm {\n return (params as { algorithm?: HashAlgorithm } | undefined)?.algorithm ?? \"sha256\";\n}\n\n// Stamp the version and variant bits, then group as 8-4-4-4-12.\nfunction uuidFrom(hex: string): string {\n const bytes = fromHex(hex);\n bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;\n bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;\n const text = toHex(bytes);\n const groups = [text.slice(0, 8), text.slice(8, 12), text.slice(12, 16), text.slice(16, 20)];\n return [...groups, text.slice(20, 32)].join(\"-\");\n}\n","import { type ActionContext, type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { equals, fromHex, toBase64Url, toBytes } from \"../bytes/index.js\";\nimport { decodeJwt } from \"../jwt/index.js\";\nimport { CryptoEnginePort, type HashAlgorithm } from \"../port/index.js\";\n\nconst ALGORITHMS: Record<string, HashAlgorithm> = {\n HS256: \"sha256\",\n HS384: \"sha384\",\n HS512: \"sha512\",\n};\n\nconst signParams = z.object({\n payload: z.record(z.string(), z.unknown()),\n secret: z.string(),\n algorithm: z.enum([\"HS256\", \"HS384\", \"HS512\"]).default(\"HS256\"),\n});\nconst verifyParams = z.object({ secret: z.string() });\n\n/** The `crypto.jwt.*` verbs: read a token, mint one, check one. */\nexport const jwtActions: ActionDefinition[] = [\n defineAction({\n name: \"jwt.decode\",\n doc: \"Take a token apart without verifying it: header, payload, signature.\",\n args: [arg(\"token\", t.string, \"The token to take apart. Nothing is verified.\")],\n result: t.ref(\"crypto.Jwt\"),\n run: (_ctx, input) => decodeJwt(String(input.args[0] ?? \"\")),\n }),\n defineAction({\n name: \"jwt.sign\",\n doc: \"Mint an HMAC-signed token.\",\n params: signParams,\n // Payload, secret and algorithm are all options, so nothing goes positionally.\n result: t.string,\n run: (ctx, input) => sign(ctx, input.params as z.infer<typeof signParams>),\n }),\n defineAction({\n name: \"jwt.verify\",\n doc: \"True when the token's signature matches the secret.\",\n params: verifyParams,\n args: [arg(\"token\", t.string, \"The token to check against the secret in the options.\")],\n result: t.bool,\n run: (ctx, input) =>\n verify(ctx, String(input.args[0] ?? \"\"), input.params as { secret: string }),\n }),\n];\n\nasync function sign(ctx: ActionContext, params: z.infer<typeof signParams>): Promise<string> {\n const header = { alg: params.algorithm, typ: \"JWT\" };\n const input = `${encode(header)}.${encode(params.payload)}`;\n const mac = await ctx.port(CryptoEnginePort).hmac({\n algorithm: ALGORITHMS[params.algorithm] as HashAlgorithm,\n key: params.secret,\n data: input,\n });\n return `${input}.${toBase64Url(fromHex(mac))}`;\n}\n\nasync function verify(\n ctx: ActionContext,\n token: string,\n params: { secret: string },\n): Promise<boolean> {\n const decoded = decodeJwt(token);\n const algorithm = ALGORITHMS[String(decoded.header.alg ?? \"\")];\n if (!algorithm) return false;\n const mac = await ctx.port(CryptoEnginePort).hmac({\n algorithm,\n key: params.secret,\n data: decoded.signingInput,\n });\n return equals(toBase64Url(fromHex(mac)), decoded.signature);\n}\n\nfunction encode(value: unknown): string {\n return toBase64Url(toBytes(JSON.stringify(value)));\n}\n","import { type ActionContext, type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { equals } from \"../bytes/index.js\";\nimport { CryptoEnginePort, type HashAlgorithm } from \"../port/index.js\";\n\nconst SCHEME = \"pbkdf2\";\nconst DEFAULT_ITERATIONS = 100_000;\n\nconst hashParams = z\n .object({\n iterations: z.number().int().positive().default(DEFAULT_ITERATIONS),\n algorithm: z.enum([\"sha256\", \"sha512\"]).default(\"sha256\"),\n })\n .optional();\nconst verifyParams = z.object({ hash: z.string() });\n\n/**\n * The `crypto.password.hash` and `crypto.password.verify` verbs.\n *\n * PBKDF2 rather than bcrypt, because WebCrypto offers PBKDF2 and so the package\n * needs no native module. The encoded form carries its own cost parameters,\n * `pbkdf2$sha256$iterations$salt$derived`, so a stored hash stays verifiable\n * after the defaults here change.\n */\nexport const passwordActions: ActionDefinition[] = [\n defineAction({\n name: \"password.hash\",\n doc: \"Hash a password with PBKDF2, returning a self-describing string.\",\n params: hashParams,\n args: [arg(\"password\", t.string, \"The password in the clear.\")],\n result: t.string,\n run: (ctx, input) => hash(ctx, String(input.args[0] ?? \"\"), input.params),\n }),\n defineAction({\n name: \"password.verify\",\n doc: \"True when the password matches a hash produced by `password.hash`.\",\n params: verifyParams,\n args: [arg(\"password\", t.string, \"The password in the clear, to check against the hash.\")],\n result: t.bool,\n run: (ctx, input) =>\n verify(ctx, String(input.args[0] ?? \"\"), (input.params as { hash: string }).hash),\n }),\n];\n\nasync function hash(ctx: ActionContext, password: string, params: unknown): Promise<string> {\n const options = (params ?? {}) as { iterations?: number; algorithm?: HashAlgorithm };\n const iterations = options.iterations ?? DEFAULT_ITERATIONS;\n const algorithm = options.algorithm ?? \"sha256\";\n const salt = ctx.port(CryptoEnginePort).randomBytes(16);\n const derived = await ctx\n .port(CryptoEnginePort)\n .derive({ password, salt, iterations, algorithm });\n return [SCHEME, algorithm, iterations, salt, derived].join(\"$\");\n}\n\nasync function verify(ctx: ActionContext, password: string, encoded: string): Promise<boolean> {\n const [scheme, algorithm, iterations, salt, derived] = encoded.split(\"$\");\n if (scheme !== SCHEME || !algorithm || !salt || !derived) return false;\n const again = await ctx.port(CryptoEnginePort).derive({\n password,\n salt,\n iterations: Number(iterations),\n algorithm: algorithm as HashAlgorithm,\n });\n return equals(again, derived);\n}\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { encodingActions } from \"./encoding-actions.js\";\nimport { hashActions } from \"./hash-actions.js\";\nimport { jwtActions } from \"./jwt-actions.js\";\nimport { passwordActions } from \"./password-actions.js\";\n\n/** The crypto namespace's verbs. */\nexport const cryptoActions: ActionDefinition[] = [\n ...hashActions,\n ...encodingActions,\n ...jwtActions,\n ...passwordActions,\n];\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The named types `@venn-lang/crypto` publishes to scripts, keyed by their bare name.\n *\n * One name is enough: every other verb answers with a string or a boolean, which\n * reads better inline than behind a name that adds nothing. Hand-mirrored from\n * `DecodedJwt` in `jwt/jwt.types.ts`, so change the two together.\n */\nexport const cryptoTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /**\n * A token taken apart. The claims are whatever the issuer put there, so both\n * sections are maps of dynamic rather than a shape nobody can promise.\n */\n Jwt: t.record({\n header: t.map(t.dynamic),\n payload: t.map(t.dynamic),\n signature: t.string,\n signingInput: t.string,\n }),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { cryptoActions } from \"./actions/index.js\";\nimport { cryptoTypeDefs } from \"./types.js\";\n\n/**\n * The `crypto` plugin: digests, encodings, password hashing and JSON Web Tokens.\n *\n * Every primitive comes from `CryptoEnginePort`, so a host can swap WebCrypto for\n * the deterministic fake and keep assertions reproducible. No capability is\n * required: WebCrypto is present in Node and in the browser alike.\n */\nexport const cryptoPlugin: PluginDefinition = definePlugin({\n name: \"venn/crypto\",\n version: \"0.1.0\",\n namespace: \"crypto\",\n actions: cryptoActions,\n typeDefs: cryptoTypeDefs,\n});\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@venn-lang/crypto",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The crypto namespace: digests, HMACs, base64, PBKDF2 password hashing and JSON Web Tokens.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"venn",
|
|
7
|
+
"testing",
|
|
8
|
+
"e2e",
|
|
9
|
+
"crypto"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-crypto#readme",
|
|
12
|
+
"bugs": "https://github.com/venn-lang/venn/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/venn-lang/venn.git",
|
|
16
|
+
"directory": "packages/std-crypto"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Vinicius Borges",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"development": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"!src/**/*.test.ts",
|
|
34
|
+
"!src/**/*.suite.ts"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@venn-lang/contracts": "0.1.0",
|
|
41
|
+
"@venn-lang/sdk": "0.1.0",
|
|
42
|
+
"@venn-lang/types": "0.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsdown": "^0.22.14",
|
|
46
|
+
"typescript": "^7.0.2",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import {
|
|
4
|
+
fromBase64,
|
|
5
|
+
fromBase64Url,
|
|
6
|
+
fromBytes,
|
|
7
|
+
toBase64,
|
|
8
|
+
toBase64Url,
|
|
9
|
+
toBytes,
|
|
10
|
+
} from "../bytes/index.js";
|
|
11
|
+
|
|
12
|
+
/** The `crypto.base64.*` and `crypto.base64url.*` verbs, both directions. */
|
|
13
|
+
export const encodingActions: ActionDefinition[] = [
|
|
14
|
+
text("base64.encode", "Encode a string as base64.", (value) => toBase64(toBytes(value))),
|
|
15
|
+
text("base64.decode", "Decode base64 back to a string.", (value) => fromBytes(fromBase64(value))),
|
|
16
|
+
text("base64url.encode", "Encode a string as base64url (JWT's flavour).", (value) =>
|
|
17
|
+
toBase64Url(toBytes(value)),
|
|
18
|
+
),
|
|
19
|
+
text("base64url.decode", "Decode base64url back to a string.", (value) =>
|
|
20
|
+
fromBytes(fromBase64Url(value)),
|
|
21
|
+
),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function text(name: string, doc: string, transform: (value: string) => string): ActionDefinition {
|
|
25
|
+
return defineAction({
|
|
26
|
+
name,
|
|
27
|
+
doc,
|
|
28
|
+
// All four share one shape, a string in and a string out, so the signature
|
|
29
|
+
// lives here instead of being repeated at each call above.
|
|
30
|
+
args: [arg("text", t.string, "What to encode or decode.")],
|
|
31
|
+
result: t.string,
|
|
32
|
+
run: (_ctx, input) => transform(String(input.args[0] ?? "")),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { fromHex, toHex } from "../bytes/index.js";
|
|
4
|
+
import { CryptoEnginePort, type HashAlgorithm } from "../port/index.js";
|
|
5
|
+
|
|
6
|
+
const algorithm = z.enum(["sha1", "sha256", "sha384", "sha512"]).default("sha256");
|
|
7
|
+
const hashParams = z.object({ algorithm }).optional();
|
|
8
|
+
const hmacParams = z.object({ key: z.string(), algorithm });
|
|
9
|
+
const bytesParams = z.object({ size: z.number().int().positive().default(16) }).optional();
|
|
10
|
+
|
|
11
|
+
/** The `crypto.hash`, `crypto.hmac`, `crypto.randomBytes` and `crypto.uuid` verbs. */
|
|
12
|
+
export const hashActions: ActionDefinition[] = [
|
|
13
|
+
defineAction({
|
|
14
|
+
name: "hash",
|
|
15
|
+
doc: "Digest a string, hex-encoded. Defaults to sha256.",
|
|
16
|
+
params: hashParams,
|
|
17
|
+
args: [arg("data", t.string, "What to digest.")],
|
|
18
|
+
result: t.string,
|
|
19
|
+
run: (ctx, input) =>
|
|
20
|
+
ctx.port(CryptoEnginePort).digest({
|
|
21
|
+
algorithm: algorithmOf(input.params),
|
|
22
|
+
data: String(input.args[0] ?? ""),
|
|
23
|
+
}),
|
|
24
|
+
}),
|
|
25
|
+
defineAction({
|
|
26
|
+
name: "hmac",
|
|
27
|
+
doc: "Keyed digest of a string, hex-encoded.",
|
|
28
|
+
params: hmacParams,
|
|
29
|
+
args: [arg("data", t.string, "What to sign. The key goes in the options.")],
|
|
30
|
+
result: t.string,
|
|
31
|
+
run: (ctx, input) => {
|
|
32
|
+
const params = input.params as { key: string; algorithm: HashAlgorithm };
|
|
33
|
+
const data = String(input.args[0] ?? "");
|
|
34
|
+
return ctx
|
|
35
|
+
.port(CryptoEnginePort)
|
|
36
|
+
.hmac({ algorithm: params.algorithm, key: params.key, data });
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
defineAction({
|
|
40
|
+
name: "randomBytes",
|
|
41
|
+
doc: "Random bytes, hex-encoded.",
|
|
42
|
+
params: bytesParams,
|
|
43
|
+
// `size` is an option, so the verb takes nothing positionally.
|
|
44
|
+
result: t.string,
|
|
45
|
+
run: (ctx, input) => {
|
|
46
|
+
const size = (input.params as { size?: number } | undefined)?.size ?? 16;
|
|
47
|
+
return ctx.port(CryptoEnginePort).randomBytes(size);
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
defineAction({
|
|
51
|
+
name: "uuid",
|
|
52
|
+
doc: "A random UUID v4.",
|
|
53
|
+
result: t.string,
|
|
54
|
+
run: (ctx) => uuidFrom(ctx.port(CryptoEnginePort).randomBytes(16)),
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function algorithmOf(params: unknown): HashAlgorithm {
|
|
59
|
+
return (params as { algorithm?: HashAlgorithm } | undefined)?.algorithm ?? "sha256";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Stamp the version and variant bits, then group as 8-4-4-4-12.
|
|
63
|
+
function uuidFrom(hex: string): string {
|
|
64
|
+
const bytes = fromHex(hex);
|
|
65
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
|
66
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
67
|
+
const text = toHex(bytes);
|
|
68
|
+
const groups = [text.slice(0, 8), text.slice(8, 12), text.slice(12, 16), text.slice(16, 20)];
|
|
69
|
+
return [...groups, text.slice(20, 32)].join("-");
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ActionDefinition } from "@venn-lang/sdk";
|
|
2
|
+
import { encodingActions } from "./encoding-actions.js";
|
|
3
|
+
import { hashActions } from "./hash-actions.js";
|
|
4
|
+
import { jwtActions } from "./jwt-actions.js";
|
|
5
|
+
import { passwordActions } from "./password-actions.js";
|
|
6
|
+
|
|
7
|
+
/** The crypto namespace's verbs. */
|
|
8
|
+
export const cryptoActions: ActionDefinition[] = [
|
|
9
|
+
...hashActions,
|
|
10
|
+
...encodingActions,
|
|
11
|
+
...jwtActions,
|
|
12
|
+
...passwordActions,
|
|
13
|
+
];
|