diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 241146d8a..0f0a56998 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -16,6 +16,7 @@ import { OpenTDF, DecoratedStream, isPublicKeyAlgorithm, + PUBLIC_KEY_ALGORITHMS, } from '@opentdf/sdk'; import { CLIError, Level, log } from './logger.js'; import * as assertions from '@opentdf/sdk/assertions'; @@ -462,6 +463,7 @@ export const handleArgs = (args: string[]) => { group: 'Encrypt Options:', desc: 'Key type for wrapping keys', type: 'string', + choices: PUBLIC_KEY_ALGORITHMS, default: 'rsa:2048', }, mimeType: { @@ -475,6 +477,7 @@ export const handleArgs = (args: string[]) => { group: 'Decrypt Options:', desc: 'Key type for rewrap', type: 'string', + choices: PUBLIC_KEY_ALGORITHMS, default: 'rsa:2048', }, userId: { diff --git a/lib/src/access.ts b/lib/src/access.ts index 5f2b11cd0..c3824ef01 100644 --- a/lib/src/access.ts +++ b/lib/src/access.ts @@ -2,6 +2,11 @@ import { type AuthConfig, resolveAuthConfig } from './auth/interceptors.js'; import { RewrapResponse } from './platform/kas/kas_pb.js'; import { getPlatformUrlFromKasEndpoint, validateSecureUrl } from './utils.js'; import { base64 } from './encodings/index.js'; +import { + KEY_ALGORITHMS, + type KeyAlgorithm, + isKeyAlgorithm, +} from '../tdf3/src/crypto/declarations.js'; import { fetchKasBasePubKey, @@ -87,16 +92,11 @@ export const rewrapAdditionalContextHeader = ( return base64.encode(JSON.stringify(context)); }; -export type KasPublicKeyAlgorithm = - | 'ec:secp256r1' - | 'ec:secp384r1' - | 'ec:secp521r1' - | 'rsa:2048' - | 'rsa:4096'; +export const PUBLIC_KEY_ALGORITHMS = KEY_ALGORITHMS; -export const isPublicKeyAlgorithm = (a: string): a is KasPublicKeyAlgorithm => { - return a === 'ec:secp256r1' || a === 'rsa:2048'; -}; +export type KasPublicKeyAlgorithm = KeyAlgorithm; + +export const isPublicKeyAlgorithm = (a: string): a is KasPublicKeyAlgorithm => isKeyAlgorithm(a); export const keyAlgorithmToPublicKeyAlgorithm = (k: CryptoKey): KasPublicKeyAlgorithm => { const a = k.algorithm; diff --git a/lib/src/auth/dpop.ts b/lib/src/auth/dpop.ts index 61a6ae0ec..4801fee83 100644 --- a/lib/src/auth/dpop.ts +++ b/lib/src/auth/dpop.ts @@ -6,7 +6,9 @@ import type { KeyPair, PrivateKey, AsymmetricSigningAlgorithm, + KeyAlgorithm, } from '../../tdf3/src/crypto/declarations.js'; +import { isRsaKeyAlgorithm } from '../../tdf3/src/crypto/declarations.js'; export type JsonObject = { [Key in string]?: JsonValue }; export type JsonArray = JsonValue[]; @@ -119,8 +121,8 @@ class UnsupportedOperationError extends Error { /** * Determines a supported JWS `alg` identifier from PublicKeyInfo algorithm string. */ -function determineJWSAlgorithmFromKeyInfo(algorithm: string): JWSAlgorithm { - if (algorithm.startsWith('rsa:')) { +function determineJWSAlgorithmFromKeyInfo(algorithm: KeyAlgorithm): JWSAlgorithm { + if (isRsaKeyAlgorithm(algorithm)) { return 'RS256'; } switch (algorithm) { diff --git a/lib/src/opentdf.ts b/lib/src/opentdf.ts index 738ab10dc..d79ff6f58 100644 --- a/lib/src/opentdf.ts +++ b/lib/src/opentdf.ts @@ -14,6 +14,7 @@ import { import { type KasPublicKeyAlgorithm, OriginAllowList, + PUBLIC_KEY_ALGORITHMS, fetchKeyAccessServers, isPublicKeyAlgorithm, } from './access.js'; @@ -45,6 +46,7 @@ export { type Payload, type Segment, type SplitType, + PUBLIC_KEY_ALGORITHMS, isPublicKeyAlgorithm, }; diff --git a/lib/tdf3/src/client/index.ts b/lib/tdf3/src/client/index.ts index f98c7e720..ed67f9acd 100644 --- a/lib/tdf3/src/client/index.ts +++ b/lib/tdf3/src/client/index.ts @@ -43,7 +43,12 @@ import { } from '../../../src/access.js'; import { ConfigurationError } from '../../../src/errors.js'; import { AesGcmCipher } from '../ciphers/aes-gcm-cipher.js'; -import { type KeyPair, type SymmetricKey } from '../crypto/declarations.js'; +import { + isEcKeyAlgorithm, + isRsaKeyAlgorithm, + type KeyPair, + type SymmetricKey, +} from '../crypto/declarations.js'; import * as defaultCryptoService from '../crypto/index.js'; import { type AttributeObject, @@ -730,18 +735,12 @@ export class Client { ); } let type: KeyAccessType; - switch (algorithm) { - case 'rsa:2048': - case 'rsa:4096': - type = 'wrapped'; - break; - case 'ec:secp384r1': - case 'ec:secp521r1': - case 'ec:secp256r1': - type = 'ec-wrapped'; - break; - default: - throw new ConfigurationError(`Unsupported algorithm ${algorithm}`); + if (isRsaKeyAlgorithm(algorithm)) { + type = 'wrapped'; + } else if (isEcKeyAlgorithm(algorithm)) { + type = 'ec-wrapped'; + } else { + throw new ConfigurationError(`Unsupported algorithm ${algorithm}`); } return buildKeyAccess({ alg: algorithm, diff --git a/lib/tdf3/src/crypto/core/key-format.ts b/lib/tdf3/src/crypto/core/key-format.ts index a8beb736f..836bb0c13 100644 --- a/lib/tdf3/src/crypto/core/key-format.ts +++ b/lib/tdf3/src/crypto/core/key-format.ts @@ -1,4 +1,7 @@ import { + ecAlgorithmToCurve, + isEcKeyAlgorithm, + isRsaKeyAlgorithm, type KeyAlgorithm, type KeyOptions, MIN_ASYMMETRIC_KEY_SIZE_BITS, @@ -238,7 +241,7 @@ export async function importPublicKey(pem: string, options: KeyOptions): Promise let cryptoAlgorithm: RsaHashedImportParams | EcKeyImportParams; let keyUsages: KeyUsage[]; - if (algorithm.startsWith('rsa:')) { + if (isRsaKeyAlgorithm(algorithm)) { if (usage === 'encrypt') { cryptoAlgorithm = rsaOaepSha1(); keyUsages = ['encrypt']; @@ -248,18 +251,8 @@ export async function importPublicKey(pem: string, options: KeyOptions): Promise } else { throw new ConfigurationError('RSA keys only support usage: encrypt or sign'); } - } else if (algorithm.startsWith('ec:')) { - const curve = algorithm.split(':')[1]; - const namedCurve = - curve === 'secp256r1' - ? 'P-256' - : curve === 'secp384r1' - ? 'P-384' - : curve === 'secp521r1' - ? 'P-521' - : (() => { - throw new ConfigurationError(`Unsupported EC curve: ${curve}`); - })(); + } else if (isEcKeyAlgorithm(algorithm)) { + const namedCurve = ecAlgorithmToCurve(algorithm); if (usage === 'derive') { cryptoAlgorithm = { name: 'ECDH', namedCurve }; @@ -344,7 +337,7 @@ export async function importPrivateKey(pem: string, options: KeyOptions): Promis let cryptoAlgorithm: RsaHashedImportParams | EcKeyImportParams; let keyUsages: KeyUsage[]; - if (algorithm.startsWith('rsa:')) { + if (isRsaKeyAlgorithm(algorithm)) { if (usage === 'encrypt') { cryptoAlgorithm = rsaOaepSha1(); keyUsages = ['decrypt']; @@ -354,18 +347,8 @@ export async function importPrivateKey(pem: string, options: KeyOptions): Promis } else { throw new ConfigurationError('RSA keys only support usage: encrypt or sign'); } - } else if (algorithm.startsWith('ec:')) { - const curve = algorithm.split(':')[1]; - const namedCurve = - curve === 'secp256r1' - ? 'P-256' - : curve === 'secp384r1' - ? 'P-384' - : curve === 'secp521r1' - ? 'P-521' - : (() => { - throw new ConfigurationError(`Unsupported EC curve: ${curve}`); - })(); + } else if (isEcKeyAlgorithm(algorithm)) { + const namedCurve = ecAlgorithmToCurve(algorithm); if (usage === 'derive') { cryptoAlgorithm = { name: 'ECDH', namedCurve }; diff --git a/lib/tdf3/src/crypto/core/keys.ts b/lib/tdf3/src/crypto/core/keys.ts index cc0ff87af..58da359e2 100644 --- a/lib/tdf3/src/crypto/core/keys.ts +++ b/lib/tdf3/src/crypto/core/keys.ts @@ -1,7 +1,11 @@ import { + ecAlgorithmToCurve, + isEcKeyAlgorithm, + isRsaKeyAlgorithm, type KeyAlgorithm, type PrivateKey, type PublicKey, + rsaAlgorithmToModulusBits, type SymmetricKey, } from '../declarations.js'; @@ -15,18 +19,10 @@ export function wrapPublicKey(key: CryptoKey, algorithm: KeyAlgorithm): PublicKe algorithm, _internal: key, }; - if (algorithm.startsWith('rsa:')) { - result.modulusBits = parseInt(algorithm.split(':')[1], 10); - } else if (algorithm.startsWith('ec:')) { - const curvePart = algorithm.split(':')[1]; - result.curve = - curvePart === 'secp256r1' - ? 'P-256' - : curvePart === 'secp384r1' - ? 'P-384' - : curvePart === 'secp521r1' - ? 'P-521' - : undefined; + if (isRsaKeyAlgorithm(algorithm)) { + result.modulusBits = rsaAlgorithmToModulusBits(algorithm); + } else if (isEcKeyAlgorithm(algorithm)) { + result.curve = ecAlgorithmToCurve(algorithm); } return result as PublicKey; } @@ -41,18 +37,10 @@ export function wrapPrivateKey(key: CryptoKey, algorithm: KeyAlgorithm): Private algorithm, _internal: key, }; - if (algorithm.startsWith('rsa:')) { - result.modulusBits = parseInt(algorithm.split(':')[1], 10); - } else if (algorithm.startsWith('ec:')) { - const curvePart = algorithm.split(':')[1]; - result.curve = - curvePart === 'secp256r1' - ? 'P-256' - : curvePart === 'secp384r1' - ? 'P-384' - : curvePart === 'secp521r1' - ? 'P-521' - : undefined; + if (isRsaKeyAlgorithm(algorithm)) { + result.modulusBits = rsaAlgorithmToModulusBits(algorithm); + } else if (isEcKeyAlgorithm(algorithm)) { + result.curve = ecAlgorithmToCurve(algorithm); } return result as PrivateKey; } diff --git a/lib/tdf3/src/crypto/declarations.ts b/lib/tdf3/src/crypto/declarations.ts index ba0c789dd..84c3c4307 100644 --- a/lib/tdf3/src/crypto/declarations.ts +++ b/lib/tdf3/src/crypto/declarations.ts @@ -21,15 +21,42 @@ export type PemKeyPair = { privateKey: string; }; +export const EC_KEY_ALGORITHMS = ['ec:secp256r1', 'ec:secp384r1', 'ec:secp521r1'] as const; +export const RSA_KEY_ALGORITHMS = ['rsa:2048', 'rsa:4096'] as const; + +/** Order is significant: re-exported as `PUBLIC_KEY_ALGORITHMS` in `access.ts` and consumed as an ordered list (e.g. CLI `--choices` output). */ +export const KEY_ALGORITHMS = [...EC_KEY_ALGORITHMS, ...RSA_KEY_ALGORITHMS] as const; + +export type EcKeyAlgorithm = (typeof EC_KEY_ALGORITHMS)[number]; +export type RsaKeyAlgorithm = (typeof RSA_KEY_ALGORITHMS)[number]; + /** * Key algorithm identifier combining key type and parameters. */ -export type KeyAlgorithm = - | 'rsa:2048' - | 'rsa:4096' - | 'ec:secp256r1' - | 'ec:secp384r1' - | 'ec:secp521r1'; +export type KeyAlgorithm = EcKeyAlgorithm | RsaKeyAlgorithm; + +export const isEcKeyAlgorithm = (a: string): a is EcKeyAlgorithm => + (EC_KEY_ALGORITHMS as readonly string[]).includes(a); +export const isRsaKeyAlgorithm = (a: string): a is RsaKeyAlgorithm => + (RSA_KEY_ALGORITHMS as readonly string[]).includes(a); +export const isKeyAlgorithm = (a: string): a is KeyAlgorithm => + (KEY_ALGORITHMS as readonly string[]).includes(a); + +const EC_ALGORITHM_CURVES: Record = { + 'ec:secp256r1': 'P-256', + 'ec:secp384r1': 'P-384', + 'ec:secp521r1': 'P-521', +}; +/** The elliptic curve for an `ec:*` key algorithm. */ +export const ecAlgorithmToCurve = (alg: EcKeyAlgorithm): ECCurve => EC_ALGORITHM_CURVES[alg]; + +const RSA_ALGORITHM_MODULUS_BITS: Record = { + 'rsa:2048': 2048, + 'rsa:4096': 4096, +}; +/** The modulus bit length for an `rsa:*` key algorithm. */ +export const rsaAlgorithmToModulusBits = (alg: RsaKeyAlgorithm): 2048 | 4096 => + RSA_ALGORITHM_MODULUS_BITS[alg]; /** * Options for key generation and import. @@ -156,7 +183,7 @@ export type HkdfParams = { */ export type PublicKeyInfo = { /** Detected algorithm of the key. */ - algorithm: 'rsa:2048' | 'rsa:4096' | 'ec:secp256r1' | 'ec:secp384r1' | 'ec:secp521r1'; + algorithm: KeyAlgorithm; /** Normalized PEM string. */ pem: string; };