was-teaching-server 0.9.1

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 (295) hide show
  1. package/CHANGELOG.md +1432 -0
  2. package/LICENSE +661 -0
  3. package/README.md +305 -0
  4. package/common/css/materialize.1.0.0.min.css +13 -0
  5. package/dist/auth-header-hooks.d.ts +84 -0
  6. package/dist/auth-header-hooks.d.ts.map +1 -0
  7. package/dist/auth-header-hooks.js +136 -0
  8. package/dist/auth-header-hooks.js.map +1 -0
  9. package/dist/authorize.d.ts +45 -0
  10. package/dist/authorize.d.ts.map +1 -0
  11. package/dist/authorize.js +76 -0
  12. package/dist/authorize.js.map +1 -0
  13. package/dist/backends/filesystem.d.ts +1179 -0
  14. package/dist/backends/filesystem.d.ts.map +1 -0
  15. package/dist/backends/filesystem.js +2727 -0
  16. package/dist/backends/filesystem.js.map +1 -0
  17. package/dist/backends/postgres.d.ts +833 -0
  18. package/dist/backends/postgres.d.ts.map +1 -0
  19. package/dist/backends/postgres.js +2164 -0
  20. package/dist/backends/postgres.js.map +1 -0
  21. package/dist/backends/postgresSchema.d.ts +36 -0
  22. package/dist/backends/postgresSchema.d.ts.map +1 -0
  23. package/dist/backends/postgresSchema.js +203 -0
  24. package/dist/backends/postgresSchema.js.map +1 -0
  25. package/dist/config.default.d.ts +360 -0
  26. package/dist/config.default.d.ts.map +1 -0
  27. package/dist/config.default.js +493 -0
  28. package/dist/config.default.js.map +1 -0
  29. package/dist/corsProxy.d.ts +45 -0
  30. package/dist/corsProxy.d.ts.map +1 -0
  31. package/dist/corsProxy.js +278 -0
  32. package/dist/corsProxy.js.map +1 -0
  33. package/dist/digest.d.ts +45 -0
  34. package/dist/digest.d.ts.map +1 -0
  35. package/dist/digest.js +174 -0
  36. package/dist/digest.js.map +1 -0
  37. package/dist/errors.d.ts +654 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +960 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +21 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lib/atomicFile.d.ts +61 -0
  46. package/dist/lib/atomicFile.d.ts.map +1 -0
  47. package/dist/lib/atomicFile.js +150 -0
  48. package/dist/lib/atomicFile.js.map +1 -0
  49. package/dist/lib/backendRegistry.d.ts +59 -0
  50. package/dist/lib/backendRegistry.d.ts.map +1 -0
  51. package/dist/lib/backendRegistry.js +92 -0
  52. package/dist/lib/backendRegistry.js.map +1 -0
  53. package/dist/lib/backendUsage.d.ts +28 -0
  54. package/dist/lib/backendUsage.d.ts.map +1 -0
  55. package/dist/lib/backendUsage.js +55 -0
  56. package/dist/lib/backendUsage.js.map +1 -0
  57. package/dist/lib/backends.d.ts +142 -0
  58. package/dist/lib/backends.d.ts.map +1 -0
  59. package/dist/lib/backends.js +272 -0
  60. package/dist/lib/backends.js.map +1 -0
  61. package/dist/lib/blindedIndex.d.ts +156 -0
  62. package/dist/lib/blindedIndex.d.ts.map +1 -0
  63. package/dist/lib/blindedIndex.js +338 -0
  64. package/dist/lib/blindedIndex.js.map +1 -0
  65. package/dist/lib/collectionDescription.d.ts +33 -0
  66. package/dist/lib/collectionDescription.d.ts.map +1 -0
  67. package/dist/lib/collectionDescription.js +21 -0
  68. package/dist/lib/collectionDescription.js.map +1 -0
  69. package/dist/lib/cursor.d.ts +18 -0
  70. package/dist/lib/cursor.d.ts.map +1 -0
  71. package/dist/lib/cursor.js +50 -0
  72. package/dist/lib/cursor.js.map +1 -0
  73. package/dist/lib/edvEnvelope.d.ts +50 -0
  74. package/dist/lib/edvEnvelope.d.ts.map +1 -0
  75. package/dist/lib/edvEnvelope.js +106 -0
  76. package/dist/lib/edvEnvelope.js.map +1 -0
  77. package/dist/lib/encryption.d.ts +192 -0
  78. package/dist/lib/encryption.d.ts.map +1 -0
  79. package/dist/lib/encryption.js +383 -0
  80. package/dist/lib/encryption.js.map +1 -0
  81. package/dist/lib/etag.d.ts +33 -0
  82. package/dist/lib/etag.d.ts.map +1 -0
  83. package/dist/lib/etag.js +36 -0
  84. package/dist/lib/etag.js.map +1 -0
  85. package/dist/lib/exportManifest.d.ts +29 -0
  86. package/dist/lib/exportManifest.d.ts.map +1 -0
  87. package/dist/lib/exportManifest.js +77 -0
  88. package/dist/lib/exportManifest.js.map +1 -0
  89. package/dist/lib/importTar.d.ts +81 -0
  90. package/dist/lib/importTar.d.ts.map +1 -0
  91. package/dist/lib/importTar.js +301 -0
  92. package/dist/lib/importTar.js.map +1 -0
  93. package/dist/lib/isJson.d.ts +16 -0
  94. package/dist/lib/isJson.d.ts.map +1 -0
  95. package/dist/lib/isJson.js +17 -0
  96. package/dist/lib/isJson.js.map +1 -0
  97. package/dist/lib/keyEpoch.d.ts +38 -0
  98. package/dist/lib/keyEpoch.d.ts.map +1 -0
  99. package/dist/lib/keyEpoch.js +75 -0
  100. package/dist/lib/keyEpoch.js.map +1 -0
  101. package/dist/lib/keyedMutex.d.ts +28 -0
  102. package/dist/lib/keyedMutex.d.ts.map +1 -0
  103. package/dist/lib/keyedMutex.js +45 -0
  104. package/dist/lib/keyedMutex.js.map +1 -0
  105. package/dist/lib/kmsModule.d.ts +62 -0
  106. package/dist/lib/kmsModule.d.ts.map +1 -0
  107. package/dist/lib/kmsModule.js +327 -0
  108. package/dist/lib/kmsModule.js.map +1 -0
  109. package/dist/lib/kmsRecordCipher.d.ts +82 -0
  110. package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
  111. package/dist/lib/kmsRecordCipher.js +226 -0
  112. package/dist/lib/kmsRecordCipher.js.map +1 -0
  113. package/dist/lib/pagination.d.ts +19 -0
  114. package/dist/lib/pagination.d.ts.map +1 -0
  115. package/dist/lib/pagination.js +21 -0
  116. package/dist/lib/pagination.js.map +1 -0
  117. package/dist/lib/paths.d.ts +273 -0
  118. package/dist/lib/paths.d.ts.map +1 -0
  119. package/dist/lib/paths.js +270 -0
  120. package/dist/lib/paths.js.map +1 -0
  121. package/dist/lib/preconditions.d.ts +65 -0
  122. package/dist/lib/preconditions.d.ts.map +1 -0
  123. package/dist/lib/preconditions.js +112 -0
  124. package/dist/lib/preconditions.js.map +1 -0
  125. package/dist/lib/resourceFileName.d.ts +41 -0
  126. package/dist/lib/resourceFileName.d.ts.map +1 -0
  127. package/dist/lib/resourceFileName.js +58 -0
  128. package/dist/lib/resourceFileName.js.map +1 -0
  129. package/dist/lib/revocations.d.ts +49 -0
  130. package/dist/lib/revocations.d.ts.map +1 -0
  131. package/dist/lib/revocations.js +86 -0
  132. package/dist/lib/revocations.js.map +1 -0
  133. package/dist/lib/validateDid.d.ts +19 -0
  134. package/dist/lib/validateDid.d.ts.map +1 -0
  135. package/dist/lib/validateDid.js +35 -0
  136. package/dist/lib/validateDid.js.map +1 -0
  137. package/dist/lib/validateId.d.ts +66 -0
  138. package/dist/lib/validateId.d.ts.map +1 -0
  139. package/dist/lib/validateId.js +126 -0
  140. package/dist/lib/validateId.js.map +1 -0
  141. package/dist/plugin.d.ts +115 -0
  142. package/dist/plugin.d.ts.map +1 -0
  143. package/dist/plugin.js +134 -0
  144. package/dist/plugin.js.map +1 -0
  145. package/dist/policy.d.ts +78 -0
  146. package/dist/policy.d.ts.map +1 -0
  147. package/dist/policy.js +115 -0
  148. package/dist/policy.js.map +1 -0
  149. package/dist/provisioning.d.ts +38 -0
  150. package/dist/provisioning.d.ts.map +1 -0
  151. package/dist/provisioning.js +72 -0
  152. package/dist/provisioning.js.map +1 -0
  153. package/dist/requests/BackendRequest.d.ts +61 -0
  154. package/dist/requests/BackendRequest.d.ts.map +1 -0
  155. package/dist/requests/BackendRequest.js +152 -0
  156. package/dist/requests/BackendRequest.js.map +1 -0
  157. package/dist/requests/CollectionRequest.d.ts +234 -0
  158. package/dist/requests/CollectionRequest.d.ts.map +1 -0
  159. package/dist/requests/CollectionRequest.js +705 -0
  160. package/dist/requests/CollectionRequest.js.map +1 -0
  161. package/dist/requests/KeyRequest.d.ts +115 -0
  162. package/dist/requests/KeyRequest.d.ts.map +1 -0
  163. package/dist/requests/KeyRequest.js +468 -0
  164. package/dist/requests/KeyRequest.js.map +1 -0
  165. package/dist/requests/KeystoreRequest.d.ts +99 -0
  166. package/dist/requests/KeystoreRequest.d.ts.map +1 -0
  167. package/dist/requests/KeystoreRequest.js +247 -0
  168. package/dist/requests/KeystoreRequest.js.map +1 -0
  169. package/dist/requests/PolicyRequest.d.ts +53 -0
  170. package/dist/requests/PolicyRequest.d.ts.map +1 -0
  171. package/dist/requests/PolicyRequest.js +124 -0
  172. package/dist/requests/PolicyRequest.js.map +1 -0
  173. package/dist/requests/ResourceRequest.d.ts +134 -0
  174. package/dist/requests/ResourceRequest.d.ts.map +1 -0
  175. package/dist/requests/ResourceRequest.js +548 -0
  176. package/dist/requests/ResourceRequest.js.map +1 -0
  177. package/dist/requests/RevocationRequest.d.ts +67 -0
  178. package/dist/requests/RevocationRequest.d.ts.map +1 -0
  179. package/dist/requests/RevocationRequest.js +187 -0
  180. package/dist/requests/RevocationRequest.js.map +1 -0
  181. package/dist/requests/SpaceRequest.d.ts +204 -0
  182. package/dist/requests/SpaceRequest.d.ts.map +1 -0
  183. package/dist/requests/SpaceRequest.js +520 -0
  184. package/dist/requests/SpaceRequest.js.map +1 -0
  185. package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
  186. package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
  187. package/dist/requests/SpacesRepositoryRequest.js +159 -0
  188. package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
  189. package/dist/requests/collectionContext.d.ts +21 -0
  190. package/dist/requests/collectionContext.d.ts.map +1 -0
  191. package/dist/requests/collectionContext.js +30 -0
  192. package/dist/requests/collectionContext.js.map +1 -0
  193. package/dist/requests/controllerConsent.d.ts +58 -0
  194. package/dist/requests/controllerConsent.d.ts.map +1 -0
  195. package/dist/requests/controllerConsent.js +73 -0
  196. package/dist/requests/controllerConsent.js.map +1 -0
  197. package/dist/requests/keystoreContext.d.ts +68 -0
  198. package/dist/requests/keystoreContext.d.ts.map +1 -0
  199. package/dist/requests/keystoreContext.js +87 -0
  200. package/dist/requests/keystoreContext.js.map +1 -0
  201. package/dist/requests/resourceInput.d.ts +19 -0
  202. package/dist/requests/resourceInput.d.ts.map +1 -0
  203. package/dist/requests/resourceInput.js +102 -0
  204. package/dist/requests/resourceInput.js.map +1 -0
  205. package/dist/requests/spaceContext.d.ts +127 -0
  206. package/dist/requests/spaceContext.d.ts.map +1 -0
  207. package/dist/requests/spaceContext.js +201 -0
  208. package/dist/requests/spaceContext.js.map +1 -0
  209. package/dist/routes.d.ts +55 -0
  210. package/dist/routes.d.ts.map +1 -0
  211. package/dist/routes.js +306 -0
  212. package/dist/routes.js.map +1 -0
  213. package/dist/server.d.ts +20 -0
  214. package/dist/server.d.ts.map +1 -0
  215. package/dist/server.js +60 -0
  216. package/dist/server.js.map +1 -0
  217. package/dist/start.d.ts +7 -0
  218. package/dist/start.d.ts.map +1 -0
  219. package/dist/start.js +61 -0
  220. package/dist/start.js.map +1 -0
  221. package/dist/storage.d.ts +29 -0
  222. package/dist/storage.d.ts.map +1 -0
  223. package/dist/storage.js +47 -0
  224. package/dist/storage.js.map +1 -0
  225. package/dist/types.d.ts +915 -0
  226. package/dist/types.d.ts.map +1 -0
  227. package/dist/types.js +2 -0
  228. package/dist/types.js.map +1 -0
  229. package/dist/views/home.hbs +9 -0
  230. package/dist/views/templates/main.hbs +12 -0
  231. package/dist/zcap.d.ts +267 -0
  232. package/dist/zcap.d.ts.map +1 -0
  233. package/dist/zcap.js +478 -0
  234. package/dist/zcap.js.map +1 -0
  235. package/package.json +109 -0
  236. package/src/auth-header-hooks.ts +163 -0
  237. package/src/authorize.ts +117 -0
  238. package/src/backends/filesystem.ts +3626 -0
  239. package/src/backends/postgres.ts +3073 -0
  240. package/src/backends/postgresSchema.ts +231 -0
  241. package/src/config.default.ts +657 -0
  242. package/src/corsProxy.ts +326 -0
  243. package/src/declarations.d.ts +28 -0
  244. package/src/digest.ts +223 -0
  245. package/src/errors.ts +1122 -0
  246. package/src/index.ts +21 -0
  247. package/src/lib/atomicFile.ts +176 -0
  248. package/src/lib/backendRegistry.ts +139 -0
  249. package/src/lib/backendUsage.ts +79 -0
  250. package/src/lib/backends.ts +351 -0
  251. package/src/lib/blindedIndex.ts +466 -0
  252. package/src/lib/collectionDescription.ts +43 -0
  253. package/src/lib/cursor.ts +52 -0
  254. package/src/lib/edvEnvelope.ts +120 -0
  255. package/src/lib/encryption.ts +472 -0
  256. package/src/lib/etag.ts +40 -0
  257. package/src/lib/exportManifest.ts +115 -0
  258. package/src/lib/importTar.ts +381 -0
  259. package/src/lib/isJson.ts +18 -0
  260. package/src/lib/keyEpoch.ts +89 -0
  261. package/src/lib/keyedMutex.ts +45 -0
  262. package/src/lib/kmsModule.ts +451 -0
  263. package/src/lib/kmsRecordCipher.ts +292 -0
  264. package/src/lib/pagination.ts +22 -0
  265. package/src/lib/paths.ts +382 -0
  266. package/src/lib/preconditions.ts +145 -0
  267. package/src/lib/resourceFileName.ts +69 -0
  268. package/src/lib/revocations.ts +116 -0
  269. package/src/lib/validateDid.ts +41 -0
  270. package/src/lib/validateId.ts +148 -0
  271. package/src/plugin.ts +288 -0
  272. package/src/policy.ts +176 -0
  273. package/src/provisioning.ts +98 -0
  274. package/src/requests/BackendRequest.ts +219 -0
  275. package/src/requests/CollectionRequest.ts +899 -0
  276. package/src/requests/KeyRequest.ts +617 -0
  277. package/src/requests/KeystoreRequest.ts +325 -0
  278. package/src/requests/PolicyRequest.ts +169 -0
  279. package/src/requests/ResourceRequest.ts +649 -0
  280. package/src/requests/RevocationRequest.ts +267 -0
  281. package/src/requests/SpaceRequest.ts +686 -0
  282. package/src/requests/SpacesRepositoryRequest.ts +194 -0
  283. package/src/requests/collectionContext.ts +41 -0
  284. package/src/requests/controllerConsent.ts +108 -0
  285. package/src/requests/keystoreContext.ts +124 -0
  286. package/src/requests/resourceInput.ts +118 -0
  287. package/src/requests/spaceContext.ts +311 -0
  288. package/src/routes.ts +420 -0
  289. package/src/server.ts +69 -0
  290. package/src/start.ts +71 -0
  291. package/src/storage.ts +60 -0
  292. package/src/types.ts +1016 -0
  293. package/src/views/home.hbs +9 -0
  294. package/src/views/templates/main.hbs +12 -0
  295. package/src/zcap.ts +657 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * The single in-process WebKMS module this server hard-wires (`local-v1`),
3
+ * collecting the module's parts in one file: a pure-crypto core per key type,
4
+ * an operation dispatch table keyed by `(key type, operation type)`, and the
5
+ * generate/describe orchestration (record assembly, `publicAlias` /
6
+ * `publicAliasTemplate` expansion). The module never touches storage -- the
7
+ * request layer loads/inserts `KmsKeyRecord`s through the storage backend and
8
+ * hands this module the (secret-bearing) stored key.
9
+ *
10
+ * Custody draws the line on what is served (see the roadmap's wire contract):
11
+ * every operation here requires the custodial secret -- Ed25519 sign, X25519
12
+ * deriveSecret, HMAC sign/verify, AES-KW wrap/unwrap. Asymmetric verify needs
13
+ * only the public key and is deliberately not served (client-local instead);
14
+ * requesting it is a clean 400.
15
+ */
16
+ import {
17
+ createCipheriv,
18
+ createDecipheriv,
19
+ createHmac,
20
+ randomBytes,
21
+ timingSafeEqual
22
+ } from 'node:crypto'
23
+ import { Ed25519VerificationKey } from '@interop/ed25519-verification-key'
24
+ import { X25519KeyAgreementKey2020 } from '@interop/x25519-key-agreement-key'
25
+ import {
26
+ InvalidRequestBodyError,
27
+ UnsupportedKeyOperationError
28
+ } from '../errors.js'
29
+ import type { IDID, KmsKeyDescription, KmsStoredKey } from '../types.js'
30
+
31
+ /** The JSON-LD context each supported key type's description carries. */
32
+ const KEY_TYPE_CONTEXTS: Record<string, string> = {
33
+ Ed25519VerificationKey2020:
34
+ 'https://w3id.org/security/suites/ed25519-2020/v1',
35
+ X25519KeyAgreementKey2020: 'https://w3id.org/security/suites/x25519-2020/v1',
36
+ Sha256HmacKey2019: 'https://w3id.org/security/suites/hmac-2019/v1',
37
+ AesKeyWrappingKey2019: 'https://w3id.org/security/suites/aes-2019/v1'
38
+ }
39
+
40
+ /**
41
+ * AES Key Wrap (RFC 3394) parameters: AES-256 in key-wrap mode with the RFC's
42
+ * default initial value. The IV is the integrity check -- `decipher.final()`
43
+ * throws when the unwrapped output would not reproduce it (i.e. the wrong KEK
44
+ * or corrupted ciphertext).
45
+ */
46
+ const AES_KW_ALGORITHM = 'id-aes256-wrap'
47
+ const AES_KW_DEFAULT_IV = Buffer.alloc(8, 0xa6)
48
+
49
+ /** Decodes a base64url (no padding) operation field into bytes. */
50
+ function decodeBase64url({
51
+ value,
52
+ field,
53
+ requestName
54
+ }: {
55
+ value: unknown
56
+ field: string
57
+ requestName: string
58
+ }): Buffer {
59
+ if (typeof value !== 'string' || value.length === 0) {
60
+ throw new InvalidRequestBodyError({
61
+ requestName,
62
+ detail: `Operation "${field}" must be a non-empty base64url string.`,
63
+ pointer: `#/${field}`
64
+ })
65
+ }
66
+ return Buffer.from(value, 'base64url')
67
+ }
68
+
69
+ /**
70
+ * Expands a `publicAliasTemplate` against the key description, using the
71
+ * `url-template` package (RFC 6570): `{var}` substitutes the
72
+ * percent-encoded description field, `{+var}` substitutes it raw (a typical
73
+ * template is `{+controller}#{publicKeyMultibase}`). An unknown
74
+ * variable expands to the empty string, as in RFC 6570.
75
+ *
76
+ * @param options {object}
77
+ * @param options.template {string}
78
+ * @param options.description {KmsKeyDescription} expansion variables
79
+ * @returns {string}
80
+ */
81
+ function expandAliasTemplate({
82
+ template,
83
+ description
84
+ }: {
85
+ template: string
86
+ description: KmsKeyDescription
87
+ }): string {
88
+ const variables = description as unknown as Record<string, unknown>
89
+ return template.replace(
90
+ /\{(\+?)([A-Za-z0-9_]+)\}/g,
91
+ (_match, rawModifier: string, name: string) => {
92
+ const value = variables[name]
93
+ const expanded = typeof value === 'string' ? value : ''
94
+ return rawModifier === '+' ? expanded : encodeURIComponent(expanded)
95
+ }
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Builds the public key-description projection of a stored key: the
101
+ * allowlisted public fields plus the live keystore `controller` (never any
102
+ * secret field), with the `publicAlias` / expanded `publicAliasTemplate`
103
+ * override applied to `id`. Applied identically at generate time and on every
104
+ * subsequent read, so descriptions are stable (the client caches them).
105
+ *
106
+ * @param options {object}
107
+ * @param options.key {KmsStoredKey} the stored (secret-bearing) key
108
+ * @param options.controller {IDID} the keystore's current controller
109
+ * @returns {KmsKeyDescription}
110
+ */
111
+ export function describeKmsKey({
112
+ key,
113
+ controller
114
+ }: {
115
+ key: KmsStoredKey
116
+ controller: IDID
117
+ }): KmsKeyDescription {
118
+ const description: KmsKeyDescription = {
119
+ '@context': key['@context'],
120
+ id: key.id,
121
+ type: key.type,
122
+ ...(key.publicKeyMultibase !== undefined && {
123
+ publicKeyMultibase: key.publicKeyMultibase
124
+ }),
125
+ controller
126
+ }
127
+ if (key.publicAlias !== undefined) {
128
+ description.id = key.publicAlias
129
+ } else if (key.publicAliasTemplate !== undefined) {
130
+ description.id = expandAliasTemplate({
131
+ template: key.publicAliasTemplate,
132
+ description
133
+ })
134
+ }
135
+ return description
136
+ }
137
+
138
+ /**
139
+ * Generates key material for a supported key type and assembles the stored
140
+ * key (full serialized form, secret material included) plus its public
141
+ * description. Field names are protocol-fixed: the asymmetric pairs carry
142
+ * `publicKeyMultibase` / `privateKeyMultibase`; the symmetric keys carry a
143
+ * base64url 256-bit `secret` (the webkms per-type key generators).
144
+ *
145
+ * @param options {object}
146
+ * @param options.keyId {string} the full key URL (`<keystoreId>/keys/<localId>`)
147
+ * @param options.type {string} the webkms key type
148
+ * @param options.controller {IDID} the keystore's current controller (for
149
+ * the returned description only -- never stored on the key)
150
+ * @param [options.maxCapabilityChainLength] {number}
151
+ * @param [options.publicAlias] {string}
152
+ * @param [options.publicAliasTemplate] {string}
153
+ * @returns {Promise<{ key: KmsStoredKey, keyDescription: KmsKeyDescription }>}
154
+ */
155
+ export async function generateKmsKey({
156
+ keyId,
157
+ type,
158
+ controller,
159
+ maxCapabilityChainLength,
160
+ publicAlias,
161
+ publicAliasTemplate
162
+ }: {
163
+ keyId: string
164
+ type: string
165
+ controller: IDID
166
+ maxCapabilityChainLength?: number
167
+ publicAlias?: string
168
+ publicAliasTemplate?: string
169
+ }): Promise<{ key: KmsStoredKey; keyDescription: KmsKeyDescription }> {
170
+ // `Object.hasOwn`, not `KEY_TYPE_CONTEXTS[type] === undefined`: `type` is
171
+ // client-supplied, so a prototype-chain name (`constructor`, `toString`,
172
+ // `hasOwnProperty`, ...) would otherwise resolve to an inherited `Object`
173
+ // member and slip past the fail-closed rejection into a stored key record.
174
+ if (!Object.hasOwn(KEY_TYPE_CONTEXTS, type)) {
175
+ throw new InvalidRequestBodyError({
176
+ requestName: 'Generate Key',
177
+ detail: `Unsupported key type "${type}".`,
178
+ pointer: '#/invocationTarget/type'
179
+ })
180
+ }
181
+ const context = KEY_TYPE_CONTEXTS[type]!
182
+ const key: KmsStoredKey = { '@context': context, id: keyId, type }
183
+ if (type === 'Ed25519VerificationKey2020') {
184
+ const keyPair = await Ed25519VerificationKey.generate()
185
+ key.publicKeyMultibase = keyPair.publicKeyMultibase
186
+ key.privateKeyMultibase = keyPair.privateKeyMultibase
187
+ } else if (type === 'X25519KeyAgreementKey2020') {
188
+ const keyPair = await X25519KeyAgreementKey2020.generate()
189
+ key.publicKeyMultibase = keyPair.publicKeyMultibase
190
+ key.privateKeyMultibase = keyPair.privateKeyMultibase
191
+ } else {
192
+ // Sha256HmacKey2019 / AesKeyWrappingKey2019: a 256-bit symmetric secret.
193
+ key.secret = randomBytes(32).toString('base64url')
194
+ }
195
+ if (maxCapabilityChainLength !== undefined) {
196
+ key.maxCapabilityChainLength = maxCapabilityChainLength
197
+ }
198
+ if (publicAlias !== undefined) {
199
+ key.publicAlias = publicAlias
200
+ }
201
+ if (publicAliasTemplate !== undefined) {
202
+ key.publicAliasTemplate = publicAliasTemplate
203
+ }
204
+ return { key, keyDescription: describeKmsKey({ key, controller }) }
205
+ }
206
+
207
+ /**
208
+ * SignOperation on an Ed25519 key: signs the `verifyData` bytes with the
209
+ * custodial private key. The signature is not a secret, so serving it is the
210
+ * custody model WebKMS is built for.
211
+ */
212
+ async function ed25519Sign({
213
+ key,
214
+ operation
215
+ }: {
216
+ key: KmsStoredKey
217
+ operation: Record<string, unknown>
218
+ }): Promise<{ signatureValue: string }> {
219
+ const data = decodeBase64url({
220
+ value: operation.verifyData,
221
+ field: 'verifyData',
222
+ requestName: 'Sign Operation'
223
+ })
224
+ const keyPair = await Ed25519VerificationKey.from({ ...key })
225
+ const signature = await keyPair.signer().sign({ data })
226
+ return { signatureValue: Buffer.from(signature).toString('base64url') }
227
+ }
228
+
229
+ /**
230
+ * DeriveSecretOperation on an X25519 key: raw ECDH against the submitted peer
231
+ * public key -- no KDF is applied; the caller runs the shared secret through
232
+ * one. A peer whose `type` does not match the key's is a clean 400 (the client
233
+ * pre-checks, so only non-client callers hit this).
234
+ */
235
+ async function x25519DeriveSecret({
236
+ key,
237
+ operation
238
+ }: {
239
+ key: KmsStoredKey
240
+ operation: Record<string, unknown>
241
+ }): Promise<{ secret: string }> {
242
+ const requestName = 'Derive Secret Operation'
243
+ const publicKey = operation.publicKey as
244
+ { type?: string; publicKeyMultibase?: string } | undefined
245
+ if (typeof publicKey !== 'object' || publicKey === null) {
246
+ throw new InvalidRequestBodyError({
247
+ requestName,
248
+ detail: 'Operation "publicKey" must be a public key object.',
249
+ pointer: '#/publicKey'
250
+ })
251
+ }
252
+ if (publicKey.type !== key.type) {
253
+ throw new InvalidRequestBodyError({
254
+ requestName,
255
+ detail:
256
+ `The given public key type "${publicKey.type}" does not match the` +
257
+ ` key agreement key's type "${key.type}".`,
258
+ pointer: '#/publicKey/type'
259
+ })
260
+ }
261
+ const keyPair = await X25519KeyAgreementKey2020.from({ ...key })
262
+ let secret: Uint8Array
263
+ try {
264
+ secret = await keyPair.deriveSecret({ publicKey })
265
+ } catch {
266
+ // A malformed / wrong-header `publicKeyMultibase` is a client error.
267
+ throw new InvalidRequestBodyError({
268
+ requestName,
269
+ detail: 'Operation "publicKey.publicKeyMultibase" is not a valid key.',
270
+ pointer: '#/publicKey/publicKeyMultibase'
271
+ })
272
+ }
273
+ return { secret: Buffer.from(secret).toString('base64url') }
274
+ }
275
+
276
+ /** Computes the HMAC-SHA-256 of `verifyData` under the key's secret. */
277
+ function hmacDigest({
278
+ key,
279
+ operation,
280
+ requestName
281
+ }: {
282
+ key: KmsStoredKey
283
+ operation: Record<string, unknown>
284
+ requestName: string
285
+ }): Buffer {
286
+ const data = decodeBase64url({
287
+ value: operation.verifyData,
288
+ field: 'verifyData',
289
+ requestName
290
+ })
291
+ const secret = Buffer.from(key.secret as string, 'base64url')
292
+ return createHmac('sha256', secret).update(data).digest()
293
+ }
294
+
295
+ /** SignOperation on an HMAC key. */
296
+ async function hmacSign({
297
+ key,
298
+ operation
299
+ }: {
300
+ key: KmsStoredKey
301
+ operation: Record<string, unknown>
302
+ }): Promise<{ signatureValue: string }> {
303
+ const digest = hmacDigest({ key, operation, requestName: 'Sign Operation' })
304
+ return { signatureValue: digest.toString('base64url') }
305
+ }
306
+
307
+ /**
308
+ * VerifyOperation on an HMAC key -- the one served verify (a client cannot
309
+ * check an HMAC without the symmetric secret). A wrong-*length*
310
+ * `signatureValue` is an ordinary `verified: false`, checked before
311
+ * `timingSafeEqual` (which throws on mismatched lengths).
312
+ */
313
+ async function hmacVerify({
314
+ key,
315
+ operation
316
+ }: {
317
+ key: KmsStoredKey
318
+ operation: Record<string, unknown>
319
+ }): Promise<{ verified: boolean }> {
320
+ const requestName = 'Verify Operation'
321
+ const signature = decodeBase64url({
322
+ value: operation.signatureValue,
323
+ field: 'signatureValue',
324
+ requestName
325
+ })
326
+ const expected = hmacDigest({ key, operation, requestName })
327
+ const verified =
328
+ signature.length === expected.length && timingSafeEqual(signature, expected)
329
+ return { verified }
330
+ }
331
+
332
+ /** WrapKeyOperation on an AES-KW key encryption key. */
333
+ async function aesWrapKey({
334
+ key,
335
+ operation
336
+ }: {
337
+ key: KmsStoredKey
338
+ operation: Record<string, unknown>
339
+ }): Promise<{ wrappedKey: string }> {
340
+ const unwrapped = decodeBase64url({
341
+ value: operation.unwrappedKey,
342
+ field: 'unwrappedKey',
343
+ requestName: 'Wrap Key Operation'
344
+ })
345
+ const secret = Buffer.from(key.secret as string, 'base64url')
346
+ const cipher = createCipheriv(AES_KW_ALGORITHM, secret, AES_KW_DEFAULT_IV)
347
+ const wrapped = Buffer.concat([cipher.update(unwrapped), cipher.final()])
348
+ return { wrappedKey: wrapped.toString('base64url') }
349
+ }
350
+
351
+ /**
352
+ * UnwrapKeyOperation on an AES-KW key encryption key. A failed unwrap -- the
353
+ * RFC 3394 integrity check rejecting the ciphertext (wrong KEK, corrupted
354
+ * `wrappedKey`) -- resolves `unwrappedKey: null` rather than erroring: that is
355
+ * the client's documented contract (`Kek.unwrapKey` resolves null when the key
356
+ * does not match).
357
+ */
358
+ async function aesUnwrapKey({
359
+ key,
360
+ operation
361
+ }: {
362
+ key: KmsStoredKey
363
+ operation: Record<string, unknown>
364
+ }): Promise<{ unwrappedKey: string | null }> {
365
+ const wrapped = decodeBase64url({
366
+ value: operation.wrappedKey,
367
+ field: 'wrappedKey',
368
+ requestName: 'Unwrap Key Operation'
369
+ })
370
+ const secret = Buffer.from(key.secret as string, 'base64url')
371
+ try {
372
+ const decipher = createDecipheriv(
373
+ AES_KW_ALGORITHM,
374
+ secret,
375
+ AES_KW_DEFAULT_IV
376
+ )
377
+ const unwrapped = Buffer.concat([
378
+ decipher.update(wrapped),
379
+ decipher.final()
380
+ ])
381
+ return { unwrappedKey: unwrapped.toString('base64url') }
382
+ } catch {
383
+ return { unwrappedKey: null }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * The operation dispatch table: which operation envelope `type`s this module
389
+ * serves for each key type, and the pure-crypto implementation for each pair.
390
+ * Deliberate omissions (custody is the criterion): `VerifyOperation` on the
391
+ * asymmetric and key-agreement types (verify client-locally against the
392
+ * description's `publicKeyMultibase`), and every cross-type combination.
393
+ */
394
+ const KEY_OPERATIONS: Record<
395
+ string,
396
+ Record<
397
+ string,
398
+ (options: {
399
+ key: KmsStoredKey
400
+ operation: Record<string, unknown>
401
+ }) => Promise<object>
402
+ >
403
+ > = {
404
+ Ed25519VerificationKey2020: { SignOperation: ed25519Sign },
405
+ X25519KeyAgreementKey2020: { DeriveSecretOperation: x25519DeriveSecret },
406
+ Sha256HmacKey2019: { SignOperation: hmacSign, VerifyOperation: hmacVerify },
407
+ AesKeyWrappingKey2019: {
408
+ WrapKeyOperation: aesWrapKey,
409
+ UnwrapKeyOperation: aesUnwrapKey
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Runs a key operation envelope against a stored key, dispatching on
415
+ * `(key.type, operation.type)`. An operation this module does not serve for
416
+ * the key's type -- or does not recognize at all -- is a clean 400
417
+ * `UnsupportedKeyOperationError`.
418
+ *
419
+ * @param options {object}
420
+ * @param options.key {KmsStoredKey} the stored (secret-bearing) key
421
+ * @param options.operation {Record<string, unknown>} the validated envelope
422
+ * @returns {Promise<object>} the operation's wire result (`signatureValue` /
423
+ * `verified` / `secret` / `wrappedKey` / `unwrappedKey`)
424
+ */
425
+ export async function runKeyOperation({
426
+ key,
427
+ operation
428
+ }: {
429
+ key: KmsStoredKey
430
+ operation: Record<string, unknown>
431
+ }): Promise<object> {
432
+ const operationType = operation.type as string
433
+ // `Object.hasOwn` at both levels: `key.type` and `operationType` are both
434
+ // client-influenced, so plain-object indexing could otherwise resolve an
435
+ // inherited `Object` member (`constructor`, `toString`, ...) to a truthy
436
+ // function and bypass this fail-closed gate.
437
+ const operations = Object.hasOwn(KEY_OPERATIONS, key.type)
438
+ ? KEY_OPERATIONS[key.type]
439
+ : undefined
440
+ const runOperation =
441
+ operations && Object.hasOwn(operations, operationType)
442
+ ? operations[operationType]
443
+ : undefined
444
+ if (runOperation === undefined) {
445
+ throw new UnsupportedKeyOperationError({
446
+ operationType,
447
+ keyType: key.type
448
+ })
449
+ }
450
+ return await runOperation({ key, operation })
451
+ }