From 2b6de58dae636fc36f438f83abb772d2efc1daa2 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 27 Jul 2026 00:59:00 +0000 Subject: [PATCH] feat: cosmology CLI env vars, cosmology-compat rename, bitcoin-first networks --- README.md | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/main/ipc.ts | 2 +- .../src/renderer/src/screens/ToolsScreen.tsx | 4 +- packages/cli/README.md | 23 +++++- packages/cli/__tests__/cli.test.ts | 76 ++++++++++++++++++- packages/cli/package.json | 2 +- packages/cli/src/commands.ts | 11 ++- .../src/commands/{legacy.ts => cosmology.ts} | 40 ++++++---- packages/cli/src/commands/keychain.ts | 4 + packages/cli/src/commands/wallet.ts | 19 +++-- packages/cli/src/utils/env.ts | 48 ++++++++++++ packages/cli/src/utils/passphrase.ts | 11 ++- packages/cli/src/utils/stash.ts | 8 +- .../{legacy => cosmology-compat}/CHANGELOG.md | 0 .../{legacy => cosmology-compat}/README.md | 9 +-- .../__tests__/compat.test.ts | 0 .../jest.config.js | 0 .../{legacy => cosmology-compat}/package.json | 6 +- .../src/cosmology.ts | 0 .../src/cryptojs.ts | 0 .../{legacy => cosmology-compat}/src/index.ts | 0 .../src/upgrade.ts | 0 .../tsconfig.esm.json | 0 .../tsconfig.json | 0 packages/wallet/src/networks.ts | 37 +++++---- packages/wallet/src/wallet.ts | 4 +- pnpm-lock.yaml | 64 ++++++++-------- 28 files changed, 280 insertions(+), 92 deletions(-) rename packages/cli/src/commands/{legacy.ts => cosmology.ts} (68%) create mode 100644 packages/cli/src/utils/env.ts rename packages/{legacy => cosmology-compat}/CHANGELOG.md (100%) rename packages/{legacy => cosmology-compat}/README.md (87%) rename packages/{legacy => cosmology-compat}/__tests__/compat.test.ts (100%) rename packages/{legacy => cosmology-compat}/jest.config.js (100%) rename packages/{legacy => cosmology-compat}/package.json (87%) rename packages/{legacy => cosmology-compat}/src/cosmology.ts (100%) rename packages/{legacy => cosmology-compat}/src/cryptojs.ts (100%) rename packages/{legacy => cosmology-compat}/src/index.ts (100%) rename packages/{legacy => cosmology-compat}/src/upgrade.ts (100%) rename packages/{legacy => cosmology-compat}/tsconfig.esm.json (100%) rename packages/{legacy => cosmology-compat}/tsconfig.json (100%) diff --git a/README.md b/README.md index 38682a2..a75ddf3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ network request. | Package | npm | Source | Description | |---------|-----|--------|-------------| | **@decryption/core** | [![npm](https://img.shields.io/npm/v/@decryption/core.svg)](https://www.npmjs.com/package/@decryption/core) | [GitHub](./packages/core) | Versioned envelope format — Argon2id + XChaCha20-Poly1305 | -| **@decryption/legacy** | [![npm](https://img.shields.io/npm/v/@decryption/legacy.svg)](https://www.npmjs.com/package/@decryption/legacy) | [GitHub](./packages/legacy) | Byte-compatible reader for the old CryptoJS/`@cosmology/core` scheme | +| **@decryption/cosmology-compat** | [![npm](https://img.shields.io/npm/v/@decryption/cosmology-compat.svg)](https://www.npmjs.com/package/@decryption/cosmology-compat) | [GitHub](./packages/cosmology-compat) | Byte-compatible reader for the cosmology CLI encryption scheme | | **@decryption/shamir** | [![npm](https://img.shields.io/npm/v/@decryption/shamir.svg)](https://www.npmjs.com/package/@decryption/shamir) | [GitHub](./packages/shamir) | Shamir secret sharing with authenticated, versioned shares | | **@decryption/wallet** | [![npm](https://img.shields.io/npm/v/@decryption/wallet.svg)](https://www.npmjs.com/package/@decryption/wallet) | [GitHub](./packages/wallet) | BIP39/BIP32 wallets and offline address derivation | | **@decryption/keys** | [![npm](https://img.shields.io/npm/v/@decryption/keys.svg)](https://www.npmjs.com/package/@decryption/keys) | [GitHub](./packages/keys) | X25519 identities, recipient strings, on-disk keyring | diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 44516fc..6ec17ba 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -23,7 +23,7 @@ "@decryption/core": "workspace:*", "@decryption/hashes": "workspace:*", "@decryption/keys": "workspace:*", - "@decryption/legacy": "workspace:*", + "@decryption/cosmology-compat": "workspace:*", "@decryption/shamir": "workspace:*", "@decryption/vault": "workspace:*", "@decryption/wallet": "workspace:*", diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts index 4b43014..da01009 100644 --- a/apps/desktop/src/main/ipc.ts +++ b/apps/desktop/src/main/ipc.ts @@ -1,5 +1,5 @@ import { decryptFromString, encryptToString } from '@decryption/core'; -import { decrypt as legacyDecrypt } from '@decryption/legacy'; +import { decrypt as legacyDecrypt } from '@decryption/cosmology-compat'; import { combineToString, splitToStrings } from '@decryption/shamir'; import { createWallet, deriveAccounts, WordCount } from '@decryption/wallet'; import { ipcMain } from 'electron'; diff --git a/apps/desktop/src/renderer/src/screens/ToolsScreen.tsx b/apps/desktop/src/renderer/src/screens/ToolsScreen.tsx index c3717e6..d96aeac 100644 --- a/apps/desktop/src/renderer/src/screens/ToolsScreen.tsx +++ b/apps/desktop/src/renderer/src/screens/ToolsScreen.tsx @@ -24,12 +24,12 @@ import { toast } from 'sonner'; import type { WalletAccountInfo } from '../../../shared/api'; import { copyWithTimeout, dcrypt } from '../lib/ipc'; -const NETWORKS = ['cosmoshub', 'osmosis', 'ethereum', 'bitcoin']; +const NETWORKS = ['bitcoin', 'ethereum', 'cosmoshub', 'osmosis', 'juno', 'stargaze', 'akash', 'celestia', 'secret', 'terra']; const errorText = (err: unknown): string => (err instanceof Error ? err.message : String(err)); const WalletTool = () => { - const [network, setNetwork] = useState('cosmoshub'); + const [network, setNetwork] = useState('bitcoin'); const [words, setWords] = useState('24'); const [mnemonic, setMnemonic] = useState(''); const [accounts, setAccounts] = useState([]); diff --git a/packages/cli/README.md b/packages/cli/README.md index f6f9a7c..d12decb 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -21,7 +21,7 @@ dcrypt --help | `dcrypt keychain set\|get\|del\|list` | Named local secrets, always encrypted | | `dcrypt shamir split\|combine` | Authenticated Shamir shares | | `dcrypt salt generate\|encrypt\|decrypt` | Two-layer encryption (data under a salt, salt under your passphrase) | -| `dcrypt legacy decrypt\|upgrade` | Read and migrate old CryptoJS blobs | +| `dcrypt cosmology decrypt\|upgrade` | Read and migrate data from the cosmology CLI (`legacy` still works as an alias) | Every command supports `--help`. @@ -63,6 +63,27 @@ Every prompt has a flag, so the CLI works in CI: dcrypt decrypt --in secret.dcrypt --passphrase-file /run/secrets/passphrase --out plain.txt ``` +### Environment variables + +Every variable the cosmology CLI read still works, plus `DCRYPT_`-prefixed aliases +(`DCRYPT_MNEMONIC`, `DCRYPT_SALT`, ...) for environments where the bare names would collide: + +| Variable | Used by | +|----------|---------| +| `MNEMONIC` | `wallet address` / `wallet validate` | +| `SALT` | `cosmology decrypt` / `cosmology upgrade` | +| `ENCRYPTED_SALT` | the two-layer cosmology scheme | +| `KEYCHAIN_ACCOUNT` | keychain namespace (default `dcrypt`) | +| `DCRYPT_PASSPHRASE` | any command that asks for a passphrase | + +Resolution order everywhere: explicit flag, then environment, then stdin, then the +interactive prompt. + +```bash +SALT=my-salt dcrypt cosmology decrypt --in old.txt +MNEMONIC="..." dcrypt wallet address --network ethereum +``` + Add `--json` for machine-readable output, and check exit codes: | Code | Meaning | diff --git a/packages/cli/__tests__/cli.test.ts b/packages/cli/__tests__/cli.test.ts index f77060b..0b67ec9 100644 --- a/packages/cli/__tests__/cli.test.ts +++ b/packages/cli/__tests__/cli.test.ts @@ -265,7 +265,7 @@ describe('salt', () => { }); }); -describe('legacy', () => { +describe('cosmology', () => { // Produced by @cosmology/core: crypt('my-salt', 'legacy plaintext'). const legacyBlob = 'U2FsdGVkX1+e1rSKneHnXFk1ufVWBgAjucz2FBFi5Qxzkuhk6KCkZF+naLOt+APM'; @@ -273,7 +273,7 @@ describe('legacy', () => { it('decrypts an old cryptojs blob and upgrades it', async () => { const blob = file('old.txt', legacyBlob); const salt = file('salt.txt', 'my-salt'); - expect(await run(`legacy decrypt --in ${blob} --salt-file ${salt}`)).toBe(0); + expect(await run(`cosmology decrypt --in ${blob} --salt-file ${salt}`)).toBe(0); expect(stdout()).toBe('legacy plaintext'); out = []; @@ -288,6 +288,78 @@ describe('legacy', () => { 'legacy plaintext' ); }); + + it('still answers to the cosmology CLI name for it, "legacy"', async () => { + const blob = file('old.txt', legacyBlob); + const salt = file('salt.txt', 'my-salt'); + expect(await run(`legacy decrypt --in ${blob} --salt-file ${salt}`)).toBe(0); + expect(stdout()).toBe('legacy plaintext'); + }); + + it('reads the salt from SALT, like the cosmology CLI', async () => { + const blob = file('old.txt', legacyBlob); + process.env.SALT = 'my-salt'; + try { + expect(await run(`cosmology decrypt --in ${blob}`)).toBe(0); + expect(stdout()).toBe('legacy plaintext'); + } finally { + delete process.env.SALT; + } + }); +}); + +describe('environment variables', () => { + it('reads the passphrase from DCRYPT_PASSPHRASE', async () => { + const plain = file('plain.txt', 'env secret'); + const encrypted = join(work, 'secret.dcrypt'); + process.env.DCRYPT_PASSPHRASE = 'from-the-environment'; + try { + expect(await run(`encrypt --in ${plain} --out ${encrypted} --kdf ${FAST_KDF}`)).toBe(0); + expect(await run(`decrypt --in ${encrypted}`)).toBe(0); + expect(stdout()).toBe('env secret'); + } finally { + delete process.env.DCRYPT_PASSPHRASE; + } + }); + + it('reads the mnemonic from MNEMONIC, like the cosmology CLI', async () => { + process.env.MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + try { + expect(await run('wallet validate --json')).toBe(0); + expect(JSON.parse(stdout()).valid).toBe(true); + } finally { + delete process.env.MNEMONIC; + } + }); + + it('namespaces the keychain with KEYCHAIN_ACCOUNT, like the cosmology CLI', async () => { + const pass = file('pass.txt', 'kc-pass'); + const value = file('value.txt', 'namespaced'); + process.env.KEYCHAIN_ACCOUNT = 'work'; + try { + expect( + await run(`keychain set token --in ${value} --kdf ${FAST_KDF} --passphrase-file ${pass}`) + ).toBe(0); + expect(existsSync(join(home, '.dcrypt', 'data', 'keychain-work.json'))).toBe(true); + } finally { + delete process.env.KEYCHAIN_ACCOUNT; + } + out = []; + // Without the namespace, the entry is invisible. + expect(await run('keychain list')).toBe(0); + expect(stdout()).toBe(''); + }); +}); + +describe('wallet defaults', () => { + it('derives bitcoin when no network is named', async () => { + expect(await run('wallet create --words 12 --json')).toBe(0); + const { accounts } = JSON.parse(stdout()); + expect(accounts).toHaveLength(1); + expect(accounts[0].network).toBe('bitcoin'); + expect(accounts[0].address.startsWith('bc1')).toBe(true); + }); }); describe('secrets', () => { diff --git a/packages/cli/package.json b/packages/cli/package.json index 50227f6..d55ea77 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,7 +42,7 @@ "@decryption/core": "workspace:*", "@decryption/hashes": "workspace:*", "@decryption/keys": "workspace:*", - "@decryption/legacy": "workspace:*", + "@decryption/cosmology-compat": "workspace:*", "@decryption/secrets": "workspace:*", "@decryption/shamir": "workspace:*", "@decryption/wallet": "workspace:*", diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 94adc67..dcdab83 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -1,16 +1,17 @@ import { CommandHandler, getPackageJson, Inquirerer } from 'inquirerer'; import { ParsedArgs } from 'minimist'; +import { cosmologyCommand } from './commands/cosmology'; import { decryptCommand, encryptCommand } from './commands/encrypt'; import { keychainCommand } from './commands/keychain'; import { keysCommand } from './commands/keys'; -import { legacyCommand } from './commands/legacy'; import { saltCommand } from './commands/salt'; import { secretsCommand } from './commands/secrets'; import { shamirCommand } from './commands/shamir'; import { vaultCommand } from './commands/vault'; import { walletCommand } from './commands/wallet'; import { Handler, takeFirst } from './utils/dispatch'; +import { envUsage } from './utils/env'; import { exitCodeFor, messageFor } from './utils/errors'; export const usageText = ` @@ -29,7 +30,7 @@ Commands: keychain Store named secrets locally, always encrypted shamir Split and recombine a secret into authenticated shares salt Two-layer encryption: data under a salt, salt under your passphrase - legacy Read and upgrade data from the old CryptoJS scheme + cosmology Read and upgrade data written by the cosmology CLI Global options: --json Machine-readable output where supported @@ -40,7 +41,7 @@ Global options: --help, -h Show this help; "dcrypt --help" for a command Nothing in dcrypt makes a network request, and passphrases are never accepted in argv. - +${envUsage} Exit codes: 1 usage 2 wrong passphrase 3 corrupt input 4 not found 5 not a recipient `; @@ -55,7 +56,9 @@ export const createCommandMap = (): Record => ({ keychain: keychainCommand, shamir: shamirCommand, salt: saltCommand, - legacy: legacyCommand, + cosmology: cosmologyCommand, + // The cosmology CLI's own name for it; kept so existing scripts keep working. + legacy: cosmologyCommand, }); /** Runs one command and maps thrown errors onto exit codes; used by the bin and by tests. */ diff --git a/packages/cli/src/commands/legacy.ts b/packages/cli/src/commands/cosmology.ts similarity index 68% rename from packages/cli/src/commands/legacy.ts rename to packages/cli/src/commands/cosmology.ts index a5ab34e..29bf802 100644 --- a/packages/cli/src/commands/legacy.ts +++ b/packages/cli/src/commands/cosmology.ts @@ -1,22 +1,25 @@ import { armor, encrypt } from '@decryption/core'; -import { decrypt as legacyDecrypt, decryptWithEncryptedSalt } from '@decryption/legacy'; +import { decrypt as compatDecrypt, decryptWithEncryptedSalt } from '@decryption/cosmology-compat'; import { Inquirerer } from 'inquirerer'; import { ParsedArgs } from 'minimist'; import { runSubcommand, takeFirst } from '../utils/dispatch'; +import { fromEnv } from '../utils/env'; import { CliError } from '../utils/errors'; import { readInput, writeOutput } from '../utils/io'; import { resolvePassphrase } from '../utils/passphrase'; import { resolveKdf } from './encrypt'; -export const legacyUsage = ` -Legacy Command: +export const cosmologyUsage = ` +Cosmology Command: - dcrypt legacy [OPTIONS] + dcrypt cosmology [OPTIONS] - Read data written by the old encryption demo and @cosmology/core (CryptoJS AES). - That format is unauthenticated and derives its key with a single round of MD5 — - use "upgrade" to move it onto the modern format as soon as you can. + Read data written by the cosmology CLI (CryptoJS AES). That format is + unauthenticated and derives its key with a single round of MD5 — use "upgrade" + to move it onto the modern format as soon as you can. + + "dcrypt legacy" is an alias, for scripts that already use that name. Subcommands: decrypt Decrypt an old blob and print the plaintext @@ -31,9 +34,13 @@ Options: --passphrase-stdin Read the new passphrase from stdin --help, -h Show this help message +Environment: + SALT, ENCRYPTED_SALT The cosmology CLI's own variables, still honoured + Examples: - dcrypt legacy decrypt --in old.txt --salt-file salt.txt - dcrypt legacy upgrade --in old.txt --salt-file salt.txt --out new.dcrypt + dcrypt cosmology decrypt --in old.txt --salt-file salt.txt + SALT=... dcrypt cosmology decrypt --in old.txt + dcrypt cosmology upgrade --in old.txt --salt-file salt.txt --out new.dcrypt `; const readSalt = async (argv: ParsedArgs, prompter: Inquirerer): Promise => { @@ -43,9 +50,11 @@ const readSalt = async (argv: ParsedArgs, prompter: Inquirerer): Promise } if (argv.salt !== undefined) { throw new CliError( - 'refusing to read the old salt from argv (it is visible in `ps`); use --salt-file ' + 'refusing to read the old salt from argv (it is visible in `ps`); use --salt-file or SALT' ); } + const envSalt = fromEnv('salt'); + if (envSalt !== undefined) return envSalt.trim(); const { salt } = await prompter.prompt<{ salt: string }>({} as { salt: string }, [ { type: 'password', name: 'salt', message: 'Legacy salt', required: true }, ]); @@ -56,10 +65,11 @@ const readLegacyPlaintext = async (argv: ParsedArgs, prompter: Inquirerer): Prom const { first, newArgv } = takeFirst(argv); const ciphertext = readInput(newArgv, first).trim(); const salt = await readSalt(newArgv, prompter); - const encryptedSalt = newArgv['encrypted-salt'] ?? newArgv.encryptedSalt; + const encryptedSalt = + newArgv['encrypted-salt'] ?? newArgv.encryptedSalt ?? fromEnv('encryptedSalt'); return typeof encryptedSalt === 'string' && encryptedSalt.length ? decryptWithEncryptedSalt(salt, encryptedSalt, ciphertext) - : legacyDecrypt(salt, ciphertext); + : compatDecrypt(salt, ciphertext); }; const decryptCmd = async (argv: ParsedArgs, prompter: Inquirerer): Promise => { @@ -75,10 +85,10 @@ const upgrade = async (argv: ParsedArgs, prompter: Inquirerer): Promise => writeOutput(argv, armor(encrypt(plaintext, passphrase, { kdf: resolveKdf(argv.kdf) }))); }; -export const legacyCommand = async (argv: ParsedArgs, prompter: Inquirerer): Promise => { +export const cosmologyCommand = async (argv: ParsedArgs, prompter: Inquirerer): Promise => { await runSubcommand(argv, prompter, { - name: 'legacy', - usage: legacyUsage, + name: 'cosmology', + usage: cosmologyUsage, handlers: { decrypt: decryptCmd, upgrade }, }); }; diff --git a/packages/cli/src/commands/keychain.ts b/packages/cli/src/commands/keychain.ts index 16e8c92..3d31d46 100644 --- a/packages/cli/src/commands/keychain.ts +++ b/packages/cli/src/commands/keychain.ts @@ -32,6 +32,10 @@ Options: --passphrase-stdin Read the passphrase from stdin --help, -h Show this help message +Environment: + KEYCHAIN_ACCOUNT Keychain namespace (default: dcrypt) + DCRYPT_PASSPHRASE The passphrase, for unattended runs + Examples: dcrypt keychain set github-token --in token.txt dcrypt keychain get github-token --out token.txt diff --git a/packages/cli/src/commands/wallet.ts b/packages/cli/src/commands/wallet.ts index 0b7d733..ec9870d 100644 --- a/packages/cli/src/commands/wallet.ts +++ b/packages/cli/src/commands/wallet.ts @@ -2,6 +2,7 @@ import { armor, encrypt } from '@decryption/core'; import { assertValidMnemonic, createMnemonic, + DEFAULT_NETWORK, deriveAccount, isValidMnemonic, NETWORKS, @@ -12,6 +13,7 @@ import { Inquirerer } from 'inquirerer'; import { ParsedArgs } from 'minimist'; import { runSubcommand, takeFirst, wantsHelp } from '../utils/dispatch'; +import { fromEnv } from '../utils/env'; import { CliError } from '../utils/errors'; import { emit, readInput, writeOutput } from '../utils/io'; import { resolvePassphrase } from '../utils/passphrase'; @@ -32,7 +34,7 @@ Subcommands: Options: --words 12, 15, 18, 21 or 24 (default: 24) - --network Network id, repeatable (default: cosmoshub) + --network Network id, repeatable (default: bitcoin) --account BIP44 account index (default: 0) --index BIP44 address index (default: 0) --path Explicit derivation path, overrides --account/--index @@ -43,18 +45,22 @@ Options: --json Machine-readable output --help, -h Show this help message +Environment: + MNEMONIC The mnemonic, for "address" and "validate" + Networks: ${Object.keys(NETWORKS).join(', ')} Examples: - dcrypt wallet create --words 12 --network osmosis --network ethereum + dcrypt wallet create --words 12 --network bitcoin --network ethereum dcrypt wallet create --encrypt --out wallet.dcrypt - dcrypt wallet address --in mnemonic.txt --network cosmoshub --index 3 + dcrypt wallet address --in mnemonic.txt --network bitcoin --index 3 + MNEMONIC="..." dcrypt wallet address --network ethereum echo "$MNEMONIC" | dcrypt wallet validate --in - `; const networksOf = (argv: ParsedArgs): string[] => { - const value = argv.network ?? argv.networks ?? 'cosmoshub'; + const value = argv.network ?? argv.networks ?? DEFAULT_NETWORK; return (Array.isArray(value) ? value : [value]).map(String); }; @@ -124,7 +130,10 @@ const validate = async (argv: ParsedArgs, prompter: Inquirerer): Promise = const readMnemonic = async (argv: ParsedArgs, prompter: Inquirerer): Promise => { const { first, newArgv } = takeFirst(argv); - if (first || newArgv.in || !process.stdin.isTTY) return readInput(newArgv, first).trim(); + if (first || newArgv.in) return readInput(newArgv, first).trim(); + const envMnemonic = fromEnv('mnemonic'); + if (envMnemonic !== undefined) return envMnemonic.trim(); + if (!process.stdin.isTTY) return readInput(newArgv, first).trim(); const { mnemonic } = await prompter.prompt<{ mnemonic: string }>({} as { mnemonic: string }, [ { type: 'password', name: 'mnemonic', message: 'Mnemonic', required: true }, ]); diff --git a/packages/cli/src/utils/env.ts b/packages/cli/src/utils/env.ts new file mode 100644 index 0000000..fa04864 --- /dev/null +++ b/packages/cli/src/utils/env.ts @@ -0,0 +1,48 @@ +/** + * Environment variable input, kept compatible with the cosmology CLI. + * + * Every name that CLI read still works — `MNEMONIC`, `SALT`, `ENCRYPTED_SALT`, + * `KEYCHAIN_ACCOUNT` — alongside `DCRYPT_`-prefixed aliases for use in + * environments where the bare names would collide. + * + * Resolution order everywhere is: explicit flag, then environment, then stdin, + * then an interactive prompt. Flags always win so a script can override an + * inherited variable, and argv still never carries a secret. + */ +export const ENV_VARS = { + mnemonic: ['MNEMONIC', 'DCRYPT_MNEMONIC'], + salt: ['SALT', 'DCRYPT_SALT'], + encryptedSalt: ['ENCRYPTED_SALT', 'DCRYPT_ENCRYPTED_SALT'], + keychainAccount: ['KEYCHAIN_ACCOUNT', 'DCRYPT_KEYCHAIN_ACCOUNT'], + passphrase: ['DCRYPT_PASSPHRASE'], +} as const; + +export type EnvInput = keyof typeof ENV_VARS; + +/** First non-empty value among the names for `input`, or undefined. */ +export const fromEnv = ( + input: EnvInput, + env: NodeJS.ProcessEnv = process.env +): string | undefined => { + for (const name of ENV_VARS[input]) { + const value = env[name]; + if (typeof value === 'string' && value.length) return value; + } + return undefined; +}; + +/** The keychain namespace, matching the cosmology CLI's `KEYCHAIN_ACCOUNT`. */ +export const keychainAccount = (env: NodeJS.ProcessEnv = process.env): string => + fromEnv('keychainAccount', env) ?? 'dcrypt'; + +export const envUsage = ` +Environment: + MNEMONIC A BIP39 mnemonic, for wallet commands + SALT The salt, for salt and legacy commands + ENCRYPTED_SALT The encrypted salt, for the two-layer scheme + KEYCHAIN_ACCOUNT Keychain namespace (default: dcrypt) + DCRYPT_PASSPHRASE The passphrase, for unattended runs + + Each also accepts a DCRYPT_-prefixed alias (DCRYPT_MNEMONIC, DCRYPT_SALT, ...). + Flags take precedence over the environment, which takes precedence over stdin. +`; diff --git a/packages/cli/src/utils/passphrase.ts b/packages/cli/src/utils/passphrase.ts index 0f792f8..afc779d 100644 --- a/packages/cli/src/utils/passphrase.ts +++ b/packages/cli/src/utils/passphrase.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { Inquirerer } from 'inquirerer'; import { ParsedArgs } from 'minimist'; +import { fromEnv } from './env'; import { CliError, EXIT } from './errors'; import { readStdin } from './io'; @@ -14,8 +15,9 @@ export interface PassphraseOptions { /** * Resolves a passphrase without ever accepting one on the command line. * - * Order: `--passphrase-file `, `--passphrase-stdin`, then an interactive masked prompt. - * Passing `--passphrase` is rejected outright: argv is visible to every process on the machine. + * Order: `--passphrase-file `, `DCRYPT_PASSPHRASE`, `--passphrase-stdin`, then an + * interactive masked prompt. Passing `--passphrase` is rejected outright: argv is visible + * to every process on the machine. */ export const resolvePassphrase = async ( argv: ParsedArgs, @@ -37,6 +39,11 @@ export const resolvePassphrase = async ( } } + const fromEnvironment = fromEnv('passphrase'); + if (fromEnvironment !== undefined) { + return fromEnvironment.replace(/\r?\n$/, ''); + } + if (argv['passphrase-stdin'] || argv.passphraseStdin) { return readStdin().replace(/\r?\n$/, ''); } diff --git a/packages/cli/src/utils/stash.ts b/packages/cli/src/utils/stash.ts index 4ce4313..691b354 100644 --- a/packages/cli/src/utils/stash.ts +++ b/packages/cli/src/utils/stash.ts @@ -4,6 +4,7 @@ import { appstash, resolve } from 'appstash'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { dirname, join } from 'path'; +import { keychainAccount } from './env'; import { CliError, EXIT } from './errors'; /** @@ -13,6 +14,7 @@ import { CliError, EXIT } from './errors'; * ~/.dcrypt/ * config/identity.json the user's X25519 identity, encrypted under a passphrase * data/keychain.json named secrets, each encrypted under a passphrase + * (KEYCHAIN_ACCOUNT selects keychain-.json instead) * data/vaults/.json team secrets files * ``` * @@ -23,7 +25,11 @@ export const APP_NAME = 'dcrypt'; export const dirs = () => appstash(APP_NAME, { ensure: true }); export const identityPath = (): string => resolve(dirs(), 'config', 'identity.json'); -export const keychainPath = (): string => resolve(dirs(), 'data', 'keychain.json'); +export const keychainPath = (): string => { + const account = keychainAccount(); + const file = account === 'dcrypt' ? 'keychain.json' : `keychain-${account}.json`; + return resolve(dirs(), 'data', file); +}; export const vaultPath = (name: string): string => join(resolve(dirs(), 'data', 'vaults'), `${name}.json`); diff --git a/packages/legacy/CHANGELOG.md b/packages/cosmology-compat/CHANGELOG.md similarity index 100% rename from packages/legacy/CHANGELOG.md rename to packages/cosmology-compat/CHANGELOG.md diff --git a/packages/legacy/README.md b/packages/cosmology-compat/README.md similarity index 87% rename from packages/legacy/README.md rename to packages/cosmology-compat/README.md index aa5ceed..4067e3b 100644 --- a/packages/legacy/README.md +++ b/packages/cosmology-compat/README.md @@ -1,7 +1,6 @@ -# @decryption/legacy +# @decryption/cosmology-compat -> **Deprecated by design.** This package exists so data written by the old -> [encryption-demo](https://github.com/constructive-io/encryption-demo) and `@cosmology/core` +> **Deprecated by design.** This package exists so data written by the cosmology CLI > stays readable. Do not use it to encrypt anything new — use > [`@decryption/core`](../core) instead. @@ -19,7 +18,7 @@ dependency), and turns the silent-failure case into a thrown `WrongPassphraseErr ## Usage ```typescript -import { decrypt, decryptWithEncryptedSalt, upgradeEnvelopeToString } from '@decryption/legacy'; +import { decrypt, decryptWithEncryptedSalt, upgradeEnvelopeToString } from '@decryption/cosmology-compat'; // Single-layer blobs const plaintext = decrypt(salt, oldCiphertext); @@ -34,7 +33,7 @@ const modern = upgradeEnvelopeToString(oldCiphertext, salt, newPassphrase); Raw CryptoJS compatibility, if you have blobs that never went through `@cosmology/core`: ```typescript -import { cryptoJsEncrypt, cryptoJsDecrypt } from '@decryption/legacy'; +import { cryptoJsEncrypt, cryptoJsDecrypt } from '@decryption/cosmology-compat'; cryptoJsDecrypt(base64Ciphertext, passphrase); // throws instead of returning '' ``` diff --git a/packages/legacy/__tests__/compat.test.ts b/packages/cosmology-compat/__tests__/compat.test.ts similarity index 100% rename from packages/legacy/__tests__/compat.test.ts rename to packages/cosmology-compat/__tests__/compat.test.ts diff --git a/packages/legacy/jest.config.js b/packages/cosmology-compat/jest.config.js similarity index 100% rename from packages/legacy/jest.config.js rename to packages/cosmology-compat/jest.config.js diff --git a/packages/legacy/package.json b/packages/cosmology-compat/package.json similarity index 87% rename from packages/legacy/package.json rename to packages/cosmology-compat/package.json index 7ca228b..2b0fc38 100644 --- a/packages/legacy/package.json +++ b/packages/cosmology-compat/package.json @@ -1,7 +1,7 @@ { - "name": "@decryption/legacy", + "name": "@decryption/cosmology-compat", "version": "0.2.0", - "description": "Read data written by the old CryptoJS/@cosmology/core encryption scheme, and upgrade it", + "description": "Read data written by the cosmology CLI encryption scheme, and upgrade it", "author": "Constructive ", "main": "index.js", "module": "esm/index.js", @@ -30,8 +30,8 @@ }, "keywords": [ "cryptojs", - "legacy", "cosmology", + "compat", "aes", "migration" ], diff --git a/packages/legacy/src/cosmology.ts b/packages/cosmology-compat/src/cosmology.ts similarity index 100% rename from packages/legacy/src/cosmology.ts rename to packages/cosmology-compat/src/cosmology.ts diff --git a/packages/legacy/src/cryptojs.ts b/packages/cosmology-compat/src/cryptojs.ts similarity index 100% rename from packages/legacy/src/cryptojs.ts rename to packages/cosmology-compat/src/cryptojs.ts diff --git a/packages/legacy/src/index.ts b/packages/cosmology-compat/src/index.ts similarity index 100% rename from packages/legacy/src/index.ts rename to packages/cosmology-compat/src/index.ts diff --git a/packages/legacy/src/upgrade.ts b/packages/cosmology-compat/src/upgrade.ts similarity index 100% rename from packages/legacy/src/upgrade.ts rename to packages/cosmology-compat/src/upgrade.ts diff --git a/packages/legacy/tsconfig.esm.json b/packages/cosmology-compat/tsconfig.esm.json similarity index 100% rename from packages/legacy/tsconfig.esm.json rename to packages/cosmology-compat/tsconfig.esm.json diff --git a/packages/legacy/tsconfig.json b/packages/cosmology-compat/tsconfig.json similarity index 100% rename from packages/legacy/tsconfig.json rename to packages/cosmology-compat/tsconfig.json diff --git a/packages/wallet/src/networks.ts b/packages/wallet/src/networks.ts index 314609a..7dd6165 100644 --- a/packages/wallet/src/networks.ts +++ b/packages/wallet/src/networks.ts @@ -23,8 +23,24 @@ const cosmos = (id: string, name: string, prefix: string, coinType = 118): Netwo prefix, }); -/** Built-in networks. Anything missing can be supplied with {@link defineNetwork}. */ +/** + * Built-in networks, in presentation order: Bitcoin, then Ethereum, then the rest. + * Anything missing can be supplied with {@link defineNetwork}. + */ export const NETWORKS: Record = { + bitcoin: { + id: 'bitcoin', + name: 'Bitcoin', + coinType: 0, + addressEncoding: 'p2wpkh', + prefix: 'bc', + }, + ethereum: { + id: 'ethereum', + name: 'Ethereum', + coinType: 60, + addressEncoding: 'evm', + }, cosmoshub: cosmos('cosmoshub', 'Cosmos Hub', 'cosmos'), osmosis: cosmos('osmosis', 'Osmosis', 'osmo'), juno: cosmos('juno', 'Juno', 'juno'), @@ -33,21 +49,14 @@ export const NETWORKS: Record = { celestia: cosmos('celestia', 'Celestia', 'celestia'), secret: cosmos('secret', 'Secret Network', 'secret', 529), terra: cosmos('terra', 'Terra', 'terra', 330), - ethereum: { - id: 'ethereum', - name: 'Ethereum', - coinType: 60, - addressEncoding: 'evm', - }, - bitcoin: { - id: 'bitcoin', - name: 'Bitcoin', - coinType: 0, - addressEncoding: 'p2wpkh', - prefix: 'bc', - }, }; +/** The network used when the caller does not name one. */ +export const DEFAULT_NETWORK = 'bitcoin'; + +/** Network ids in presentation order. */ +export const NETWORK_IDS = Object.keys(NETWORKS); + /** * Builds a network definition for a chain that is not built in — the common case for * cosmos-family chains, which differ only by bech32 prefix and coin type. diff --git a/packages/wallet/src/wallet.ts b/packages/wallet/src/wallet.ts index a17366d..49be597 100644 --- a/packages/wallet/src/wallet.ts +++ b/packages/wallet/src/wallet.ts @@ -3,7 +3,7 @@ import { bytesToHex } from '@decryption/hashes/utils'; import { publicKeyToAddress } from './address'; import { createMnemonic, mnemonicToSeed, WordCount } from './mnemonic'; -import { derivationPath, Network, resolveNetwork } from './networks'; +import { DEFAULT_NETWORK, derivationPath, Network, resolveNetwork } from './networks'; export interface AccountOptions { /** BIP44 account index (`m/44'/coin'/'/…`). */ @@ -77,7 +77,7 @@ export interface CreateWalletResult { /** Generates a new mnemonic and derives its first account on each requested network. */ export const createWallet = ( - networks: (string | Network)[] = ['cosmoshub'], + networks: (string | Network)[] = [DEFAULT_NETWORK], words: WordCount = 24, options: AccountOptions = {} ): CreateWalletResult => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03bee5a..9d0e1a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,15 +74,15 @@ importers: '@decryption/core': specifier: workspace:* version: link:../../packages/core/dist + '@decryption/cosmology-compat': + specifier: workspace:* + version: link:../../packages/cosmology-compat/dist '@decryption/hashes': specifier: workspace:* version: link:../../packages/hashes/dist '@decryption/keys': specifier: workspace:* version: link:../../packages/keys/dist - '@decryption/legacy': - specifier: workspace:* - version: link:../../packages/legacy/dist '@decryption/shamir': specifier: workspace:* version: link:../../packages/shamir/dist @@ -237,15 +237,15 @@ importers: '@decryption/core': specifier: workspace:* version: link:../core/dist + '@decryption/cosmology-compat': + specifier: workspace:* + version: link:../cosmology-compat/dist '@decryption/hashes': specifier: workspace:* version: link:../hashes/dist '@decryption/keys': specifier: workspace:* version: link:../keys/dist - '@decryption/legacy': - specifier: workspace:* - version: link:../legacy/dist '@decryption/secrets': specifier: workspace:* version: link:../secrets/dist @@ -293,65 +293,65 @@ importers: version: 0.3.0 publishDirectory: dist - packages/curves: + packages/cosmology-compat: dependencies: + '@decryption/base': + specifier: workspace:* + version: link:../base/dist + '@decryption/ciphers': + specifier: workspace:* + version: link:../ciphers/dist + '@decryption/core': + specifier: workspace:* + version: link:../core/dist '@decryption/hashes': specifier: workspace:* version: link:../hashes/dist devDependencies: + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 makage: specifier: 0.3.0 version: 0.3.0 publishDirectory: dist - packages/hashes: + packages/curves: + dependencies: + '@decryption/hashes': + specifier: workspace:* + version: link:../hashes/dist devDependencies: makage: specifier: 0.3.0 version: 0.3.0 publishDirectory: dist - packages/keys: - dependencies: - '@decryption/base': - specifier: workspace:* - version: link:../base/dist - '@decryption/core': - specifier: workspace:* - version: link:../core/dist - '@decryption/curves': - specifier: workspace:* - version: link:../curves/dist - '@decryption/hashes': - specifier: workspace:* - version: link:../hashes/dist + packages/hashes: devDependencies: makage: specifier: 0.3.0 version: 0.3.0 publishDirectory: dist - packages/legacy: + packages/keys: dependencies: '@decryption/base': specifier: workspace:* version: link:../base/dist - '@decryption/ciphers': - specifier: workspace:* - version: link:../ciphers/dist '@decryption/core': specifier: workspace:* version: link:../core/dist + '@decryption/curves': + specifier: workspace:* + version: link:../curves/dist '@decryption/hashes': specifier: workspace:* version: link:../hashes/dist devDependencies: - '@types/crypto-js': - specifier: ^4.2.2 - version: 4.2.2 - crypto-js: - specifier: ^4.2.0 - version: 4.2.0 makage: specifier: 0.3.0 version: 0.3.0