@sebastienrousseau/crypto-cli 0.0.7 → 0.0.9

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.
Files changed (59) hide show
  1. package/README.md +325 -3
  2. package/dist/cli.js +37 -196
  3. package/dist/commands/decrypt.command.js +2 -2
  4. package/dist/commands/encrypt.command.js +2 -2
  5. package/dist/commands/generate.command.js +2 -2
  6. package/dist/commands/modern/cbom.command.d.ts +2 -1
  7. package/dist/commands/modern/cbom.command.d.ts.map +1 -1
  8. package/dist/commands/modern/cbom.command.js +99 -146
  9. package/dist/commands/modern/cbom.core.d.ts +5 -0
  10. package/dist/commands/modern/cbom.core.d.ts.map +1 -0
  11. package/dist/commands/modern/cbom.core.js +57 -0
  12. package/dist/commands/modern/encrypt.command.d.ts.map +1 -1
  13. package/dist/commands/modern/encrypt.command.js +42 -63
  14. package/dist/commands/modern/hash.command.d.ts.map +1 -1
  15. package/dist/commands/modern/hash.command.js +1 -1
  16. package/dist/commands/modern/keygen.command.js +1 -1
  17. package/dist/commands/modern/password-hash.command.d.ts.map +1 -1
  18. package/dist/commands/modern/password-hash.command.js +28 -27
  19. package/dist/commands/modern/sign.command.d.ts.map +1 -1
  20. package/dist/commands/modern/sign.command.js +83 -73
  21. package/dist/commands/reformat.command.js +2 -2
  22. package/dist/commands/revoke.command.js +2 -2
  23. package/dist/commands/session.command.js +2 -2
  24. package/dist/commands/sign.command.js +2 -2
  25. package/dist/commands/verify.command.js +2 -2
  26. package/dist/menu.d.ts +12 -0
  27. package/dist/menu.d.ts.map +1 -0
  28. package/dist/menu.js +107 -0
  29. package/dist/program/cbom.d.ts +4 -0
  30. package/dist/program/cbom.d.ts.map +1 -0
  31. package/dist/program/cbom.js +49 -0
  32. package/dist/program/encrypt.d.ts +6 -0
  33. package/dist/program/encrypt.d.ts.map +1 -0
  34. package/dist/program/encrypt.js +73 -0
  35. package/dist/program/hash.d.ts +4 -0
  36. package/dist/program/hash.d.ts.map +1 -0
  37. package/dist/program/hash.js +21 -0
  38. package/dist/program/index.d.ts +7 -0
  39. package/dist/program/index.d.ts.map +1 -0
  40. package/dist/program/index.js +69 -0
  41. package/dist/program/io.d.ts +35 -0
  42. package/dist/program/io.d.ts.map +1 -0
  43. package/dist/program/io.js +65 -0
  44. package/dist/program/keygen.d.ts +4 -0
  45. package/dist/program/keygen.d.ts.map +1 -0
  46. package/dist/program/keygen.js +32 -0
  47. package/dist/program/password.d.ts +4 -0
  48. package/dist/program/password.d.ts.map +1 -0
  49. package/dist/program/password.js +73 -0
  50. package/dist/program/secrets.d.ts +30 -0
  51. package/dist/program/secrets.d.ts.map +1 -0
  52. package/dist/program/secrets.js +70 -0
  53. package/dist/program/sign.d.ts +18 -0
  54. package/dist/program/sign.d.ts.map +1 -0
  55. package/dist/program/sign.js +130 -0
  56. package/dist/program/usage.d.ts +7 -0
  57. package/dist/program/usage.d.ts.map +1 -0
  58. package/dist/program/usage.js +36 -0
  59. package/package.json +28 -36
@@ -0,0 +1,35 @@
1
+ export declare const EXIT: {
2
+ readonly OK: 0;
3
+ readonly FAILURE: 1;
4
+ readonly USAGE: 2;
5
+ };
6
+ export type InputSource = AsyncIterable<Buffer | string> | Iterable<Buffer | string>;
7
+ export interface CliIO {
8
+ stdin: InputSource;
9
+ stdout: (data: string | Uint8Array) => void;
10
+ stderr: (text: string) => void;
11
+ isTTY: boolean;
12
+ promptSecret?: ((message: string) => Promise<string | undefined>) | undefined;
13
+ }
14
+ export declare class UsageError extends Error {
15
+ }
16
+ export interface RunContext {
17
+ io: CliIO;
18
+ exitCode: number;
19
+ }
20
+ export declare const isInteractive: (input: {
21
+ isTTY?: boolean;
22
+ }, output: {
23
+ isTTY?: boolean;
24
+ }) => boolean;
25
+ export declare const exitOnClosedPipe: (stream: NodeJS.EventEmitter, exit: (code: number) => void) => NodeJS.EventEmitter<any>;
26
+ export declare const promptHidden: (message: string) => Promise<string | undefined>;
27
+ export declare const secretPrompt: (stdin: {
28
+ isTTY?: boolean;
29
+ }) => ((message: string) => Promise<string | undefined>) | undefined;
30
+ export declare const processIO: () => CliIO;
31
+ export declare const isStdin: (file: string | undefined) => file is undefined | "-";
32
+ export declare const readInput: (file: string | undefined, io: CliIO) => Promise<Buffer<ArrayBuffer>>;
33
+ export declare const valueOrJsonField: (input: Buffer, field: string) => string;
34
+ export declare const emit: (io: CliIO, json: boolean, data: unknown, text: string) => void;
35
+ //# sourceMappingURL=io.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/program/io.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,IAAI;;;;CAA2C,CAAC;AAG7D,MAAM,MAAM,WAAW,GACrB,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;AAG7D,MAAM,WAAW,KAAK;IAEpB,KAAK,EAAE,WAAW,CAAC;IAEnB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,KAAK,IAAI,CAAC;IAE5C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAE/B,KAAK,EAAE,OAAO,CAAC;IAKf,YAAY,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;CAC/E;AAMD,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAGxC,MAAM,WAAW,UAAU;IAEzB,EAAE,EAAE,KAAK,CAAC;IAEV,QAAQ,EAAE,MAAM,CAAC;CAClB;AASD,eAAO,MAAM,aAAa,GACxB,OAAO;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EAC1B,QAAQ;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,YACY,CAAC;AAO1C,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,MAAM,CAAC,YAAY,EAC3B,MAAM,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,6BAK1B,CAAC;AASL,eAAO,MAAM,YAAY,GAAU,SAAS,MAAM,gCAQjD,CAAC;AAQF,eAAO,MAAM,YAAY,GAAI,OAAO;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,gBAhBX,MAAM,6CAiBV,CAAC;AAGzC,eAAO,MAAM,SAAS,QAAO,KAM3B,CAAC;AAgBH,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,IAAI,IAAI,SAAS,GAAG,GACnC,CAAC;AASrC,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,GAAG,SAAS,EAAE,IAAI,KAAK,iCACT,CAAC;AAUrD,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,EAAE,OAAO,MAAM,KAAG,MAQ/D,CAAC;AAWF,eAAO,MAAM,IAAI,GAAI,IAAI,KAAK,EAAE,MAAM,OAAO,EAAE,MAAM,OAAO,EAAE,MAAM,MAAM,SACpB,CAAC"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.emit = exports.valueOrJsonField = exports.readInput = exports.isStdin = exports.processIO = exports.secretPrompt = exports.promptHidden = exports.exitOnClosedPipe = exports.isInteractive = exports.UsageError = exports.EXIT = void 0;
7
+ const promises_1 = require("node:fs/promises");
8
+ const prompts_1 = __importDefault(require("prompts"));
9
+ exports.EXIT = { OK: 0, FAILURE: 1, USAGE: 2 };
10
+ class UsageError extends Error {
11
+ }
12
+ exports.UsageError = UsageError;
13
+ const isInteractive = (input, output) => Boolean(input.isTTY && output.isTTY);
14
+ exports.isInteractive = isInteractive;
15
+ const exitOnClosedPipe = (stream, exit) => stream.on("error", (err) => {
16
+ if (err.code !== "EPIPE")
17
+ throw err;
18
+ exit(exports.EXIT.OK);
19
+ });
20
+ exports.exitOnClosedPipe = exitOnClosedPipe;
21
+ const promptHidden = async (message) => {
22
+ const answer = await (0, prompts_1.default)({
23
+ type: "password",
24
+ name: "secret",
25
+ message,
26
+ stdout: process.stderr,
27
+ });
28
+ return answer.secret;
29
+ };
30
+ exports.promptHidden = promptHidden;
31
+ const secretPrompt = (stdin) => stdin.isTTY ? exports.promptHidden : undefined;
32
+ exports.secretPrompt = secretPrompt;
33
+ const processIO = () => ({
34
+ stdin: process.stdin,
35
+ stdout: (data) => process.stdout.write(data),
36
+ stderr: (text) => process.stderr.write(text),
37
+ isTTY: (0, exports.isInteractive)(process.stdin, process.stdout),
38
+ promptSecret: (0, exports.secretPrompt)(process.stdin),
39
+ });
40
+ exports.processIO = processIO;
41
+ const readAll = async (stream) => {
42
+ const chunks = [];
43
+ for await (const chunk of stream) {
44
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
45
+ }
46
+ return Buffer.concat(chunks);
47
+ };
48
+ const isStdin = (file) => file === undefined || file === "-";
49
+ exports.isStdin = isStdin;
50
+ const readInput = (file, io) => (0, exports.isStdin)(file) ? readAll(io.stdin) : (0, promises_1.readFile)(file);
51
+ exports.readInput = readInput;
52
+ const valueOrJsonField = (input, field) => {
53
+ const text = input.toString("utf8").trim();
54
+ if (!text.startsWith("{"))
55
+ return text;
56
+ const value = JSON.parse(text)[field];
57
+ if (typeof value !== "string") {
58
+ throw new Error(`the JSON input has no "${field}" string`);
59
+ }
60
+ return value;
61
+ };
62
+ exports.valueOrJsonField = valueOrJsonField;
63
+ const emit = (io, json, data, text) => io.stdout(`${json ? JSON.stringify(data) : text}\n`);
64
+ exports.emit = emit;
65
+ //# sourceMappingURL=io.js.map
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ import { type RunContext } from "./io";
3
+ export declare const registerKeygen: (program: Command, ctx: RunContext) => Command;
4
+ //# sourceMappingURL=keygen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keygen.d.ts","sourceRoot":"","sources":["../../src/program/keygen.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAQ5C,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AA0B7C,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,EAAE,KAAK,UAAU,YAoB1D,CAAC"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerKeygen = void 0;
4
+ const commander_1 = require("commander");
5
+ const keys_1 = require("@sebastienrousseau/crypto-lib/keys");
6
+ const io_1 = require("./io");
7
+ const describe = (key) => [
8
+ `Algorithm: ${key.algorithm}`,
9
+ `Key ID: ${key.kid}`,
10
+ `Public: ${key.publicKey}`,
11
+ `Private: ${key.privateKey}`,
12
+ ].join("\n");
13
+ const registerKeygen = (program, ctx) => program
14
+ .command("keygen")
15
+ .description("Generate a key pair (the private key is printed to stdout)")
16
+ .addOption(new commander_1.Option("-a, --algorithm <name>", "key algorithm")
17
+ .choices(keys_1.KEY_ALGORITHMS)
18
+ .makeOptionMandatory())
19
+ .option("--kid <id>", "key ID (default: thumbprint of the public key)")
20
+ .addOption(new commander_1.Option("--use <use>", "intended key usage").choices(["sig", "enc"]))
21
+ .option("--json", "print the key pair as one line of JSON")
22
+ .action((opts) => {
23
+ const metadata = {};
24
+ if (opts.kid !== undefined)
25
+ metadata.kid = opts.kid;
26
+ if (opts.use !== undefined)
27
+ metadata.use = opts.use;
28
+ const key = (0, keys_1.generateKeyPair)(opts.algorithm, metadata);
29
+ (0, io_1.emit)(ctx.io, Boolean(opts.json), key, describe(key));
30
+ });
31
+ exports.registerKeygen = registerKeygen;
32
+ //# sourceMappingURL=keygen.js.map
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ import { type RunContext } from "./io";
3
+ export declare const registerPassword: (program: Command, ctx: RunContext) => Command;
4
+ //# sourceMappingURL=password.d.ts.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"}
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerVerify = exports.registerSign = exports.parseKeyFile = exports.SIGNING_ALGORITHMS = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const commander_1 = require("commander");
6
+ const modern_1 = require("@sebastienrousseau/crypto-lib/modern");
7
+ const io_1 = require("./io");
8
+ const secrets_1 = require("./secrets");
9
+ const mlDsa = (level) => ({
10
+ sign: (key, msg) => (0, modern_1.mlDsaSign)(level, key, msg).signature,
11
+ verify: (key, msg, sig) => (0, modern_1.mlDsaVerify)(level, key, msg, sig).valid,
12
+ });
13
+ const SIGNERS = {
14
+ ed25519: {
15
+ sign: (key, msg) => (0, modern_1.ed25519Sign)(key, msg).signature,
16
+ verify: (key, msg, sig) => (0, modern_1.ed25519Verify)(key, msg, sig).valid,
17
+ },
18
+ ed448: {
19
+ sign: (key, msg) => (0, modern_1.ed448Sign)(key, msg).signature,
20
+ verify: (key, msg, sig) => (0, modern_1.ed448Verify)(key, msg, sig).valid,
21
+ },
22
+ p256: {
23
+ sign: (key, msg) => (0, modern_1.p256Sign)(key, msg).signature,
24
+ verify: (key, msg, sig) => (0, modern_1.p256Verify)(key, msg, sig).valid,
25
+ },
26
+ p384: {
27
+ sign: (key, msg) => (0, modern_1.p384Sign)(key, msg).signature,
28
+ verify: (key, msg, sig) => (0, modern_1.p384Verify)(key, msg, sig).valid,
29
+ },
30
+ "ml-dsa-44": mlDsa(44),
31
+ "ml-dsa-65": mlDsa(65),
32
+ "ml-dsa-87": mlDsa(87),
33
+ };
34
+ exports.SIGNING_ALGORITHMS = Object.keys(SIGNERS);
35
+ const HEX = /^(?:[0-9a-fA-F]{2})+$/;
36
+ const asRecord = (value) => typeof value === "object" && value !== null
37
+ ? value
38
+ : {};
39
+ const parseJson = (raw) => {
40
+ try {
41
+ return JSON.parse(raw.toString("utf8"));
42
+ }
43
+ catch {
44
+ throw new Error("the key file is not JSON from crypto-cli keygen --json");
45
+ }
46
+ };
47
+ const parseKeyFile = (raw, field) => {
48
+ const key = asRecord(parseJson(raw));
49
+ const { algorithm, kid } = key;
50
+ if (typeof algorithm !== "string" || !Object.hasOwn(SIGNERS, algorithm)) {
51
+ throw new Error(`the key must be a signing key from crypto-cli keygen --json ` +
52
+ `(${exports.SIGNING_ALGORITHMS.join(", ")})`);
53
+ }
54
+ const material = key[field];
55
+ if (typeof material !== "string") {
56
+ throw new Error(`the key file has no ${field}`);
57
+ }
58
+ return {
59
+ algorithm,
60
+ kid: typeof kid === "string" ? kid : undefined,
61
+ material,
62
+ signer: SIGNERS[algorithm],
63
+ };
64
+ };
65
+ exports.parseKeyFile = parseKeyFile;
66
+ const readKey = async (ctx, opts, file, field) => {
67
+ const source = { file: opts.keyFile, stdin: opts.keyStdin };
68
+ const raw = await (0, secrets_1.requireSecret)({
69
+ io: ctx.io,
70
+ source,
71
+ flags: secrets_1.KEY_FLAGS,
72
+ dataOnStdin: (0, io_1.isStdin)(file),
73
+ checkMode: field === "privateKey",
74
+ }, "key");
75
+ return (0, exports.parseKeyFile)(raw, field);
76
+ };
77
+ const JSON_OPTION = ["--json", "print the result as one line of JSON"];
78
+ const signatureCommand = (program, name, description, key) => (0, secrets_1.withSecretOptions)(program
79
+ .command(name)
80
+ .description(description)
81
+ .argument("[file]", `file to ${name}; '-' or omitted reads standard input`), secrets_1.KEY_FLAGS, { ...key, takesData: true });
82
+ const registerSign = (program, ctx) => signatureCommand(program, "sign", "Sign a file with a key pair from keygen --json and print the " +
83
+ "signature in hex", {
84
+ what: "key pair",
85
+ format: `keygen --json output (${exports.SIGNING_ALGORITHMS.join(", ")})`,
86
+ })
87
+ .option(...JSON_OPTION)
88
+ .action(async (file, opts) => {
89
+ const key = await readKey(ctx, opts, file, "privateKey");
90
+ const signature = key.signer.sign(key.material, await (0, io_1.readInput)(file, ctx.io));
91
+ const result = { signature, algorithm: key.algorithm, kid: key.kid };
92
+ (0, io_1.emit)(ctx.io, Boolean(opts.json), result, signature);
93
+ });
94
+ exports.registerSign = registerSign;
95
+ const readSignature = async (opts) => {
96
+ if (opts.signature !== undefined)
97
+ return opts.signature.trim();
98
+ if (opts.signatureFile === undefined) {
99
+ throw new io_1.UsageError("missing signature: pass --signature <hex> or --signature-file <path>");
100
+ }
101
+ return (0, io_1.valueOrJsonField)(await (0, promises_1.readFile)(opts.signatureFile), "signature");
102
+ };
103
+ const checkSignature = (key, data, signature) => {
104
+ if (!HEX.test(signature))
105
+ return false;
106
+ try {
107
+ return key.signer.verify(key.material, data, signature);
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ };
113
+ const registerVerify = (program, ctx) => signatureCommand(program, "verify", "Verify a signature from sign; exits 1 when it is not valid", {
114
+ what: "public key",
115
+ format: "keygen --json output, or JSON with algorithm and publicKey",
116
+ })
117
+ .addOption(new commander_1.Option("--signature <hex>", "the signature, in hex").conflicts("signatureFile"))
118
+ .option("--signature-file <path>", "read the signature from a file (sign output or its --json line)")
119
+ .option(...JSON_OPTION)
120
+ .action(async (file, opts) => {
121
+ const signature = await readSignature(opts);
122
+ const key = await readKey(ctx, opts, file, "publicKey");
123
+ const valid = checkSignature(key, await (0, io_1.readInput)(file, ctx.io), signature);
124
+ const result = { valid, algorithm: key.algorithm, kid: key.kid };
125
+ (0, io_1.emit)(ctx.io, Boolean(opts.json), result, valid ? "valid" : "invalid");
126
+ if (!valid)
127
+ ctx.exitCode = io_1.EXIT.FAILURE;
128
+ });
129
+ exports.registerVerify = registerVerify;
130
+ //# sourceMappingURL=sign.js.map
@@ -0,0 +1,7 @@
1
+ export declare const USAGE_MARKERS: {
2
+ readonly start: "<!-- cli-usage:start (generated: pnpm run readme:usage) -->";
3
+ readonly end: "<!-- cli-usage:end -->";
4
+ };
5
+ export declare const renderUsage: () => string;
6
+ export declare const replaceUsage: (readme: string) => string;
7
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/program/usage.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,aAAa;;;CAGhB,CAAC;AAgBX,eAAO,MAAM,WAAW,QAAO,MAQ9B,CAAC;AAQF,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,KAAG,MAW7C,CAAC"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replaceUsage = exports.renderUsage = exports.USAGE_MARKERS = void 0;
4
+ const index_1 = require("./index");
5
+ exports.USAGE_MARKERS = {
6
+ start: "<!-- cli-usage:start (generated: pnpm run readme:usage) -->",
7
+ end: "<!-- cli-usage:end -->",
8
+ };
9
+ const walk = (command, prefix) => {
10
+ const name = prefix ? `${prefix} ${command.name()}` : command.name();
11
+ return [
12
+ [name, command],
13
+ ...command.commands.flatMap((sub) => walk(sub, name)),
14
+ ];
15
+ };
16
+ const renderUsage = () => {
17
+ const program = (0, index_1.buildProgram)({ io: (0, index_1.processIO)(), exitCode: index_1.EXIT.OK }, "");
18
+ const sections = walk(program, "").map(([name, command]) => {
19
+ command.configureHelp({ helpWidth: 80 });
20
+ return `#### \`${name}\`\n\n\`\`\`text\n${command.helpInformation()}\`\`\`\n`;
21
+ });
22
+ return [exports.USAGE_MARKERS.start, "", ...sections, exports.USAGE_MARKERS.end].join("\n");
23
+ };
24
+ exports.renderUsage = renderUsage;
25
+ const replaceUsage = (readme) => {
26
+ const start = readme.indexOf(exports.USAGE_MARKERS.start);
27
+ const end = readme.indexOf(exports.USAGE_MARKERS.end);
28
+ if (start < 0 || end < start) {
29
+ throw new Error("README.md has no cli-usage block");
30
+ }
31
+ return (readme.slice(0, start) +
32
+ (0, exports.renderUsage)() +
33
+ readme.slice(end + exports.USAGE_MARKERS.end.length));
34
+ };
35
+ exports.replaceUsage = replaceUsage;
36
+ //# sourceMappingURL=usage.js.map
package/package.json CHANGED
@@ -12,46 +12,46 @@
12
12
  "source": "git",
13
13
  "target": "git://github.com/sebastienrousseau/crypto-service.git"
14
14
  },
15
+ "type": "commonjs",
15
16
  "bin": {
16
17
  "crypto-cli": "dist/cli.js",
17
18
  "cryptocli": "dist/cli.js"
18
19
  },
20
+ "exports": {
21
+ "./package.json": "./package.json"
22
+ },
19
23
  "bugs": {
20
24
  "url": "https://github.com/sebastienrousseau/crypto-service/issues"
21
25
  },
22
26
  "dependencies": {
27
+ "commander": "^14.0.3",
23
28
  "figlet": "^1.6.0",
24
29
  "kleur": "^4.1.5",
25
30
  "prompts": "^2.4.2",
26
- "@sebastienrousseau/crypto-lib": "0.0.7",
27
- "@sebastienrousseau/crypto-cbom": "0.0.7"
31
+ "@sebastienrousseau/crypto-cbom": "0.0.9",
32
+ "@sebastienrousseau/crypto-lib": "0.0.9"
28
33
  },
29
34
  "description": "The Crypto Lib Command Line Interface (CLI) is a unified tool to perform common cryptographic operations such as key generation, data encryption, digital signing, and signature verification which are invoked from the command prompt or terminal.",
30
35
  "devDependencies": {
31
- "@sebastienrousseau/c8-config": "^0.0.3",
32
- "@sebastienrousseau/eslint-config": "^0.0.2",
33
- "@sebastienrousseau/jsdoc-config": "^0.0.5",
36
+ "@sebastienrousseau/c8-config": "^0.0.7",
37
+ "@sebastienrousseau/eslint-config": "^0.0.7",
38
+ "@sebastienrousseau/jsdoc-config": "^0.0.7",
34
39
  "@sebastienrousseau/markdownlint-config": "^0.0.1",
35
40
  "@sebastienrousseau/mocha-config": "^0.0.5",
36
- "@sebastienrousseau/prettier-config": "^0.0.4",
37
- "@sebastienrousseau/remark-config": "^0.0.3",
38
- "@types/chai": "^4.3.7",
41
+ "@sebastienrousseau/prettier-config": "^0.0.7",
42
+ "@sebastienrousseau/remark-config": "^0.0.7",
43
+ "@types/chai": "^5.2.3",
39
44
  "@types/chai-as-promised": "^8.0.2",
40
45
  "@types/figlet": "^1.5.6",
41
- "@types/mocha": "^10.0.2",
42
- "@types/node": "^26.4.1",
46
+ "@types/mocha": "^10.0.10",
47
+ "@types/node": "^26.6.3",
43
48
  "@types/prompts": "^2.4.5",
44
- "@typescript-eslint/eslint-plugin": "^6.21.0",
45
- "@typescript-eslint/parser": "^6.21.0",
46
49
  "c8": "^12.0.0",
47
- "chai": "^4.5.0",
48
- "chai-as-promised": "^7.1.2",
49
- "eslint": "^8.57.1",
50
- "eslint-import-resolver-typescript": "^3.10.1",
51
- "eslint-plugin-import": "^2.28.1",
50
+ "chai": "^6.3.0",
51
+ "chai-as-promised": "^8.0.2",
52
52
  "filesizes": "^0.1.2",
53
- "mocha": "^10.8.2",
54
- "prettier": "^3.9.8",
53
+ "mocha": "^12.0.3",
54
+ "prettier": "^3.9.9",
55
55
  "remark-cli": "^12.0.0",
56
56
  "remark-footnotes": "^5.0.0",
57
57
  "remark-preset-lint-consistent": "^6.0.1",
@@ -86,39 +86,31 @@
86
86
  }
87
87
  ],
88
88
  "homepage": "https://crypto-cli.io",
89
+ "repository": {
90
+ "type": "git",
91
+ "url": "git+https://github.com/sebastienrousseau/crypto-service.git",
92
+ "directory": "packages/crypto-cli"
93
+ },
89
94
  "keywords": [
90
95
  "crypto-cli"
91
96
  ],
92
97
  "license": "MIT OR Apache-2.0",
93
98
  "license_URI": "http://www.opensource.org/licenses/mit-license.php",
94
- "main": "./dist/cli.js",
95
- "exports": {
96
- ".": {
97
- "types": "./dist/index.d.ts",
98
- "require": "./dist/cli.js",
99
- "import": "./dist/cli.js"
100
- }
101
- },
102
99
  "name": "@sebastienrousseau/crypto-cli",
103
100
  "private": false,
104
101
  "publishConfig": {
105
102
  "access": "public"
106
103
  },
107
- "repository": {
108
- "directory": "packages/crypto-cli",
109
- "type": "git",
110
- "url": "git@github.com:sebastienrousseau/crypto-service.git"
111
- },
112
- "types": "./dist/index.d.ts",
113
104
  "sideEffects": false,
114
- "version": "0.0.7",
105
+ "version": "0.0.9",
115
106
  "scripts": {
116
107
  "build": "tsc --build tsconfig.json",
117
108
  "clean": "rimraf ./coverage ./dist ./docs",
118
- "lint": "eslint --ext .ts src __tests__",
119
- "lint:fix": "eslint --cache --fix --ext .ts src __tests__",
109
+ "lint": "eslint src __tests__",
110
+ "lint:fix": "eslint --cache --fix src __tests__",
120
111
  "format": "prettier --write src/**/*.ts",
121
112
  "format:check": "prettier --check \"src/**/*.ts\"",
113
+ "readme:usage": "node scripts/readme-usage.mjs",
122
114
  "start": "node dist/cli.js",
123
115
  "test": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --reporter=lcov --reporter=text mocha --config=.mocharc.cjs",
124
116
  "docs": "typedoc"