From a031327708273d05172b6ce19c4ca9fb2dcea36d Mon Sep 17 00:00:00 2001 From: Dmitri Zagidulin Date: Sun, 12 Jul 2026 13:57:09 -0400 Subject: [PATCH] Add support for key create --type aes256. Signed-off-by: Dmitri Zagidulin --- CHANGELOG.md | 14 +++ README.md | 28 +++++- package.json | 10 +- pnpm-lock.yaml | 207 ++++++++++++++++++++------------------- pnpm-workspace.yaml | 8 +- src/commands/key.test.ts | 90 +++++++++++++++++ src/commands/key.ts | 69 ++++++++++++- src/meta.ts | 2 +- 8 files changed, 312 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd4d73..a0930f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # History +## Unreleased - TBD + +### Added + +- `key create --type aes256` generates a symmetric AES-256 key-encryption key + (KEK): 32 random bytes carried as a `Multikey` `secretKeyMultibase` (the + AES-256 multicodec header `0xa2 0x01` followed by the raw key bytes), + identified by a `urn:kek:sha256:` id derived from the SHA-256 digest of the + raw key bytes. This is the form Wallet Attached Storage servers accept for + their at-rest key-encryption-key configuration. Generation is always fresh and + random, so `--with-seed` and `SECRET_KEY_SEED` are not supported for this + type. `key show` prints only the safe `id` and `type` for a saved KEK, never + the secret material. + ## 0.12.0 - 2026-07-09 ### Changed diff --git a/README.md b/README.md index c6e04c9..f5855c9 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ SECRET_KEY_SEED=z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv ./di key create ``` Specify an explicit key type with `--type` (defaults to `ed25519`; supported: -`ed25519`, `ecdsa`, `x25519`, `hmac`): +`ed25519`, `ecdsa`, `x25519`, `hmac`, `aes256`): ``` SECRET_KEY_SEED=z1Aaj5A4UCsd... ./di key create --type ed25519 @@ -244,6 +244,32 @@ half), identified by a random `urn:uuid:` id: HMAC key generation is non-deterministic, so `--with-seed` and `SECRET_KEY_SEED` are not supported with `--type hmac`. +Generate a symmetric AES-256 key-encryption key (KEK) -- 32 random bytes carried +as a Multikey `secretKeyMultibase` -- with `--type aes256`: + +``` +./di key create --type aes256 +``` + +It is serialized as a `Multikey` with the secret carried as a base58btc +`secretKeyMultibase` (the AES-256 multicodec header `0xa2 0x01` followed by the +32 raw key bytes, no public half), identified by a `urn:kek:sha256:` id derived +from the SHA-256 digest of the raw key bytes: + +```json +{ + "id": "urn:kek:sha256:...", + "type": "Multikey", + "secretKeyMultibase": "z..." +} +``` + +This is the form Wallet Attached Storage servers accept for their at-rest +key-encryption-key configuration (`KMS_RECORD_KEK` / `KMS_RECORD_KEKS`), where +each KEK is identified by the same derived `urn:kek:sha256:` id. Generation is +always fresh and random, so `--with-seed` and `SECRET_KEY_SEED` are not +supported with `--type aes256`. + Save the key to local wallet storage (`~/.config/did-cli-wallet/keys/` by default, or `$WALLET_DIR/keys/` if set) with `--save`. A `.meta.json` metadata sidecar is diff --git a/package.json b/package.json index b78b707..1609561 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "@digitalcredentials/bnid": "^5.0.0", "@digitalcredentials/issuer-registry-client": "^4.0.0", - "@interop/data-integrity-core": "^8.1.0", + "@interop/data-integrity-core": "^8.2.0", "@interop/data-integrity-proof": "^3.4.1", "@interop/did-io": "^4.0.6", "@interop/did-method-key": "^7.3.3", @@ -36,14 +36,14 @@ "@interop/ecdsa-signature": "2.0.4", "@interop/ed25519-signature": "^7.1.3", "@interop/ed25519-verification-key": "^8.0.2", - "@interop/edv-client": "^17.5.0", + "@interop/edv-client": "^17.6.1", "@interop/ezcap": "^7.3.2", - "@interop/minimal-cipher": "^7.5.0", + "@interop/minimal-cipher": "^7.6.1", "@interop/security-document-loader": "^9.4.1", "@interop/vc": "^11.0.5", "@interop/verifier-core": "^3.3.1", - "@interop/was-client": "^0.13.3", - "@interop/x25519-key-agreement-key": "^5.1.1", + "@interop/was-client": "^0.14.5", + "@interop/x25519-key-agreement-key": "^5.2.0", "@interop/zcap": "^11.0.4", "@scure/base": "^2.2.0", "commander": "^15.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbdbc2e..a61ae2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 '@interop/data-integrity-core': - specifier: ^8.1.0 - version: 8.1.0 + specifier: ^8.2.0 + version: 8.2.0 '@interop/data-integrity-proof': specifier: ^3.4.1 version: 3.4.1 @@ -45,14 +45,14 @@ importers: specifier: ^8.0.2 version: 8.0.2 '@interop/edv-client': - specifier: ^17.5.0 - version: 17.5.0 + specifier: ^17.6.1 + version: 17.6.1 '@interop/ezcap': specifier: ^7.3.2 version: 7.3.2 '@interop/minimal-cipher': - specifier: ^7.5.0 - version: 7.5.0 + specifier: ^7.6.1 + version: 7.6.1 '@interop/security-document-loader': specifier: ^9.4.1 version: 9.4.1 @@ -63,11 +63,11 @@ importers: specifier: ^3.3.1 version: 3.3.1 '@interop/was-client': - specifier: ^0.13.3 - version: 0.13.3(@interop/jsonld-signatures@11.8.2) + specifier: ^0.14.5 + version: 0.14.5(@interop/jsonld-signatures@11.8.2) '@interop/x25519-key-agreement-key': - specifier: ^5.1.1 - version: 5.1.1 + specifier: ^5.2.0 + version: 5.2.0 '@interop/zcap': specifier: ^11.0.4 version: 11.0.4 @@ -80,22 +80,22 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.6.0) + version: 10.0.1(eslint@10.7.0) '@types/node': specifier: ^25.6.0 version: 25.9.5 eslint: specifier: ^10.2.0 - version: 10.6.0 + version: 10.7.0 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.6.0) + version: 10.1.8(eslint@10.7.0) globals: specifier: ^17.7.0 version: 17.7.0 prettier: specifier: ^3.8.2 - version: 3.9.4 + version: 3.9.5 tsx: specifier: ^4.21.0 version: 4.23.0 @@ -104,7 +104,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.58.2 - version: 8.63.0(eslint@10.6.0)(typescript@5.9.3) + version: 8.63.0(eslint@10.7.0)(typescript@5.9.3) packages: @@ -451,8 +451,8 @@ packages: react-native-get-random-values: optional: true - '@interop/data-integrity-core@8.1.0': - resolution: {integrity: sha512-85NyTH7UhFtFfgnK0J6d/Y/LIMXY8TzE2rMwkOugfrzbgaxVO6YFTkmaNUqnpM4EolJR1t4FN3Xvu69OgXRf/g==} + '@interop/data-integrity-core@8.2.0': + resolution: {integrity: sha512-T3ObUEH1+cEtKU8d0euDTMNrQiXZc5FZUP/T9sbc+wVo+bSBebnh9tUNLpDnqTGy1d6m/2C0JdrTbLnJNjgJCg==} engines: {node: '>=24.0'} '@interop/data-integrity-proof@3.4.1': @@ -506,8 +506,8 @@ packages: react-native-get-random-values: optional: true - '@interop/edv-client@17.5.0': - resolution: {integrity: sha512-je6+KMxBduahv4tFY/vDa3idwvgOlOx9OaMljh7VQlrN+nDnEsuK+MlfXPT58WJjyMcUhD3phowgykeIFygo0A==} + '@interop/edv-client@17.6.1': + resolution: {integrity: sha512-CFEGb5F/tDc0MxE3wMNq+UotcR0f1KsiqeMa1Ppuba56PqCsjsld5vuFDktD/6yTtOrXuXO6vSe9oF8icFE2Yg==} engines: {node: '>=24.0'} peerDependencies: react-native-get-random-values: ^2.0.0 @@ -552,16 +552,16 @@ packages: resolution: {integrity: sha512-6c8dnoQNIiL25Q4YovC1a44mREO1lzW3F/lmvdwbAMMbhtGGRoQeqo1H4diwNhBzaxRrUMXrAIwbM26POmI/Yg==} engines: {node: '>=24.0'} - '@interop/minimal-cipher@7.5.0': - resolution: {integrity: sha512-fvX0udPj3SsnE/qDe00eqmIUWvZQ4nxr+0KZW/HYBvHMM7t/AjjC+ew5NC/f2zy4f+4HDSpAfnKild7z027dBA==} + '@interop/minimal-cipher@7.6.1': + resolution: {integrity: sha512-fgxXARys+2Pr2DMGUiLOnJKpALgStqHWxaD7zBF29pf7IeFllZ/0b22cDdV/MheAvXP7Uula46viFSrapwtRFQ==} engines: {node: '>=24.0'} '@interop/security-document-loader@9.4.1': resolution: {integrity: sha512-AilGgeLYER/6SP9jxKp5wCT2SLP1YYhahsRFkCJT8gxf8WiaI53Vz8BpfKuIvwaYa+rIicT9wn/+nWUhc+jWMA==} engines: {node: '>=24.0'} - '@interop/storage-core@0.3.3': - resolution: {integrity: sha512-dSdYREbmJIiu4hffZktF/KI6H+aP3aWB8LnTw56GcA5j7+EzPgY1YD5eTcHD5g9igkI0NcT5CAmWLEYttv6KHg==} + '@interop/storage-core@0.3.4': + resolution: {integrity: sha512-QhAC/O5veP1GlPIWdcjVJKIeggsSh6BcFhJjviGDXdky/fwskm1xxDhyJ/6ZaILleM9nxNq/V/q6U9AwDduKlQ==} engines: {node: '>=22.0'} '@interop/vc-bitstring-status-list@3.0.4': @@ -576,12 +576,12 @@ packages: resolution: {integrity: sha512-QvRM/VTVTG+9VpOnEd15Eklt/IwFc+XJHu7e0xJI4jKVwr43qxBwblWPeTvvM5AdfCkmIxzlvIXdnf62vO0ZUg==} engines: {node: '>=24.0'} - '@interop/was-client@0.13.3': - resolution: {integrity: sha512-DmmldTNhqp5cq92OwZOWTNnNSWOT43NCe8SxNTVIdpmna1Q3PhMy9Z4nNJPx4aLR94f1tY/j3/gBSylEahpcbw==} + '@interop/was-client@0.14.5': + resolution: {integrity: sha512-ZJJYsxUD60n+UN3tKb3WeHK/tAionUXk43NkVFfXixOwAVuyASMg3RBZgiv8IzYHWq1n1EacrfN8YSUfFoOSLg==} engines: {node: '>=24.0'} - '@interop/x25519-key-agreement-key@5.1.1': - resolution: {integrity: sha512-OfrYufUk1J6VWDZk6HWad8378Xa9VJq8rmkhiarzleti2StnQ+zfudTVXkAiI/dFmgkRscR/93BZdKrAnPTNKA==} + '@interop/x25519-key-agreement-key@5.2.0': + resolution: {integrity: sha512-e8yYMYnK2Ok0FpAqiTs/rJoIStPZ+0L4mMPh6mkKwyLaEc/yYBg9le89uhgoj4vS5gpjL23yGkfG/WoEESO+9Q==} engines: {node: '>=24.0'} '@interop/zcap@11.0.4': @@ -891,8 +891,8 @@ packages: resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} engines: {node: '>=6.0.0'} - baseline-browser-mapping@2.10.42: - resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + baseline-browser-mapping@2.10.43: + resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1081,8 +1081,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.6.0: - resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + eslint@10.7.0: + resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1242,8 +1242,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} image-size@1.2.1: @@ -1500,8 +1500,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.50: - resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} nullthrows@1.1.1: @@ -1565,8 +1565,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.9.4: - resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} engines: {node: '>=14'} hasBin: true @@ -1681,8 +1681,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.9.0: - resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} source-map-support@0.5.21: @@ -2107,9 +2107,9 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0)': dependencies: - eslint: 10.6.0 + eslint: 10.7.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2130,9 +2130,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.6.0)': + '@eslint/js@10.0.1(eslint@10.7.0)': optionalDependencies: - eslint: 10.6.0 + eslint: 10.7.0 '@eslint/object-schema@3.0.5': {} @@ -2161,24 +2161,24 @@ snapshots: dependencies: '@scure/base': 2.2.0 - '@interop/data-integrity-core@8.1.0': {} + '@interop/data-integrity-core@8.2.0': {} '@interop/data-integrity-proof@3.4.1': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/jsonld-signatures': 11.8.2 '@noble/hashes': 2.2.0 '@scure/base': 2.2.0 '@interop/did-io@4.0.6': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/lru-memoize': 4.0.3 '@interop/did-method-key@7.3.3': dependencies: '@digitalbazaar/x25519-key-agreement-key-2020': 3.0.1 - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/did-io': 4.0.6 '@interop/ed25519-verification-key': 8.0.2 transitivePeerDependencies: @@ -2194,7 +2194,7 @@ snapshots: '@interop/did-web-resolver@6.2.3': dependencies: '@interop/bnid': 6.0.2 - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/did-io': 4.0.6 '@interop/http-client': 1.0.4 did-context: 3.1.1 @@ -2203,7 +2203,7 @@ snapshots: '@interop/ecdsa-multikey@2.3.2': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@scure/base': 2.2.0 '@interop/ecdsa-signature@2.0.4(@interop/jsonld-signatures@11.8.2)': @@ -2217,7 +2217,7 @@ snapshots: '@interop/ed25519-signature@7.1.3(@interop/jsonld-signatures@11.8.2)': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/data-integrity-proof': 3.4.1 '@interop/ed25519-verification-key': 8.0.2 '@interop/jsonld': 9.0.3 @@ -2231,18 +2231,18 @@ snapshots: '@interop/ed25519-verification-key@8.0.2': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@noble/ed25519': 3.1.0 '@noble/hashes': 2.2.0 '@scure/base': 2.2.0 - '@interop/edv-client@17.5.0': + '@interop/edv-client@17.6.1': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/http-client': 1.0.4 '@interop/http-signature-zcap-invoke': 6.2.3 '@interop/lru-memoize': 4.0.3 - '@interop/minimal-cipher': 7.5.0 + '@interop/minimal-cipher': 7.6.1 '@noble/hashes': 2.2.0 '@scure/base': 2.2.0 canonicalize: 3.0.0 @@ -2252,7 +2252,7 @@ snapshots: '@interop/ezcap@7.3.2': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/data-integrity-proof': 3.4.1 '@interop/ecdsa-signature': 2.0.4(@interop/jsonld-signatures@11.8.2) '@interop/http-client': 1.0.4 @@ -2273,7 +2273,7 @@ snapshots: '@interop/http-signature-zcap-invoke@6.2.3': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/http-digest-header': 3.0.0 '@interop/http-signature-header': 5.0.2 pako: 2.2.0 @@ -2283,7 +2283,7 @@ snapshots: '@interop/jsonld-signatures@11.8.2': dependencies: '@digitalbazaar/security-context': 1.0.1 - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/jsonld': 9.0.3 fast-text-encoding: 1.0.6 rdf-canonize: 5.0.0 @@ -2300,9 +2300,9 @@ snapshots: dependencies: lru-cache: 11.5.2 - '@interop/minimal-cipher@7.5.0': + '@interop/minimal-cipher@7.6.1': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/ecdsa-multikey': 2.3.2 '@noble/ciphers': 2.2.0 '@noble/curves': 2.2.0 @@ -2319,7 +2319,7 @@ snapshots: '@digitalbazaar/zcap-context': 2.0.1 '@digitalcredentials/credentials-v2-context': 1.0.0 '@digitalcredentials/open-badges-context': 3.0.0 - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/did-io': 4.0.6 '@interop/did-method-key': 7.3.3 '@interop/did-web-resolver': 6.2.3 @@ -2337,9 +2337,9 @@ snapshots: transitivePeerDependencies: - react-native-get-random-values - '@interop/storage-core@0.3.3': + '@interop/storage-core@0.3.4': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/vc-bitstring-status-list@3.0.4': dependencies: @@ -2351,14 +2351,14 @@ snapshots: '@interop/vc@11.0.5': dependencies: '@digitalcredentials/credentials-v2-context': 1.0.0 - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/jsonld': 9.0.3 '@interop/jsonld-signatures': 11.8.2 credentials-context: 2.0.0 '@interop/verifier-core@3.3.1': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/data-integrity-proof': 3.4.1 '@interop/did-io': 4.0.6 '@interop/did-method-key': 7.3.3 @@ -2377,29 +2377,32 @@ snapshots: transitivePeerDependencies: - react-native-get-random-values - '@interop/was-client@0.13.3(@interop/jsonld-signatures@11.8.2)': + '@interop/was-client@0.14.5(@interop/jsonld-signatures@11.8.2)': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/ed25519-signature': 7.1.3(@interop/jsonld-signatures@11.8.2) - '@interop/edv-client': 17.5.0 + '@interop/edv-client': 17.6.1 '@interop/ezcap': 7.3.2 '@interop/http-client': 1.0.4 - '@interop/storage-core': 0.3.3 + '@interop/minimal-cipher': 7.6.1 + '@interop/storage-core': 0.3.4 + '@interop/x25519-key-agreement-key': 5.2.0 + '@scure/base': 2.2.0 transitivePeerDependencies: - '@interop/jsonld-signatures' - expo-crypto - react-native-get-random-values - '@interop/x25519-key-agreement-key@5.1.1': + '@interop/x25519-key-agreement-key@5.2.0': dependencies: - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@noble/curves': 2.2.0 '@scure/base': 2.2.0 '@interop/zcap@11.0.4': dependencies: '@digitalbazaar/zcap-context': 2.0.1 - '@interop/data-integrity-core': 8.1.0 + '@interop/data-integrity-core': 8.2.0 '@interop/jsonld-signatures': 11.8.2 '@isaacs/ttlcache@1.4.1': {} @@ -2582,30 +2585,30 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/type-utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.63.0 - eslint: 10.6.0 - ignore: 7.0.5 + eslint: 10.7.0 + ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.63.0 '@typescript-eslint/types': 8.63.0 '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.63.0 debug: 4.4.3 - eslint: 10.6.0 + eslint: 10.7.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2628,13 +2631,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.63.0(eslint@10.6.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.63.0(eslint@10.7.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.63.0 '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) debug: 4.4.3 - eslint: 10.6.0 + eslint: 10.7.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -2657,13 +2660,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.63.0(eslint@10.6.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.63.0(eslint@10.7.0)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0) '@typescript-eslint/scope-manager': 8.63.0 '@typescript-eslint/types': 8.63.0 '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) - eslint: 10.6.0 + eslint: 10.7.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2740,7 +2743,7 @@ snapshots: base64url@3.0.1: {} - baseline-browser-mapping@2.10.42: {} + baseline-browser-mapping@2.10.43: {} brace-expansion@5.0.7: dependencies: @@ -2752,10 +2755,10 @@ snapshots: browserslist@4.28.5: dependencies: - baseline-browser-mapping: 2.10.42 + baseline-browser-mapping: 2.10.43 caniuse-lite: 1.0.30001803 electron-to-chromium: 1.5.389 - node-releases: 2.0.50 + node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.5) bser@2.1.1: @@ -2908,9 +2911,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.6.0): + eslint-config-prettier@10.1.8(eslint@10.7.0): dependencies: - eslint: 10.6.0 + eslint: 10.7.0 eslint-scope@9.1.2: dependencies: @@ -2923,9 +2926,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.6.0: + eslint@10.7.0: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -3088,7 +3091,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.6: {} image-size@1.2.1: dependencies: @@ -3424,7 +3427,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.50: {} + node-releases@2.0.51: {} nullthrows@1.1.1: {} @@ -3478,7 +3481,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.9.4: {} + prettier@3.9.5: {} pretty-format@29.7.0: dependencies: @@ -3504,7 +3507,7 @@ snapshots: react-devtools-core@6.1.5: dependencies: - shell-quote: 1.9.0 + shell-quote: 1.10.0 ws: 7.5.11 transitivePeerDependencies: - bufferutil @@ -3621,7 +3624,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.9.0: {} + shell-quote@1.10.0: {} source-map-support@0.5.21: dependencies: @@ -3704,13 +3707,13 @@ snapshots: type-fest@0.7.1: {} - typescript-eslint@8.63.0(eslint@10.6.0)(typescript@5.9.3): + typescript-eslint@8.63.0(eslint@10.7.0)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) - eslint: 10.6.0 + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + eslint: 10.7.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f47085c..252f859 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,8 @@ allowBuilds: core-js: false esbuild: true minimumReleaseAgeExclude: - - '@interop/did-method-webvh@3.7.0' - - '@interop/storage-core@0.3.3' - - '@interop/was-client@0.13.3' + - '@interop/edv-client@17.6.1' + - '@interop/minimal-cipher@7.6.1' + - '@interop/storage-core@0.3.4' + - '@interop/was-client@0.14.5' + - '@interop/x25519-key-agreement-key@5.2.0' diff --git a/src/commands/key.test.ts b/src/commands/key.test.ts index 9de9f05..caf7e7a 100644 --- a/src/commands/key.test.ts +++ b/src/commands/key.test.ts @@ -3,6 +3,8 @@ import assert from 'node:assert/strict' import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { tmpdir } from 'node:os' +import { createHash } from 'node:crypto' +import { IdDecoder } from '@digitalcredentials/bnid' import { makeKeyCommand } from './key.js' import { makeDidCommand } from './did.js' @@ -341,6 +343,94 @@ describe('did key', () => { } }) + it('generates an aes256 KEK as a symmetric Multikey', async () => { + await makeKeyCommand().parseAsync(['create', '--type', 'aes256'], { + from: 'user' + }) + const parsed = JSON.parse(logs[0]) + assert.equal(parsed.type, 'Multikey') + // No public half is emitted for a symmetric KEK. + assert.equal(parsed.publicKeyMultibase, undefined) + // The secretKeyMultibase is a base58btc multibase value that decodes to + // the 2-byte AES-256 header (0xa2 0x01) followed by 32 raw key bytes. + assert.match(parsed.secretKeyMultibase, /^z/) + const decoded = Buffer.from( + new IdDecoder({ encoding: 'base58', multibase: true }).decode( + parsed.secretKeyMultibase + ) + ) + assert.equal(decoded.length, 34) + assert.deepEqual([...decoded.subarray(0, 2)], [0xa2, 0x01]) + // The id is urn:kek:sha256:. + const expectedId = `urn:kek:sha256:${createHash('sha256') + .update(decoded.subarray(2)) + .digest('hex')}` + assert.equal(parsed.id, expectedId) + }) + + it('generates a different aes256 KEK on each invocation', async () => { + await makeKeyCommand().parseAsync(['create', '--type', 'aes256'], { + from: 'user' + }) + const first = JSON.parse(logs[0]) + logs.length = 0 + await makeKeyCommand().parseAsync(['create', '--type', 'aes256'], { + from: 'user' + }) + const second = JSON.parse(logs[0]) + assert.notEqual(first.secretKeyMultibase, second.secretKeyMultibase) + assert.notEqual(first.id, second.id) + }) + + it('exits with error for aes256 --with-seed', async () => { + await makeKeyCommand().parseAsync( + ['create', '--type', 'aes256', '--with-seed'], + { from: 'user' } + ) + assert.equal(exitCode, 1) + assert.ok(errors[0].includes('--with-seed')) + }) + + it('saves an aes256 KEK and round-trips list/show without the secret', async () => { + const walletDir = await mkdtemp(join(tmpdir(), 'did-cli-test-')) + process.env.WALLET_DIR = walletDir + try { + await makeKeyCommand().parseAsync( + ['create', '--type', 'aes256', '--save', '--handle', 'record-kek'], + { from: 'user' } + ) + const created = JSON.parse(logs[0]) + const filePath = errors[0].slice('Key saved to '.length) + const filename = filePath.slice(filePath.lastIndexOf('/') + 1) + // filename format: YYYY-MM-DD-aes256-urn_kek_sha256_.json + assert.match( + filename, + /^\d{4}-\d{2}-\d{2}-aes256-urn_kek_sha256_[0-9a-f]{64}\.json$/ + ) + + // `list --json` reports the saved KEK with its aes256 type. + logs.length = 0 + await makeKeyCommand().parseAsync(['list', '--json'], { from: 'user' }) + const entries = JSON.parse(logs.join('\n')) + assert.equal(entries.length, 1) + assert.equal(entries[0].type, 'aes256') + assert.equal(entries[0].fingerprint, created.id) + + // `show` finds it by handle and never prints the secret material. + logs.length = 0 + await makeKeyCommand().parseAsync(['show', 'record-kek'], { + from: 'user' + }) + const shown = JSON.parse(logs[0]) + assert.equal(shown.id, created.id) + assert.equal(shown.type, 'Multikey') + assert.equal(shown.secretKeyMultibase, undefined) + assert.ok(!logs[0].includes(created.secretKeyMultibase)) + } finally { + await rm(walletDir, { recursive: true }) + } + }) + it('exits with error for --handle without --save', async () => { await makeKeyCommand().parseAsync(['create', '--handle', 'x'], { from: 'user' diff --git a/src/commands/key.ts b/src/commands/key.ts index a652809..1150988 100644 --- a/src/commands/key.ts +++ b/src/commands/key.ts @@ -13,10 +13,12 @@ * stub. Keys are referenced by their `publicKeyMultibase` fingerprint or by a * metadata handle. Data goes to stdout, diagnostics and errors to stderr. */ +import { createHash, randomBytes } from 'node:crypto' import { Command } from 'commander' import { decodeSecretKeySeed, - generateSecretKeySeed + generateSecretKeySeed, + IdEncoder } from '@digitalcredentials/bnid' import { Ed25519VerificationKey } from '@interop/ed25519-verification-key' import * as EcdsaMultikey from '@interop/ecdsa-multikey' @@ -52,6 +54,13 @@ import { /** The wallet collection name for stored keys. */ const COLLECTION = 'keys' +/** + * The Multikey header for an AES-256 symmetric key (`0xa2 0x01`, the multicodec + * varint for `aes-256`), prefixed before the 32 raw key bytes in a KEK's + * `secretKeyMultibase`. + */ +const AES_256_MULTIKEY_HEADER = Buffer.from([0xa2, 0x01]) + /** * Build the human-readable key type label shown in list/show output, e.g. * `ed25519` or `ecdsa-p256`, from a parsed key storage ID. @@ -95,7 +104,8 @@ function didsCell({ dids }: { dids: string[] }): string { * wallet, and prints the key (or the seed-wrapped form) to stdout. * * @param options {object} - * @param options.type {string} Key type: ed25519, ecdsa, x25519, or hmac. + * @param options.type {string} Key type: ed25519, ecdsa, x25519, hmac, or + * aes256. * @param options.curve {string} ECDSA curve, for --type ecdsa. * @param [options.save] {boolean} Save the key to wallet storage. * @param [options.handle] {string} Short tag stored in the metadata sidecar. @@ -276,10 +286,53 @@ export async function runCreate(options: { console.log(JSON.stringify(exported, null, 2)) return 0 } + case 'aes256': { + if (options.withSeed) { + console.error( + '--with-seed is not supported for aes256 keys; an AES-256 KEK is ' + + 'always generated from fresh random bytes, never derived from a ' + + 'seed.' + ) + return 1 + } + // 32 random key bytes prefixed with the AES-256 Multikey header, encoded + // as base58btc multibase -- a symmetric Multikey `secretKeyMultibase`. + const keyBytes = randomBytes(32) + const secretKeyMultibase = new IdEncoder({ + encoding: 'base58', + multibase: true + }).encode(Buffer.concat([AES_256_MULTIKEY_HEADER, keyBytes])) + // The id is derived from the RAW 32 key bytes (header excluded), matching + // the KEK id a Wallet Attached Storage server derives from the same value. + const id = `urn:kek:sha256:${createHash('sha256') + .update(keyBytes) + .digest('hex')}` + const exported = { id, type: 'Multikey', secretKeyMultibase } + if (options.save) { + const now = new Date() + const date = now.toISOString().slice(0, 10) + const storageId = `${date}-aes256-${id}`.replaceAll(':', '_') + const filePath = await saveToCollection({ + collection: 'keys', + storageId, + data: exported + }) + await writeCreateMeta({ + collection: COLLECTION, + storageId, + created: now.toISOString(), + handle: options.handle, + description: options.description + }) + console.error(`Key saved to ${filePath}`) + } + console.log(JSON.stringify(exported, null, 2)) + return 0 + } default: console.error( `Unknown key type: ${options.type}. ` + - 'Supported: ed25519, ecdsa, x25519, hmac' + 'Supported: ed25519, ecdsa, x25519, hmac, aes256' ) return 1 } @@ -450,6 +503,14 @@ export async function runShow(options: { // A symmetric HMAC key has no public half; show only its safe fields, // never the secret material. publicKey = { id: storedKey.id, type: 'Sha256HmacKey2019' } + } else if ( + (storedKey as { type?: string }).type === 'Multikey' && + (storedKey as { secretKeyMultibase?: string }).secretKeyMultibase && + !storedKey.publicKeyMultibase + ) { + // A symmetric AES-256 KEK (a Multikey with a secretKeyMultibase and no + // public half); show only its safe fields, never the secret material. + publicKey = { id: storedKey.id, type: 'Multikey' } } else if ( (storedKey as { type?: string }).type === 'X25519KeyAgreementKey2020' ) { @@ -595,7 +656,7 @@ export function makeKeyCommand(): Command { .description('Create a new key') .option( '-t, --type ', - 'key type (supported: ed25519, ecdsa, x25519, hmac)', + 'key type (supported: ed25519, ecdsa, x25519, hmac, aes256)', 'ed25519' ) .option( diff --git a/src/meta.ts b/src/meta.ts index 0db88f4..ce6a912 100644 --- a/src/meta.ts +++ b/src/meta.ts @@ -35,7 +35,7 @@ export function parseKeyStorageId({ storageId }: { storageId: string }): { curve?: string } { const match = storageId.match( - /^(\d{4}-\d{2}-\d{2})-(ed25519|ecdsa|x25519|hmac)(?:-(p\d{3}))?-/ + /^(\d{4}-\d{2}-\d{2})-(ed25519|ecdsa|x25519|hmac|aes256)(?:-(p\d{3}))?-/ ) if (!match) { return {}