diff --git a/.changeset/keyset-backend-full-names.md b/.changeset/keyset-backend-full-names.md new file mode 100644 index 000000000..b1be09877 --- /dev/null +++ b/.changeset/keyset-backend-full-names.md @@ -0,0 +1,6 @@ +--- +'@cipherstash/auth': patch +'stash': patch +--- + +`keysetCreate` and `stash keyset create --backend` now also take the full backend names that their output gives: `aws-kms`, `gcp-kms`, `azure-key-vault` and `vault-transit`. The short names (`aws`, `gcp`, `azure`, `vault`, `static`) still work. Before, the output named a backend `aws-kms`, but the input refused that name. The error for an unknown backend now lists both forms. diff --git a/languages/typescript/packages/auth/native.d.ts b/languages/typescript/packages/auth/native.d.ts index 0c163a481..ae6de36e5 100644 --- a/languages/typescript/packages/auth/native.d.ts +++ b/languages/typescript/packages/auth/native.d.ts @@ -5,7 +5,11 @@ /** The options of [`keyset_create`]. */ export interface KeysetCreateOptions { - /** The key backend: `aws`, `gcp`, `azure`, `vault` or `static`. */ + /** + * The key backend: `aws`, `gcp`, `azure`, `vault` or `static`, or the + * full name that [`KeysetResult::backend`] gives (`aws-kms`, + * `gcp-kms`, `azure-key-vault`, `vault-transit`). + */ backend: string /** * The vendor key that wraps the keyset's key: an AWS KMS key ARN, a diff --git a/languages/typescript/packages/auth/src/keyset.rs b/languages/typescript/packages/auth/src/keyset.rs index 3a19d426f..bfc8983e3 100644 --- a/languages/typescript/packages/auth/src/keyset.rs +++ b/languages/typescript/packages/auth/src/keyset.rs @@ -40,7 +40,9 @@ const STATIC_KEY_LEN: usize = 32; #[napi(object)] #[derive(Debug, Clone)] pub struct KeysetCreateOptions { - /// The key backend: `aws`, `gcp`, `azure`, `vault` or `static`. + /// The key backend: `aws`, `gcp`, `azure`, `vault` or `static`, or the + /// full name that [`KeysetResult::backend`] gives (`aws-kms`, + /// `gcp-kms`, `azure-key-vault`, `vault-transit`). pub backend: String, /// The vendor key that wraps the keyset's key: an AWS KMS key ARN, a /// Cloud KMS crypto key name, an Azure Key Vault key identifier, a Vault @@ -230,7 +232,11 @@ fn vendor_config(vendor: KeyVendor, error: impl std::error::Error + 'static) -> // Parsing the options // --------------------------------------------------------------------------- -/// The `--backend` values, in the order the help text lists them. +/// The backends `stash keyset` can use, in the order the help text lists +/// them, each with its short name (the `--backend` value). The output +/// names a backend with [`KeyVendor::name`] (`aws-kms`, …), and the input +/// takes that name too, so a value the output gives is a valid input. +/// This table is the only list of the short names. pub(crate) const BACKENDS: [(&str, KeyVendor); 5] = [ ("aws", KeyVendor::Aws), ("gcp", KeyVendor::Gcp), @@ -239,18 +245,36 @@ pub(crate) const BACKENDS: [(&str, KeyVendor); 5] = [ ("static", KeyVendor::Static), ]; +/// The backend that `text` names: a short name of [`BACKENDS`] or the +/// [`KeyVendor::name`] of one of its vendors. fn parse_backend(text: &str) -> Result { BACKENDS .iter() - .find(|(name, _)| *name == text) + .find(|(short, vendor)| *short == text || vendor.name() == text) .map(|(_, vendor)| *vendor) .ok_or_else(|| { KeysetError::Options(format!( - "Unknown key backend {text:?}. Use one of: aws, gcp, azure, vault, static." + "Unknown key backend {text:?}. {}", + backend_choices() )) }) } +/// The accepted backend names, for an error message, from [`BACKENDS`]. +fn backend_choices() -> String { + let short: Vec<&str> = BACKENDS.iter().map(|(short, _)| *short).collect(); + let full: Vec<&str> = BACKENDS + .iter() + .map(|(_, vendor)| vendor.name()) + .filter(|name| !short.contains(name)) + .collect(); + format!( + "Use one of: {}. The full names {} also work.", + short.join(", "), + full.join(", ") + ) +} + fn parse_mode(text: &str) -> Result { KeyMode::ALL .into_iter() @@ -871,6 +895,24 @@ mod tests { } } + /// The input takes every name the output gives, and the table has every + /// vendor. + #[test] + fn the_backend_input_takes_the_output_names() { + for vendor in KeyVendor::ALL { + assert_eq!(parse_backend(vendor.name()).unwrap(), vendor); + assert!(BACKENDS.iter().any(|(_, v)| *v == vendor), "{vendor}"); + } + let Err(KeysetError::Options(message)) = parse_backend("zerokms") else { + panic!("zerokms is not a key vendor backend"); + }; + assert_eq!( + message, + "Unknown key backend \"zerokms\". Use one of: aws, gcp, azure, vault, static. \ + The full names aws-kms, gcp-kms, azure-key-vault, vault-transit also work." + ); + } + #[test] fn a_vendor_config_error_names_the_vendor() { let envelope = envelope(KeysetError::VendorConfig { diff --git a/languages/typescript/packages/cli/src/cli/registry.ts b/languages/typescript/packages/cli/src/cli/registry.ts index 46e681aa3..dd00658b3 100644 --- a/languages/typescript/packages/cli/src/cli/registry.ts +++ b/languages/typescript/packages/cli/src/cli/registry.ts @@ -881,7 +881,8 @@ export const registry: CommandGroup[] = [ { name: '--backend', value: '', - description: 'The key vendor backend. Required.', + description: + 'The key vendor backend. The full names that the output gives (aws-kms, gcp-kms, azure-key-vault, vault-transit) also work. Required.', }, { name: '--vendor-key', diff --git a/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts b/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts index bcf1853c5..7e9359ead 100644 --- a/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts +++ b/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts @@ -135,10 +135,25 @@ describe('stash keyset create', () => { 1, ) expect(errors()).toContain(messages.keyset.unknownBackend) + expect(errors()).toContain( + 'Use one of: aws, gcp, azure, vault, static. The full names aws-kms, gcp-kms, azure-key-vault, vault-transit also work.', + ) const hints = clack.log.info.mock.calls.map((call) => String(call[0])) expect(hints).toContain(messages.keyset.zeroKmsNote) }) + it('takes the backend name that its own output gives', async () => { + authMock.keysetCreate.mockResolvedValue({ data: result() }) + await keysetCreateCommand({ + backend: 'aws-kms', + vendorKey: 'arn:aws:kms:us-east-1:111122223333:key/k', + mode: 'wrapped', + }) + expect(authMock.keysetCreate).toHaveBeenCalledWith( + expect.objectContaining({ backend: 'aws-kms' }), + ) + }) + it('refuses wrapped data keys on the static backend', async () => { await expectExit( keysetCreateCommand({ diff --git a/languages/typescript/packages/cli/src/commands/keyset/index.ts b/languages/typescript/packages/cli/src/commands/keyset/index.ts index 329b667d2..f16441972 100644 --- a/languages/typescript/packages/cli/src/commands/keyset/index.ts +++ b/languages/typescript/packages/cli/src/commands/keyset/index.ts @@ -22,14 +22,37 @@ import { emitJsonError, emitJsonEvent } from '../auth/events.js' */ const CHROME = { output: process.stderr } as const -/** The `--backend` values. The Rust side holds the same list. */ -export const KEYSET_BACKENDS = [ - 'aws', - 'gcp', - 'azure', - 'vault', - 'static', -] as const +/** + * The `--backend` values: each short name, and the full name that the output + * (`# backend: …`, the `backend` of `--json`) and the SDKs use. `--backend` + * takes either, so a value the output gives is a valid input. The Rust side + * (`BACKENDS` in `@cipherstash/auth`) holds the same table. + */ +export const KEYSET_BACKEND_NAMES = { + aws: 'aws-kms', + gcp: 'gcp-kms', + azure: 'azure-key-vault', + vault: 'vault-transit', + static: 'static', +} as const + +/** The short `--backend` names, in the order the help text lists them. */ +export const KEYSET_BACKENDS = Object.keys(KEYSET_BACKEND_NAMES) as Array< + keyof typeof KEYSET_BACKEND_NAMES +> + +/** The full names that are not also a short name. */ +const KEYSET_FULL_BACKENDS = Object.values(KEYSET_BACKEND_NAMES).filter( + (name) => !(KEYSET_BACKENDS as readonly string[]).includes(name), +) + +/** Whether `--backend` takes `value`. */ +export function isKeysetBackend(value: string): boolean { + return ( + (KEYSET_BACKENDS as readonly string[]).includes(value) || + (KEYSET_FULL_BACKENDS as readonly string[]).includes(value) + ) +} /** The `--mode` values. */ export const KEYSET_MODES = ['wrapped', 'derived'] as const @@ -296,10 +319,10 @@ function validateCreate(options: KeysetCreateOptions) { ) } const backend = options.backend as string - if (!(KEYSET_BACKENDS as readonly string[]).includes(backend)) { + if (!isKeysetBackend(backend)) { throw new KeysetError( 'invalid_backend', - `${messages.keyset.unknownBackend} '${backend}'. Use one of: ${KEYSET_BACKENDS.join(', ')}.`, + `${messages.keyset.unknownBackend} '${backend}'. Use one of: ${KEYSET_BACKENDS.join(', ')}. The full names ${KEYSET_FULL_BACKENDS.join(', ')} also work.`, messages.keyset.zeroKmsNote, ) } diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md index b7fe1bbcc..d163170a1 100644 --- a/skills/stash-cli/SKILL.md +++ b/skills/stash-cli/SKILL.md @@ -835,6 +835,9 @@ CS_KEYSET_RECORD_USERS=stack-keyset:… - `--backend `, `--vendor-key ` and `--mode ` are required. `--name ` labels the keyset and names the variable (`CS_KEYSET_RECORD_`, else `CS_KEYSET_RECORD`). + The output names the backend in full (`aws-kms`, `gcp-kms`, + `azure-key-vault`, `vault-transit`, `static`), and `--backend` takes those + full names too. - **The record is not secret.** Only the vendor key can unwrap its key. Keep it in config or an env var. **No SDK reads `CS_KEYSET_RECORD*` by itself**: the app passes the value to `encrypt.WithKeysetRecords` (Go) or