@sebastienrousseau/crypto-cli 0.0.8 → 0.0.24
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 +685 -3
- package/dist/cli.js +37 -196
- package/dist/commands/decrypt.command.js +2 -2
- package/dist/commands/encrypt.command.js +2 -2
- package/dist/commands/generate.command.js +2 -2
- package/dist/commands/modern/cbom.command.d.ts +2 -1
- package/dist/commands/modern/cbom.command.d.ts.map +1 -1
- package/dist/commands/modern/cbom.command.js +99 -146
- package/dist/commands/modern/cbom.core.d.ts +5 -0
- package/dist/commands/modern/cbom.core.d.ts.map +1 -0
- package/dist/commands/modern/cbom.core.js +57 -0
- package/dist/commands/modern/encrypt.command.d.ts.map +1 -1
- package/dist/commands/modern/encrypt.command.js +42 -63
- package/dist/commands/modern/hash.command.d.ts.map +1 -1
- package/dist/commands/modern/hash.command.js +1 -1
- package/dist/commands/modern/keygen.command.js +1 -1
- package/dist/commands/modern/password-hash.command.d.ts.map +1 -1
- package/dist/commands/modern/password-hash.command.js +28 -27
- package/dist/commands/modern/sign.command.d.ts.map +1 -1
- package/dist/commands/modern/sign.command.js +83 -73
- package/dist/commands/reformat.command.js +2 -2
- package/dist/commands/revoke.command.js +2 -2
- package/dist/commands/session.command.js +2 -2
- package/dist/commands/sign.command.js +2 -2
- package/dist/commands/verify.command.js +2 -2
- package/dist/menu.d.ts +12 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +107 -0
- package/dist/program/cbom.d.ts +4 -0
- package/dist/program/cbom.d.ts.map +1 -0
- package/dist/program/cbom.js +49 -0
- package/dist/program/encrypt.d.ts +6 -0
- package/dist/program/encrypt.d.ts.map +1 -0
- package/dist/program/encrypt.js +73 -0
- package/dist/program/hash.d.ts +4 -0
- package/dist/program/hash.d.ts.map +1 -0
- package/dist/program/hash.js +21 -0
- package/dist/program/hpke.d.ts +4 -0
- package/dist/program/hpke.d.ts.map +1 -0
- package/dist/program/hpke.js +111 -0
- package/dist/program/index.d.ts +7 -0
- package/dist/program/index.d.ts.map +1 -0
- package/dist/program/index.js +77 -0
- package/dist/program/io.d.ts +35 -0
- package/dist/program/io.d.ts.map +1 -0
- package/dist/program/io.js +65 -0
- package/dist/program/keygen.d.ts +4 -0
- package/dist/program/keygen.d.ts.map +1 -0
- package/dist/program/keygen.js +32 -0
- package/dist/program/kms.d.ts +8 -0
- package/dist/program/kms.d.ts.map +1 -0
- package/dist/program/kms.js +247 -0
- package/dist/program/opaque.d.ts +4 -0
- package/dist/program/opaque.d.ts.map +1 -0
- package/dist/program/opaque.js +209 -0
- package/dist/program/password.d.ts +4 -0
- package/dist/program/password.d.ts.map +1 -0
- package/dist/program/password.js +73 -0
- package/dist/program/secrets.d.ts +30 -0
- package/dist/program/secrets.d.ts.map +1 -0
- package/dist/program/secrets.js +70 -0
- package/dist/program/sign.d.ts +18 -0
- package/dist/program/sign.d.ts.map +1 -0
- package/dist/program/sign.js +130 -0
- package/dist/program/stream-multi.d.ts +7 -0
- package/dist/program/stream-multi.d.ts.map +1 -0
- package/dist/program/stream-multi.js +163 -0
- package/dist/program/stream.d.ts +6 -0
- package/dist/program/stream.d.ts.map +1 -0
- package/dist/program/stream.js +102 -0
- package/dist/program/usage.d.ts +7 -0
- package/dist/program/usage.d.ts.map +1 -0
- package/dist/program/usage.js +36 -0
- package/package.json +21 -34
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCliKmsProvider = registerCliKmsProvider;
|
|
4
|
+
exports.resetCliKmsProviders = resetCliKmsProviders;
|
|
5
|
+
exports.resolveCliKmsProvider = resolveCliKmsProvider;
|
|
6
|
+
exports.registerKms = registerKms;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const crypto_kms_1 = require("@sebastienrousseau/crypto-kms");
|
|
9
|
+
const io_1 = require("./io");
|
|
10
|
+
const providerRegistry = new Map();
|
|
11
|
+
providerRegistry.set("local", new crypto_kms_1.LocalKmsProvider());
|
|
12
|
+
function registerCliKmsProvider(name, provider) {
|
|
13
|
+
providerRegistry.set(name.toLowerCase(), provider);
|
|
14
|
+
}
|
|
15
|
+
function resetCliKmsProviders() {
|
|
16
|
+
providerRegistry.clear();
|
|
17
|
+
providerRegistry.set("local", new crypto_kms_1.LocalKmsProvider());
|
|
18
|
+
}
|
|
19
|
+
function createCliAwsProvider() {
|
|
20
|
+
return new crypto_kms_1.AwsKmsProvider({
|
|
21
|
+
region: process.env["AWS_REGION"] ?? "us-east-1",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function createCliGcpProvider() {
|
|
25
|
+
return new crypto_kms_1.GcpKmsProvider({
|
|
26
|
+
projectId: process.env["GCP_PROJECT_ID"] ?? "test-project",
|
|
27
|
+
locationId: process.env["GCP_LOCATION_ID"] ?? "global",
|
|
28
|
+
keyRingId: process.env["GCP_KEY_RING_ID"] ?? "test-ring",
|
|
29
|
+
token: process.env["GCP_AUTH_TOKEN"] ?? "test-token",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function createCliVaultProvider() {
|
|
33
|
+
return new crypto_kms_1.VaultKmsProvider({
|
|
34
|
+
address: process.env["VAULT_ADDR"] ?? "http://localhost:8200",
|
|
35
|
+
token: process.env["VAULT_TOKEN"] ?? "test-token",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function createCliAzureProvider() {
|
|
39
|
+
return new crypto_kms_1.AzureKmsProvider({
|
|
40
|
+
vaultUrl: process.env["AZURE_VAULT_URL"] ?? "https://my-vault.vault.azure.net",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function createDefaultCliProvider(key, name) {
|
|
44
|
+
switch (key) {
|
|
45
|
+
case "aws":
|
|
46
|
+
return createCliAwsProvider();
|
|
47
|
+
case "gcp":
|
|
48
|
+
return createCliGcpProvider();
|
|
49
|
+
case "vault":
|
|
50
|
+
return createCliVaultProvider();
|
|
51
|
+
case "azure":
|
|
52
|
+
return createCliAzureProvider();
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unsupported KMS provider: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function resolveCliKmsProvider(name) {
|
|
58
|
+
const key = (name ?? "local").toLowerCase();
|
|
59
|
+
const existing = providerRegistry.get(key);
|
|
60
|
+
if (existing)
|
|
61
|
+
return existing;
|
|
62
|
+
const provider = createDefaultCliProvider(key, name);
|
|
63
|
+
providerRegistry.set(key, provider);
|
|
64
|
+
return provider;
|
|
65
|
+
}
|
|
66
|
+
function parseBytes(text) {
|
|
67
|
+
const isHex = /^[0-9a-fA-F]*$/.test(text) && text.length % 2 === 0;
|
|
68
|
+
return isHex
|
|
69
|
+
? new Uint8Array(Buffer.from(text, "hex"))
|
|
70
|
+
: new Uint8Array(Buffer.from(text, "utf8"));
|
|
71
|
+
}
|
|
72
|
+
async function executeCreateKey(opts, ctx) {
|
|
73
|
+
const provider = resolveCliKmsProvider(opts.provider);
|
|
74
|
+
const meta = await provider.createKey(opts.algorithm, opts.usage);
|
|
75
|
+
const desc = [
|
|
76
|
+
`Key ID: ${meta.keyId}`,
|
|
77
|
+
`Algorithm: ${meta.algorithm}`,
|
|
78
|
+
`Usage: ${meta.usage}`,
|
|
79
|
+
`Provider: ${meta.provider}`,
|
|
80
|
+
].join("\n");
|
|
81
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), meta, desc);
|
|
82
|
+
}
|
|
83
|
+
async function executeWrap(file, opts, ctx) {
|
|
84
|
+
const rawKey = opts.key
|
|
85
|
+
? Buffer.from(opts.key, "utf8")
|
|
86
|
+
: await (0, io_1.readInput)(file, ctx.io);
|
|
87
|
+
const text = (0, io_1.valueOrJsonField)(rawKey, "key").trim();
|
|
88
|
+
const bytes = parseBytes(text);
|
|
89
|
+
const provider = resolveCliKmsProvider(opts.provider);
|
|
90
|
+
if (!provider.wrapKey) {
|
|
91
|
+
throw new Error(`Provider ${provider.name} does not support wrapKey`);
|
|
92
|
+
}
|
|
93
|
+
const res = await provider.wrapKey(opts.keyId, bytes);
|
|
94
|
+
const desc = [
|
|
95
|
+
`Key ID: ${res.keyId}`,
|
|
96
|
+
`Wrapped Key: ${res.wrappedKey}`,
|
|
97
|
+
`Provider: ${provider.name}`,
|
|
98
|
+
].join("\n");
|
|
99
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), { ...res, provider: provider.name }, desc);
|
|
100
|
+
}
|
|
101
|
+
async function executeUnwrap(file, opts, ctx) {
|
|
102
|
+
const rawWk = opts.wrappedKey
|
|
103
|
+
? Buffer.from(opts.wrappedKey, "utf8")
|
|
104
|
+
: await (0, io_1.readInput)(file, ctx.io);
|
|
105
|
+
const wrappedKey = (0, io_1.valueOrJsonField)(rawWk, "wrappedKey").trim();
|
|
106
|
+
const provider = resolveCliKmsProvider(opts.provider);
|
|
107
|
+
if (!provider.unwrapKey) {
|
|
108
|
+
throw new Error(`Provider ${provider.name} does not support unwrapKey`);
|
|
109
|
+
}
|
|
110
|
+
const res = await provider.unwrapKey(opts.keyId, wrappedKey);
|
|
111
|
+
const hex = Buffer.from(res.unwrappedKey).toString("hex");
|
|
112
|
+
const data = { unwrappedKey: hex, keyId: res.keyId, provider: provider.name };
|
|
113
|
+
const desc = [
|
|
114
|
+
`Key ID: ${res.keyId}`,
|
|
115
|
+
`Unwrapped Key: ${hex}`,
|
|
116
|
+
`Provider: ${provider.name}`,
|
|
117
|
+
].join("\n");
|
|
118
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), data, desc);
|
|
119
|
+
}
|
|
120
|
+
async function executeDataKey(opts, ctx) {
|
|
121
|
+
const provider = resolveCliKmsProvider(opts.provider);
|
|
122
|
+
const res = await provider.generateDataKey(opts.keyId, opts.keySpec);
|
|
123
|
+
const hex = Buffer.from(res.plaintext).toString("hex");
|
|
124
|
+
const data = {
|
|
125
|
+
plaintext: hex,
|
|
126
|
+
ciphertext: res.ciphertext,
|
|
127
|
+
keyId: opts.keyId,
|
|
128
|
+
provider: provider.name,
|
|
129
|
+
};
|
|
130
|
+
const desc = [
|
|
131
|
+
`Key ID: ${opts.keyId}`,
|
|
132
|
+
`Plaintext: ${hex}`,
|
|
133
|
+
`Ciphertext: ${res.ciphertext}`,
|
|
134
|
+
`Provider: ${provider.name}`,
|
|
135
|
+
].join("\n");
|
|
136
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), data, desc);
|
|
137
|
+
}
|
|
138
|
+
async function executeEncrypt(file, opts, ctx) {
|
|
139
|
+
const raw = opts.data
|
|
140
|
+
? Buffer.from(opts.data, "utf8")
|
|
141
|
+
: await (0, io_1.readInput)(file, ctx.io);
|
|
142
|
+
const text = (0, io_1.valueOrJsonField)(raw, "data").trim();
|
|
143
|
+
const bytes = parseBytes(text);
|
|
144
|
+
const provider = resolveCliKmsProvider(opts.provider);
|
|
145
|
+
const res = await provider.encrypt(opts.keyId, bytes);
|
|
146
|
+
const data = { ...res, provider: provider.name };
|
|
147
|
+
const desc = [
|
|
148
|
+
`Key ID: ${res.keyId}`,
|
|
149
|
+
`Ciphertext: ${res.ciphertext}`,
|
|
150
|
+
`Provider: ${provider.name}`,
|
|
151
|
+
].join("\n");
|
|
152
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), data, desc);
|
|
153
|
+
}
|
|
154
|
+
async function executeDecrypt(file, opts, ctx) {
|
|
155
|
+
const raw = opts.ciphertext
|
|
156
|
+
? Buffer.from(opts.ciphertext, "utf8")
|
|
157
|
+
: await (0, io_1.readInput)(file, ctx.io);
|
|
158
|
+
const ct = (0, io_1.valueOrJsonField)(raw, "ciphertext").trim();
|
|
159
|
+
const provider = resolveCliKmsProvider(opts.provider);
|
|
160
|
+
const res = await provider.decrypt(opts.keyId, ct);
|
|
161
|
+
const text = Buffer.from(res.plaintext).toString("utf8");
|
|
162
|
+
const hex = Buffer.from(res.plaintext).toString("hex");
|
|
163
|
+
const data = {
|
|
164
|
+
plaintext: text,
|
|
165
|
+
hex,
|
|
166
|
+
keyId: res.keyId,
|
|
167
|
+
provider: provider.name,
|
|
168
|
+
};
|
|
169
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), data, text);
|
|
170
|
+
}
|
|
171
|
+
function registerKmsKeyOps(kms, ctx) {
|
|
172
|
+
kms
|
|
173
|
+
.command("create-key")
|
|
174
|
+
.description("Create a new managed key in KMS")
|
|
175
|
+
.option("-a, --algorithm <alg>", "Key algorithm", "aes-256-gcm")
|
|
176
|
+
.addOption(new commander_1.Option("-u, --usage <usage>", "Key usage")
|
|
177
|
+
.choices(["encrypt", "sign", "wrap"])
|
|
178
|
+
.default("encrypt"))
|
|
179
|
+
.option("-p, --provider <name>", "KMS provider (local, aws, gcp, vault, azure)", "local")
|
|
180
|
+
.option("--json", "Emit output as JSON")
|
|
181
|
+
.action(async (opts) => {
|
|
182
|
+
await executeCreateKey(opts, ctx);
|
|
183
|
+
});
|
|
184
|
+
kms
|
|
185
|
+
.command("generate-data-key")
|
|
186
|
+
.description("Generate a Data Encryption Key (DEK) via KMS")
|
|
187
|
+
.requiredOption("-k, --key-id <id>", "Key ID")
|
|
188
|
+
.option("-s, --key-spec <spec>", "Key spec (e.g. AES_256)", "AES_256")
|
|
189
|
+
.option("-p, --provider <name>", "KMS provider", "local")
|
|
190
|
+
.option("--json", "Emit output as JSON")
|
|
191
|
+
.action(async (opts) => {
|
|
192
|
+
await executeDataKey(opts, ctx);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function registerKmsWrapOps(kms, ctx) {
|
|
196
|
+
kms
|
|
197
|
+
.command("wrap [file]")
|
|
198
|
+
.description("Wrap (encrypt) a key using a KMS key")
|
|
199
|
+
.requiredOption("-k, --key-id <id>", "Wrapping Key ID")
|
|
200
|
+
.option("-w, --key <hex>", "Raw key material to wrap")
|
|
201
|
+
.option("-p, --provider <name>", "KMS provider", "local")
|
|
202
|
+
.option("--json", "Emit output as JSON")
|
|
203
|
+
.action(async (file, opts) => {
|
|
204
|
+
await executeWrap(file, opts, ctx);
|
|
205
|
+
});
|
|
206
|
+
kms
|
|
207
|
+
.command("unwrap [file]")
|
|
208
|
+
.description("Unwrap a wrapped key using a KMS key")
|
|
209
|
+
.requiredOption("-k, --key-id <id>", "Wrapping Key ID")
|
|
210
|
+
.option("-w, --wrapped-key <str>", "Wrapped key string")
|
|
211
|
+
.option("-p, --provider <name>", "KMS provider", "local")
|
|
212
|
+
.option("--json", "Emit output as JSON")
|
|
213
|
+
.action(async (file, opts) => {
|
|
214
|
+
await executeUnwrap(file, opts, ctx);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function registerKmsCipherOps(kms, ctx) {
|
|
218
|
+
kms
|
|
219
|
+
.command("encrypt [file]")
|
|
220
|
+
.description("Encrypt data using a KMS key")
|
|
221
|
+
.requiredOption("-k, --key-id <id>", "Key ID")
|
|
222
|
+
.option("-d, --data <text>", "Plaintext data to encrypt")
|
|
223
|
+
.option("-p, --provider <name>", "KMS provider", "local")
|
|
224
|
+
.option("--json", "Emit output as JSON")
|
|
225
|
+
.action(async (file, opts) => {
|
|
226
|
+
await executeEncrypt(file, opts, ctx);
|
|
227
|
+
});
|
|
228
|
+
kms
|
|
229
|
+
.command("decrypt [file]")
|
|
230
|
+
.description("Decrypt ciphertext using a KMS key")
|
|
231
|
+
.requiredOption("-k, --key-id <id>", "Key ID")
|
|
232
|
+
.option("-c, --ciphertext <str>", "Ciphertext to decrypt")
|
|
233
|
+
.option("-p, --provider <name>", "KMS provider", "local")
|
|
234
|
+
.option("--json", "Emit output as JSON")
|
|
235
|
+
.action(async (file, opts) => {
|
|
236
|
+
await executeDecrypt(file, opts, ctx);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function registerKms(program, ctx) {
|
|
240
|
+
const kms = program
|
|
241
|
+
.command("kms")
|
|
242
|
+
.description("Key Management Service (KMS) operations across native cloud providers");
|
|
243
|
+
registerKmsKeyOps(kms, ctx);
|
|
244
|
+
registerKmsWrapOps(kms, ctx);
|
|
245
|
+
registerKmsCipherOps(kms, ctx);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=kms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opaque.d.ts","sourceRoot":"","sources":["../../src/program/opaque.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAwUzD,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,EAAE,KAAK,UAAU,KAAG,OASlE,CAAC"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerOpaque = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const crypto_lib_1 = require("@sebastienrousseau/crypto-lib");
|
|
6
|
+
const io_1 = require("./io");
|
|
7
|
+
const secrets_1 = require("./secrets");
|
|
8
|
+
const { pake } = crypto_lib_1.protocols;
|
|
9
|
+
const SUITE_CHOICES = [
|
|
10
|
+
"P256-SHA256",
|
|
11
|
+
"ristretto255-SHA512",
|
|
12
|
+
];
|
|
13
|
+
const DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
14
|
+
function parseHex(hex) {
|
|
15
|
+
return new Uint8Array(Buffer.from(hex, "hex"));
|
|
16
|
+
}
|
|
17
|
+
function toHex(bytes) {
|
|
18
|
+
return Buffer.from(bytes).toString("hex");
|
|
19
|
+
}
|
|
20
|
+
function getServerUrl(opts) {
|
|
21
|
+
return (opts.serverUrl || process.env["CRYPTO_SERVER_URL"] || DEFAULT_SERVER_URL);
|
|
22
|
+
}
|
|
23
|
+
async function postJson(baseUrl, path, body) {
|
|
24
|
+
const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
throw new Error(`failed to connect to server at ${url}: ${err.message}`, { cause: err });
|
|
35
|
+
}
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
let json;
|
|
38
|
+
try {
|
|
39
|
+
json = JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
catch (parseErr) {
|
|
42
|
+
throw new Error(`server returned non-JSON response (HTTP ${res.status}): ${text.slice(0, 100)}`, { cause: parseErr });
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const p = json;
|
|
46
|
+
const msg = p.detail ?? p.title ?? `HTTP ${res.status}`;
|
|
47
|
+
throw new Error(`OPAQUE server error (${res.status}): ${msg}`);
|
|
48
|
+
}
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
51
|
+
async function promptPassword(ctx, confirm) {
|
|
52
|
+
const ask = ctx.io.promptSecret;
|
|
53
|
+
if (ask === undefined) {
|
|
54
|
+
throw new io_1.UsageError("missing password: pass --password-file <path> or " +
|
|
55
|
+
"--password-stdin (a prompt needs a terminal on stdin)");
|
|
56
|
+
}
|
|
57
|
+
const password = (await ask("Password")) ?? "";
|
|
58
|
+
if (confirm && (await ask("Repeat the password")) !== password) {
|
|
59
|
+
throw new Error("the passwords do not match");
|
|
60
|
+
}
|
|
61
|
+
return password;
|
|
62
|
+
}
|
|
63
|
+
async function readPassword(ctx, opts, confirm) {
|
|
64
|
+
const source = {
|
|
65
|
+
file: opts.passwordFile,
|
|
66
|
+
stdin: opts.passwordStdin,
|
|
67
|
+
};
|
|
68
|
+
const raw = await (0, secrets_1.readSecret)({
|
|
69
|
+
io: ctx.io,
|
|
70
|
+
source,
|
|
71
|
+
flags: secrets_1.PASSWORD_FLAGS,
|
|
72
|
+
dataOnStdin: false,
|
|
73
|
+
checkMode: true,
|
|
74
|
+
});
|
|
75
|
+
const password = raw === undefined
|
|
76
|
+
? await promptPassword(ctx, confirm)
|
|
77
|
+
: raw.toString("utf8").replace(/\r?\n$/, "");
|
|
78
|
+
if (password === "")
|
|
79
|
+
throw new Error("the password is empty");
|
|
80
|
+
return password;
|
|
81
|
+
}
|
|
82
|
+
async function handleSetup(ctx, opts) {
|
|
83
|
+
const suite = opts.suite;
|
|
84
|
+
const setup = pake.createServerSetup({ suite });
|
|
85
|
+
const fake = pake.createFakeRecord({ suite });
|
|
86
|
+
const result = {
|
|
87
|
+
suite,
|
|
88
|
+
serverPublicKey: toHex(setup.serverPublicKey),
|
|
89
|
+
serverPrivateKey: toHex(setup.serverPrivateKey),
|
|
90
|
+
oprfSeed: toHex(setup.oprfSeed),
|
|
91
|
+
fakeRecord: toHex(pake.serializeRegistrationRecord(fake)),
|
|
92
|
+
};
|
|
93
|
+
const formatted = [
|
|
94
|
+
`OPAQUE Server Setup (${suite}):`,
|
|
95
|
+
`Server Public Key: ${result.serverPublicKey}`,
|
|
96
|
+
`Server Private Key: ${result.serverPrivateKey}`,
|
|
97
|
+
`OPRF Seed: ${result.oprfSeed}`,
|
|
98
|
+
`Fake Record: ${result.fakeRecord}`,
|
|
99
|
+
].join("\n");
|
|
100
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), result, formatted);
|
|
101
|
+
}
|
|
102
|
+
async function handleRegister(id, ctx, opts) {
|
|
103
|
+
const suite = opts.suite;
|
|
104
|
+
const serverUrl = getServerUrl(opts);
|
|
105
|
+
const password = await readPassword(ctx, opts, true);
|
|
106
|
+
const { request, blind } = pake.createRegistrationRequest(password, {
|
|
107
|
+
suite,
|
|
108
|
+
});
|
|
109
|
+
const initRes = await postJson(serverUrl, "/v2/opaque/register/init", {
|
|
110
|
+
credentialIdentifier: id,
|
|
111
|
+
request: toHex(pake.serializeRegistrationRequest(request)),
|
|
112
|
+
suite,
|
|
113
|
+
});
|
|
114
|
+
const responseBytes = parseHex(initRes.data.response);
|
|
115
|
+
const regResponse = pake.deserializeRegistrationResponse(responseBytes, suite);
|
|
116
|
+
const { record, exportKey } = pake.finalizeRegistrationRequest(password, blind, regResponse, {}, { suite });
|
|
117
|
+
await postJson(serverUrl, "/v2/opaque/register/finish", {
|
|
118
|
+
credentialIdentifier: id,
|
|
119
|
+
record: toHex(pake.serializeRegistrationRecord(record)),
|
|
120
|
+
suite,
|
|
121
|
+
});
|
|
122
|
+
const result = {
|
|
123
|
+
status: "registered",
|
|
124
|
+
credentialIdentifier: id,
|
|
125
|
+
suite,
|
|
126
|
+
exportKey: toHex(exportKey),
|
|
127
|
+
};
|
|
128
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), result, `Registered credential '${id}' successfully`);
|
|
129
|
+
}
|
|
130
|
+
async function handleLogin(id, ctx, opts) {
|
|
131
|
+
const suite = opts.suite;
|
|
132
|
+
const serverUrl = getServerUrl(opts);
|
|
133
|
+
const password = await readPassword(ctx, opts, false);
|
|
134
|
+
const client = pake.generateKE1(password, { suite });
|
|
135
|
+
const initRes = await postJson(serverUrl, "/v2/opaque/login/init", {
|
|
136
|
+
credentialIdentifier: id,
|
|
137
|
+
ke1: toHex(pake.serializeKE1(client.ke1)),
|
|
138
|
+
suite,
|
|
139
|
+
});
|
|
140
|
+
const ke2Bytes = parseHex(initRes.data.ke2);
|
|
141
|
+
const ke2 = pake.deserializeKE2(ke2Bytes, suite);
|
|
142
|
+
const { ke3, sessionKey, exportKey } = pake.generateKE3(client.state, ke2, {});
|
|
143
|
+
const finishRes = await postJson(serverUrl, "/v2/opaque/login/finish", {
|
|
144
|
+
sessionId: initRes.data.sessionId,
|
|
145
|
+
ke3: toHex(pake.serializeKE3(ke3)),
|
|
146
|
+
});
|
|
147
|
+
const expectedKey = toHex(sessionKey);
|
|
148
|
+
if (finishRes.data.sessionKey !== expectedKey) {
|
|
149
|
+
throw new Error("server session key does not match client session key");
|
|
150
|
+
}
|
|
151
|
+
const result = {
|
|
152
|
+
status: "authenticated",
|
|
153
|
+
credentialIdentifier: id,
|
|
154
|
+
suite,
|
|
155
|
+
sessionKey: expectedKey,
|
|
156
|
+
exportKey: toHex(exportKey),
|
|
157
|
+
};
|
|
158
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), result, `Authenticated as '${id}'. Session key: ${expectedKey}`);
|
|
159
|
+
}
|
|
160
|
+
function registerSetup(parent, ctx) {
|
|
161
|
+
parent
|
|
162
|
+
.command("setup")
|
|
163
|
+
.description("Generate server long-term keys and fake record")
|
|
164
|
+
.addOption(new commander_1.Option("-s, --suite <name>", "ciphersuite")
|
|
165
|
+
.choices(SUITE_CHOICES)
|
|
166
|
+
.default("P256-SHA256"))
|
|
167
|
+
.option("--json", "print result as JSON")
|
|
168
|
+
.action(async (opts) => handleSetup(ctx, opts));
|
|
169
|
+
}
|
|
170
|
+
function registerRegister(parent, ctx) {
|
|
171
|
+
(0, secrets_1.withSecretOptions)(parent
|
|
172
|
+
.command("register <id>")
|
|
173
|
+
.description("Register a password with an OPAQUE server")
|
|
174
|
+
.option("-u, --server-url <url>", "server base URL")
|
|
175
|
+
.addOption(new commander_1.Option("-s, --suite <name>", "ciphersuite")
|
|
176
|
+
.choices(SUITE_CHOICES)
|
|
177
|
+
.default("P256-SHA256"))
|
|
178
|
+
.option("--json", "print result as JSON"), secrets_1.PASSWORD_FLAGS, {
|
|
179
|
+
what: "password",
|
|
180
|
+
format: "one trailing line break is ignored",
|
|
181
|
+
takesData: false,
|
|
182
|
+
}).action(async (id, opts) => handleRegister(id, ctx, opts));
|
|
183
|
+
}
|
|
184
|
+
function registerLogin(parent, ctx) {
|
|
185
|
+
(0, secrets_1.withSecretOptions)(parent
|
|
186
|
+
.command("login <id>")
|
|
187
|
+
.description("Authenticate against an OPAQUE server")
|
|
188
|
+
.option("-u, --server-url <url>", "server base URL")
|
|
189
|
+
.addOption(new commander_1.Option("-s, --suite <name>", "ciphersuite")
|
|
190
|
+
.choices(SUITE_CHOICES)
|
|
191
|
+
.default("P256-SHA256"))
|
|
192
|
+
.option("--json", "print result as JSON"), secrets_1.PASSWORD_FLAGS, {
|
|
193
|
+
what: "password",
|
|
194
|
+
format: "one trailing line break is ignored",
|
|
195
|
+
takesData: false,
|
|
196
|
+
}).action(async (id, opts) => handleLogin(id, ctx, opts));
|
|
197
|
+
}
|
|
198
|
+
const registerOpaque = (program, ctx) => {
|
|
199
|
+
const opaque = program
|
|
200
|
+
.command("opaque")
|
|
201
|
+
.description("OPAQUE zero-knowledge password authentication (RFC 9807)")
|
|
202
|
+
.helpCommand(false);
|
|
203
|
+
registerSetup(opaque, ctx);
|
|
204
|
+
registerRegister(opaque, ctx);
|
|
205
|
+
registerLogin(opaque, ctx);
|
|
206
|
+
return opaque;
|
|
207
|
+
};
|
|
208
|
+
exports.registerOpaque = registerOpaque;
|
|
209
|
+
//# sourceMappingURL=opaque.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/program/password.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,MAAM,CAAC;AA2Hd,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,EAAE,KAAK,UAAU,YAQjE,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPassword = void 0;
|
|
4
|
+
const modern_1 = require("@sebastienrousseau/crypto-lib/modern");
|
|
5
|
+
const io_1 = require("./io");
|
|
6
|
+
const secrets_1 = require("./secrets");
|
|
7
|
+
const promptPassword = async (ctx, dataOnStdin, confirm) => {
|
|
8
|
+
const ask = ctx.io.promptSecret;
|
|
9
|
+
if (ask === undefined || dataOnStdin) {
|
|
10
|
+
throw new io_1.UsageError("missing password: pass --password-file <path> or " +
|
|
11
|
+
"--password-stdin (a prompt needs a terminal on stdin)");
|
|
12
|
+
}
|
|
13
|
+
const password = (await ask("Password")) ?? "";
|
|
14
|
+
if (confirm && (await ask("Repeat the password")) !== password) {
|
|
15
|
+
throw new Error("the passwords do not match");
|
|
16
|
+
}
|
|
17
|
+
return password;
|
|
18
|
+
};
|
|
19
|
+
const readPassword = async (ctx, opts, dataOnStdin, confirm) => {
|
|
20
|
+
const source = {
|
|
21
|
+
file: opts.passwordFile,
|
|
22
|
+
stdin: opts.passwordStdin,
|
|
23
|
+
};
|
|
24
|
+
const raw = await (0, secrets_1.readSecret)({
|
|
25
|
+
io: ctx.io,
|
|
26
|
+
source,
|
|
27
|
+
flags: secrets_1.PASSWORD_FLAGS,
|
|
28
|
+
dataOnStdin,
|
|
29
|
+
checkMode: true,
|
|
30
|
+
});
|
|
31
|
+
const password = raw === undefined
|
|
32
|
+
? await promptPassword(ctx, dataOnStdin, confirm)
|
|
33
|
+
: raw.toString("utf8").replace(/\r?\n$/, "");
|
|
34
|
+
if (password === "")
|
|
35
|
+
throw new Error("the password is empty");
|
|
36
|
+
return password;
|
|
37
|
+
};
|
|
38
|
+
const passwordCommand = (command, takesData) => (0, secrets_1.withSecretOptions)(command, secrets_1.PASSWORD_FLAGS, {
|
|
39
|
+
what: "password",
|
|
40
|
+
format: "one trailing line break is ignored",
|
|
41
|
+
takesData,
|
|
42
|
+
}).option("--json", "print the result as one line of JSON");
|
|
43
|
+
const registerHash = (password, ctx) => passwordCommand(password
|
|
44
|
+
.command("hash")
|
|
45
|
+
.description("Hash a password with Argon2id (crypto-lib defaults: t=3, " +
|
|
46
|
+
"m=64 MiB, p=4) and print its PHC string"), false).action(async (opts) => {
|
|
47
|
+
const secret = await readPassword(ctx, opts, false, true);
|
|
48
|
+
const { phc, algorithm, params } = (0, modern_1.hashPassword)({ password: secret });
|
|
49
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), { phc, algorithm, params }, phc);
|
|
50
|
+
});
|
|
51
|
+
const registerVerify = (password, ctx) => passwordCommand(password
|
|
52
|
+
.command("verify")
|
|
53
|
+
.description("Check a password against a PHC string; exits 1 when it does not match")
|
|
54
|
+
.argument("[file]", "PHC string (password hash output or its --json line); '-' or " +
|
|
55
|
+
"omitted reads standard input"), true).action(async (file, opts) => {
|
|
56
|
+
const secret = await readPassword(ctx, opts, (0, io_1.isStdin)(file), false);
|
|
57
|
+
const phc = (0, io_1.valueOrJsonField)(await (0, io_1.readInput)(file, ctx.io), "phc");
|
|
58
|
+
const { valid } = (0, modern_1.verifyPasswordPhc)({ password: secret, phc });
|
|
59
|
+
(0, io_1.emit)(ctx.io, Boolean(opts.json), { valid }, valid ? "valid" : "invalid");
|
|
60
|
+
if (!valid)
|
|
61
|
+
ctx.exitCode = io_1.EXIT.FAILURE;
|
|
62
|
+
});
|
|
63
|
+
const registerPassword = (program, ctx) => {
|
|
64
|
+
const password = program
|
|
65
|
+
.command("password")
|
|
66
|
+
.description("Hash or verify a password (Argon2id)")
|
|
67
|
+
.helpCommand(false);
|
|
68
|
+
registerHash(password, ctx);
|
|
69
|
+
registerVerify(password, ctx);
|
|
70
|
+
return password;
|
|
71
|
+
};
|
|
72
|
+
exports.registerPassword = registerPassword;
|
|
73
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type CliIO } from "./io";
|
|
3
|
+
export interface SecretFlags {
|
|
4
|
+
file: string;
|
|
5
|
+
stdin: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SecretSource {
|
|
8
|
+
file?: string | undefined;
|
|
9
|
+
stdin?: boolean | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare const KEY_FLAGS: SecretFlags;
|
|
12
|
+
export declare const PASSWORD_FLAGS: SecretFlags;
|
|
13
|
+
export declare const sharedModeWarning: (file: string, mode: number, platform: NodeJS.Platform) => string | undefined;
|
|
14
|
+
export declare const readSecretFile: (file: string, io: CliIO, platform: NodeJS.Platform) => Promise<NonSharedBuffer>;
|
|
15
|
+
export interface SecretHelp {
|
|
16
|
+
what: string;
|
|
17
|
+
format: string;
|
|
18
|
+
takesData: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const withSecretOptions: (command: Command, flags: SecretFlags, help: SecretHelp) => Command;
|
|
21
|
+
export interface SecretRequest {
|
|
22
|
+
io: CliIO;
|
|
23
|
+
source: SecretSource;
|
|
24
|
+
flags: SecretFlags;
|
|
25
|
+
dataOnStdin: boolean;
|
|
26
|
+
checkMode: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare const readSecret: (req: SecretRequest) => Promise<Buffer | undefined>;
|
|
29
|
+
export declare const requireSecret: (req: SecretRequest, what: string) => Promise<Buffer<ArrayBufferLike>>;
|
|
30
|
+
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/program/secrets.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,MAAM,CAAC;AAGzD,MAAM,WAAW,WAAW;IAE1B,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,MAAM,CAAC;CACf;AAGD,MAAM,WAAW,YAAY;IAE3B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE1B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAGD,eAAO,MAAM,SAAS,EAAE,WAGvB,CAAC;AAGF,eAAO,MAAM,cAAc,EAAE,WAG5B,CAAC;AAeF,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,UAAU,MAAM,CAAC,QAAQ,uBAU1B,CAAC;AAWF,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EACZ,IAAI,KAAK,EACT,UAAU,MAAM,CAAC,QAAQ,6BAc1B,CAAC;AAGF,MAAM,WAAW,UAAU;IAEzB,IAAI,EAAE,MAAM,CAAC;IAEb,MAAM,EAAE,MAAM,CAAC;IAEf,SAAS,EAAE,OAAO,CAAC;CACpB;AAUD,eAAO,MAAM,iBAAiB,GAC5B,SAAS,OAAO,EAChB,OAAO,WAAW,EAClB,MAAM,UAAU,YAejB,CAAC;AAGF,MAAM,WAAW,aAAa;IAE5B,EAAE,EAAE,KAAK,CAAC;IAEV,MAAM,EAAE,YAAY,CAAC;IAErB,KAAK,EAAE,WAAW,CAAC;IAEnB,WAAW,EAAE,OAAO,CAAC;IAErB,SAAS,EAAE,OAAO,CAAC;CACpB;AASD,eAAO,MAAM,UAAU,GACrB,KAAK,aAAa,KACjB,OAAO,CAAC,MAAM,GAAG,SAAS,CAe5B,CAAC;AAQF,eAAO,MAAM,aAAa,GAAU,KAAK,aAAa,EAAE,MAAM,MAAM,qCAQnE,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireSecret = exports.readSecret = exports.withSecretOptions = exports.readSecretFile = exports.sharedModeWarning = exports.PASSWORD_FLAGS = exports.KEY_FLAGS = void 0;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const commander_1 = require("commander");
|
|
6
|
+
const io_1 = require("./io");
|
|
7
|
+
exports.KEY_FLAGS = {
|
|
8
|
+
file: "--key-file",
|
|
9
|
+
stdin: "--key-stdin",
|
|
10
|
+
};
|
|
11
|
+
exports.PASSWORD_FLAGS = {
|
|
12
|
+
file: "--password-file",
|
|
13
|
+
stdin: "--password-stdin",
|
|
14
|
+
};
|
|
15
|
+
const SHARED_MODE_BITS = 0o077;
|
|
16
|
+
const sharedModeWarning = (file, mode, platform) => {
|
|
17
|
+
if (platform === "win32" || (mode & SHARED_MODE_BITS) === 0) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const octal = (mode & 0o777).toString(8).padStart(3, "0");
|
|
21
|
+
return (`crypto-cli: warning: ${file} can be read by other users ` +
|
|
22
|
+
`(mode ${octal}); restrict it with: chmod 600 ${file}\n`);
|
|
23
|
+
};
|
|
24
|
+
exports.sharedModeWarning = sharedModeWarning;
|
|
25
|
+
const readSecretFile = async (file, io, platform) => {
|
|
26
|
+
const handle = await (0, promises_1.open)(file, "r");
|
|
27
|
+
try {
|
|
28
|
+
const warning = (0, exports.sharedModeWarning)(file, (await handle.stat()).mode, platform);
|
|
29
|
+
if (warning !== undefined)
|
|
30
|
+
io.stderr(warning);
|
|
31
|
+
return await handle.readFile();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await handle.close();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
exports.readSecretFile = readSecretFile;
|
|
38
|
+
const withSecretOptions = (command, flags, help) => {
|
|
39
|
+
const note = help.takesData ? " (the data must then be a file argument)" : "";
|
|
40
|
+
const stdinOption = new commander_1.Option(flags.stdin, `read the ${help.what} from standard input${note}`);
|
|
41
|
+
return command
|
|
42
|
+
.addOption(new commander_1.Option(`${flags.file} <path>`, `read the ${help.what} from a file: ${help.format}`).conflicts(stdinOption.attributeName()))
|
|
43
|
+
.addOption(stdinOption);
|
|
44
|
+
};
|
|
45
|
+
exports.withSecretOptions = withSecretOptions;
|
|
46
|
+
const readSecret = async (req) => {
|
|
47
|
+
const { io, source, flags } = req;
|
|
48
|
+
if (source.stdin) {
|
|
49
|
+
if (req.dataOnStdin) {
|
|
50
|
+
throw new io_1.UsageError(`${flags.stdin} needs the data as a file argument: standard ` +
|
|
51
|
+
`input cannot carry both`);
|
|
52
|
+
}
|
|
53
|
+
return (0, io_1.readInput)(undefined, io);
|
|
54
|
+
}
|
|
55
|
+
if (source.file === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
return req.checkMode
|
|
58
|
+
? (0, exports.readSecretFile)(source.file, io, process.platform)
|
|
59
|
+
: (0, promises_1.readFile)(source.file);
|
|
60
|
+
};
|
|
61
|
+
exports.readSecret = readSecret;
|
|
62
|
+
const requireSecret = async (req, what) => {
|
|
63
|
+
const secret = await (0, exports.readSecret)(req);
|
|
64
|
+
if (secret === undefined) {
|
|
65
|
+
throw new io_1.UsageError(`missing ${what}: pass ${req.flags.file} <path> or ${req.flags.stdin}`);
|
|
66
|
+
}
|
|
67
|
+
return secret;
|
|
68
|
+
};
|
|
69
|
+
exports.requireSecret = requireSecret;
|
|
70
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type RunContext } from "./io";
|
|
3
|
+
interface Signer {
|
|
4
|
+
sign: (privateKey: string, message: Uint8Array) => string;
|
|
5
|
+
verify: (publicKey: string, message: Uint8Array, signature: string) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const SIGNING_ALGORITHMS: string[];
|
|
8
|
+
interface LoadedKey {
|
|
9
|
+
algorithm: string;
|
|
10
|
+
kid?: string | undefined;
|
|
11
|
+
material: string;
|
|
12
|
+
signer: Signer;
|
|
13
|
+
}
|
|
14
|
+
export declare const parseKeyFile: (raw: Buffer, field: "privateKey" | "publicKey") => LoadedKey;
|
|
15
|
+
export declare const registerSign: (program: Command, ctx: RunContext) => Command;
|
|
16
|
+
export declare const registerVerify: (program: Command, ctx: RunContext) => Command;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=sign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../src/program/sign.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAc5C,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,MAAM,CAAC;AASd,UAAU,MAAM;IACd,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,KAAK,MAAM,CAAC;IAC1D,MAAM,EAAE,CACN,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,KACd,OAAO,CAAC;CACd;AAmCD,eAAO,MAAM,kBAAkB,UAAuB,CAAC;AAMvD,UAAU,SAAS;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA0BD,eAAO,MAAM,YAAY,GACvB,KAAK,MAAM,EACX,OAAO,YAAY,GAAG,WAAW,KAChC,SAmBF,CAAC;AAkEF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,EAAE,KAAK,UAAU,YAoBxD,CAAC;AAiCP,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,EAAE,KAAK,UAAU,YA+B1D,CAAC"}
|