node-tpm2 0.0.4-beta.4 → 0.0.5-beta.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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ const { message, signature } = await ak.quote({
18
18
  });
19
19
  ```
20
20
 
21
- **Pre-release** (`0.0.x-beta`). [Roadmap](./docs/roadmap.md) for remaining namespaces.
21
+ **Pre-release** (`0.0.x-beta`). Full public API implemented; validated on real Windows 11 + Intel TPM. [API reference](./docs/api-reference.md) · [Roadmap](./docs/roadmap.md).
22
22
 
23
23
  ---
24
24
 
@@ -194,19 +194,21 @@ More detail: [getting-started.md](./docs/getting-started.md) · [api-reference.m
194
194
  | `tpm.random.bytes(n)` | ✓ | ✓ | ✓ |
195
195
  | **pcr** | | | |
196
196
  | `tpm.pcr.read(...)` | ✓ | ✓ | ✓ |
197
- | `tpm.pcr.extend(i, digest)` | ✓ * | * | ✓ |
197
+ | `tpm.pcr.extend(i, digest)` | ✓ | `REQUIRES_ELEVATION` | ✓ |
198
198
  | **nv** | | | |
199
- | `tpm.nv.read(...)` | ✓ *planned* | ✓ *planned* | ✓ |
200
- | `tpm.nv.write(...)` | ✓ *planned* | ✓ *planned* | ✓ |
199
+ | `tpm.nv.read(...)` | ✓ | ✓ | ✓ |
200
+ | `tpm.nv.write(...)` | ✓ | ✓ | ✓ |
201
+ | `tpm.nv.define(...)` | ✓ § | ✓ § | ✓ § |
202
+ | `tpm.nv.undefine(...)` | ✓ § | ✓ § | ✓ § |
201
203
  | `tpm.attest.ekCertificate()` | ✓ | ✓ | ✓ |
202
204
  | **keys** | | | |
203
205
  | `tpm.keys.create(...)` | ✓ | ✓ | ✓ |
204
206
  | `tpm.keys.load(blob)` | ✓ | ✓ | ✓ |
205
207
  | `key.sign(digest)` | ✓ | ✓ | ✓ |
206
- | `key.decrypt(cipher)` | *planned* | *planned* | *planned* |
208
+ | `key.decrypt(cipher)` | | | |
207
209
  | **seal** | | | |
208
- | `tpm.seal(...)` | ✓ *planned* | ✓ *planned* | ✓ |
209
- | `tpm.unseal(blob)` | ✓ *planned* | ✓ *planned* | ✓ |
210
+ | `tpm.seal.seal(...)` | ✓ | ✓ | ✓ |
211
+ | `tpm.unseal(blob)` | ✓ | ✓ | ✓ |
210
212
  | **attest** | | | |
211
213
  | `tpm.attest.provisionAk()` user | ✓ | ✓ | ✓ |
212
214
  | `tpm.attest.provisionAk({ scope: 'machine' })` | — | ✗ | ✓ |
@@ -217,7 +219,15 @@ More detail: [getting-started.md](./docs/getting-started.md) · [api-reference.m
217
219
 
218
220
  **Windows fleet pattern:** provision machine AK elevated or as SYSTEM once → persist `akBlob` → standard users quote forever after. See [docs/windows-pcp.md](./docs/windows-pcp.md).
219
221
 
220
- **Planned rows** are design targets from the [roadmap](./docs/roadmap.md); unprivileged use matches the Phase 0 spike (`GetRandom`, `CreatePrimary` succeeded on Windows 11 without admin). Firmware or group policy can still deny specific PCR/NV operations — those surface as `TPM_RC` or `COMMAND_BLOCKED`, not silent failure.
222
+ **Hardware validation (beta):** Windows 11 Intel TPM attestation suite, `random`, `keys`, `pcr.read` / `pcr.extend` (elevated), `nv.read`. Linux: CI + swtpm. Firmware or group policy can still deny specific PCR/NV operations — those surface as `TPM_RC` or `REQUIRES_ELEVATION`, not silent failure.
223
+
224
+ **‡ `nv.read/write`:** Success depends on index attributes and auth. EK cert indices (`0x01c00002`, `0x01c0000A`) are read-only.
225
+
226
+ **§ `nv.define/undefine`:** Owner NV range only (`0x01800000`–`0x01BFFFFF`). Requires owner authorization (often empty password). **Consumes NV space** until undefined — use only on test machines or with a chosen index. Windows standard user → **`REQUIRES_ELEVATION`** (same TBS block as `pcr.extend`); run Admin PowerShell.
227
+
228
+ **Note:** On Windows, raw TBS may reject `nv.readPublic` for owner-range indices (`TPM_RC` ~`0xA6`) even when define/read/write succeed. Factory indices (`0x01c00002` EK cert) work. After `nv.define`, use the known size for read/write; `examples/nv-smoke.mjs` handles this.
229
+
230
+ **† `pcr.extend`:** Linux standard user (prefer indices **16–23** for experiments; avoid **0–7** boot/Secure Boot PCRs). **Windows standard user → `REQUIRES_ELEVATION`** (`TPM_E_COMMAND_BLOCKED` from TBS). Windows Administrator can extend on real hardware (validated). Standard-user failure is not `COMMAND_BLOCKED` — re-run elevated.
221
231
 
222
232
  ---
223
233
 
@@ -268,7 +278,31 @@ const reloaded = await tpm.keys.load(saved);
268
278
  await reloaded.sign(digest);
269
279
  ```
270
280
 
271
- Flat: `Tpm.createKey()`, `Tpm.signKeyBlob({ keyBlob, digest })`. RSA `decrypt` is not yet implemented.
281
+ const rsaKey = await tpm.keys.create({ type: 'rsa', sign: true, decrypt: true });
282
+ const plain = await rsaKey.decrypt(ciphertext);
283
+
284
+ Flat: `Tpm.createKey()`, `Tpm.signKeyBlob({ keyBlob, digest })`, `Tpm.decryptKeyBlob({ keyBlob, cipher })`.
285
+
286
+ ### NV
287
+
288
+ ```javascript
289
+ await tpm.nv.read('0x01c00002'); // EK cert index (read-only on most hardware)
290
+ await tpm.nv.readPublic('0x01800042'); // metadata before read/write
291
+ await tpm.nv.define({ handle: '0x01800042', size: 64 }); // owner NV — test machines only
292
+ await tpm.nv.write('0x01800042', data, 0);
293
+ await tpm.nv.undefine('0x01800042');
294
+ ```
295
+
296
+ Flat: `Tpm.nvRead`, `Tpm.nvWrite`, `Tpm.nvReadPublic`, `Tpm.nvDefine`, `Tpm.nvUndefine`. See `examples/nv-smoke.mjs`.
297
+
298
+ ### Seal
299
+
300
+ ```javascript
301
+ const sealed = await tpm.seal.seal({ data: secret, pcrSelection: [23] });
302
+ const plain = await tpm.seal.unseal(sealed);
303
+ ```
304
+
305
+ Flat: `Tpm.seal`, `Tpm.unseal`.
272
306
 
273
307
  ### Attestation
274
308
 
@@ -347,8 +381,8 @@ try {
347
381
  |------|------|:-------:|:---------:|----------------------|
348
382
  | `TPM_UNAVAILABLE` | No TPM, no native binary, macOS, or backend not built | — | — | Install platform package / check TPM |
349
383
  | `ACCESS_DENIED` | OS denied device or key access | — | sometimes | Linux: `tss` group; container: pass device |
350
- | `REQUIRES_ELEVATION` | Windows operation needs Admin/SYSTEM | — | ✓ | Re-run enrollment elevated or as SYSTEM |
351
- | `COMMAND_BLOCKED` | Windows TBS driver blocked the command ordinal | ✓ | — | Use NCrypt PCP path (e.g. activation) |
384
+ | `REQUIRES_ELEVATION` | Windows operation needs Admin/SYSTEM | — | ✓ | Re-run elevated; **`pcr.extend`** and **`nv.define`/`nv.undefine`** from standard user |
385
+ | `COMMAND_BLOCKED` | Windows TBS blocked raw ordinal (e.g. ActivateCredential) | ✓ | — | Use NCrypt PCP elevation does not help |
352
386
  | `NOT_SUPPORTED` | Feature or PCP capability missing on this platform | — | sometimes | — |
353
387
  | `INVALID_ARGUMENT` | Bad JS/Rust option (e.g. empty machine `keyName`) | — | sometimes | Fix caller input |
354
388
  | `KEY_NOT_FOUND` | NCrypt key / blob locator not found | — | ✓ | Check persisted blob / key name |
@@ -367,7 +401,9 @@ When the TPM returns a non-zero response code, the library classifies it:
367
401
  | Success | `rc === 0` | (no error) | `0` |
368
402
  | Auth | `(rc & 0x0300) === 0x0300` | `AUTH_FAILED` | `0x38E` (`TPM_RC_AUTH_FAIL`) |
369
403
  | Format | `(rc & 0xFF00) === 0x0100` or FMT1 bit set | `MARSHALLING_ERROR` | `0x125` (`TPM_RC_ASYMMETRIC`) |
370
- | Windows TBS blocked | `rc === 0x80280400` | `COMMAND_BLOCKED` | `0x80280400` |
404
+ | Windows TBS blocked | `rc === 0x80280400` | `COMMAND_BLOCKED` * | `0x80280400` |
405
+
406
+ \* **`PCR_Extend`:** mapped to **`REQUIRES_ELEVATION`** (same `hresult` `0x80280400`) — Administrator can extend on Windows client; standard user should re-run elevated.
371
407
  | Other | everything else | `TPM_RC` | vendor-specific |
372
408
 
373
409
  Auth-class and format-class detection follows TPM 2.0 response-code layout (see `src/tbs/rc.rs`). **`tpmRc` on the error is the full 32-bit value** from the TPM response header — use it for logs and TPM spec lookup.
@@ -394,16 +430,17 @@ PCP / NCrypt failures on Windows map through `classify_ncrypt` (`src/tbs/ncrypt.
394
430
 
395
431
  ---
396
432
 
397
- ## API reference (planned)
433
+ ## API reference
398
434
 
399
- Subsystem namespaces not yet on `TpmHandle`. See [docs/roadmap.md](./docs/roadmap.md) for phases and acceptance criteria.
435
+ Subsystem namespaces on `TpmHandle`. See [docs/api-reference.md](./docs/api-reference.md) for full detail.
400
436
 
401
437
  | Namespace | Methods |
402
438
  |-----------|---------|
403
439
  | `tpm.random` | `bytes(n)` ✅ |
404
- | `tpm.keys` | `create`, `load`, `KeyHandle.sign` ✅ · `decrypt` planned |
405
- | `tpm.pcr` | `extend(index, digest)` |
406
- | `tpm.seal` | `seal`, `unseal` |
440
+ | `tpm.keys` | `create`, `load`, `KeyHandle.sign`, `KeyHandle.decrypt` |
441
+ | `tpm.pcr` | `read`, `extend` |
442
+ | `tpm.nv` | `read`, `write`, `readPublic`, `define`, `undefine` ✅ |
443
+ | `tpm.seal` | `seal`, `unseal` ✅ |
407
444
 
408
445
  ---
409
446
 
package/api.js CHANGED
@@ -66,10 +66,11 @@ export class TpmError extends Error {
66
66
  }
67
67
  }
68
68
 
69
- function notSupported(feature) {
70
- return async () => {
71
- throw new TpmError('NOT_SUPPORTED', `${feature} is not implemented yet.`);
72
- };
69
+ function parseTpmHandle(handle) {
70
+ if (typeof handle === 'number') {
71
+ return `0x${handle.toString(16).padStart(8, '0')}`;
72
+ }
73
+ return handle;
73
74
  }
74
75
 
75
76
  function createKeyHandle(publicKeyDer, keyBlob) {
@@ -94,7 +95,14 @@ function createKeyHandle(publicKeyDer, keyBlob) {
94
95
  return Buffer.from(sig);
95
96
  }),
96
97
 
97
- decrypt: notSupported('key.decrypt'),
98
+ decrypt: wrapNative(async (cipher) => {
99
+ requireNative('decryptKeyBlob');
100
+ const plain = await native.decryptKeyBlob({
101
+ keyBlob,
102
+ cipher,
103
+ });
104
+ return Buffer.from(plain);
105
+ }),
98
106
  };
99
107
  }
100
108
 
@@ -164,8 +172,49 @@ function createTpmHandle() {
164
172
  },
165
173
 
166
174
  nv: {
167
- read: notSupported('tpm.nv.read'),
168
- write: notSupported('tpm.nv.write'),
175
+ readPublic: wrapNative(async (handle) => {
176
+ requireNative('nvReadPublic');
177
+ return native.nvReadPublic(parseTpmHandle(handle));
178
+ }),
179
+
180
+ read: wrapNative(async (handle, offset, size, auth) => {
181
+ requireNative('nvRead');
182
+ const buf = await native.nvRead(
183
+ parseTpmHandle(handle),
184
+ offset ?? undefined,
185
+ size ?? undefined,
186
+ auth ?? undefined,
187
+ );
188
+ return Buffer.from(buf);
189
+ }),
190
+
191
+ write: wrapNative(async (handle, data, offset, auth) => {
192
+ requireNative('nvWrite');
193
+ await native.nvWrite({
194
+ handle: parseTpmHandle(handle),
195
+ data,
196
+ offset: offset ?? undefined,
197
+ auth: auth ?? undefined,
198
+ });
199
+ }),
200
+
201
+ define: wrapNative(async (opts) => {
202
+ requireNative('nvDefine');
203
+ await native.nvDefine({
204
+ handle: parseTpmHandle(opts.handle),
205
+ size: opts.size,
206
+ auth: opts.auth ?? undefined,
207
+ ownerAuth: opts.ownerAuth ?? undefined,
208
+ });
209
+ }),
210
+
211
+ undefine: wrapNative(async (handle, ownerAuth) => {
212
+ requireNative('nvUndefine');
213
+ await native.nvUndefine({
214
+ handle: parseTpmHandle(handle),
215
+ ownerAuth: ownerAuth ?? undefined,
216
+ });
217
+ }),
169
218
  },
170
219
 
171
220
  keys: {
@@ -187,8 +236,20 @@ function createTpmHandle() {
187
236
  },
188
237
 
189
238
  seal: {
190
- seal: notSupported('tpm.seal'),
191
- unseal: notSupported('tpm.unseal'),
239
+ seal: wrapNative(async (opts) => {
240
+ requireNative('seal');
241
+ const buf = await native.seal({
242
+ data: opts.data,
243
+ pcrSelection: opts.pcrSelection,
244
+ });
245
+ return Buffer.from(buf);
246
+ }),
247
+
248
+ unseal: wrapNative(async (blob) => {
249
+ requireNative('unseal');
250
+ const plain = await native.unseal(blob);
251
+ return Buffer.from(plain);
252
+ }),
192
253
  },
193
254
 
194
255
  attest: {
@@ -352,4 +413,69 @@ export const Tpm = {
352
413
  const sig = await native.signKeyBlob(opts);
353
414
  return Buffer.from(sig);
354
415
  }),
416
+
417
+ decryptKeyBlob: wrapNative(async (opts) => {
418
+ requireNative('decryptKeyBlob');
419
+ const plain = await native.decryptKeyBlob(opts);
420
+ return Buffer.from(plain);
421
+ }),
422
+
423
+ nvRead: wrapNative(async (handle, offset, size, auth) => {
424
+ requireNative('nvRead');
425
+ const buf = await native.nvRead(
426
+ parseTpmHandle(handle),
427
+ offset ?? undefined,
428
+ size ?? undefined,
429
+ auth ?? undefined,
430
+ );
431
+ return Buffer.from(buf);
432
+ }),
433
+
434
+ nvWrite: wrapNative(async (handle, data, offset, auth) => {
435
+ requireNative('nvWrite');
436
+ await native.nvWrite({
437
+ handle: parseTpmHandle(handle),
438
+ data,
439
+ offset: offset ?? undefined,
440
+ auth: auth ?? undefined,
441
+ });
442
+ }),
443
+
444
+ nvReadPublic: wrapNative(async (handle) => {
445
+ requireNative('nvReadPublic');
446
+ return native.nvReadPublic(parseTpmHandle(handle));
447
+ }),
448
+
449
+ nvDefine: wrapNative(async (opts) => {
450
+ requireNative('nvDefine');
451
+ await native.nvDefine({
452
+ handle: parseTpmHandle(opts.handle),
453
+ size: opts.size,
454
+ auth: opts.auth ?? undefined,
455
+ ownerAuth: opts.ownerAuth ?? undefined,
456
+ });
457
+ }),
458
+
459
+ nvUndefine: wrapNative(async (handle, ownerAuth) => {
460
+ requireNative('nvUndefine');
461
+ await native.nvUndefine({
462
+ handle: parseTpmHandle(handle),
463
+ ownerAuth: ownerAuth ?? undefined,
464
+ });
465
+ }),
466
+
467
+ seal: wrapNative(async (opts) => {
468
+ requireNative('seal');
469
+ const buf = await native.seal({
470
+ data: opts.data,
471
+ pcrSelection: opts.pcrSelection,
472
+ });
473
+ return Buffer.from(buf);
474
+ }),
475
+
476
+ unseal: wrapNative(async (blob) => {
477
+ requireNative('unseal');
478
+ const plain = await native.unseal(blob);
479
+ return Buffer.from(plain);
480
+ }),
355
481
  };
@@ -27,7 +27,7 @@ import { Tpm, TpmError } from 'node-tpm2';
27
27
  11. [Error model (`TpmError`)](#error-model-tpmerror)
28
28
  12. [Platform differences](#platform-differences)
29
29
  13. [Privilege matrix](#privilege-matrix)
30
- 14. [Planned / not yet implemented](#planned--not-yet-implemented)
30
+ 14. [Deferred / not in public API](#deferred--not-in-public-api)
31
31
  15. [Symbol index](#symbol-index)
32
32
 
33
33
  ---
@@ -66,7 +66,7 @@ flowchart TB
66
66
 
67
67
  **Design rules:**
68
68
 
69
- - **General keys, PCR, random, ReadPublic, seal (planned)** use the shared TBS command path on both Linux and Windows so blobs and behavior stay aligned.
69
+ - **General keys, PCR, random, ReadPublic, NV, seal** use the shared TBS command path on both Linux and Windows so blobs and behavior stay aligned.
70
70
  - **Attestation key persistence on Windows** uses NCrypt Platform Crypto Provider (PCP) because raw TBS cannot persist cross-user identity keys reliably.
71
71
  - **Transient TPM handles** are created and flushed inside each native call. You do not manage TPM handle slots in JavaScript.
72
72
 
@@ -378,7 +378,11 @@ Returned by `Tpm.open()`. All sub-namespaces are plain objects with async method
378
378
 
379
379
  **Under the hood:** Sends `TPM2_PCR_Extend` with `authHandle = TPM_RH_NULL`, one SHA-256 digest in `TPML_DIGEST_VALUES`. The TPM updates the bank as `SHA256(old_pcr || digest)`.
380
380
 
381
- **Caveats:** Firmware or platform policy may lock specific PCRs; failures surface as `TPM_RC` or `COMMAND_BLOCKED`.
381
+ **Caveats:**
382
+
383
+ - **Linux:** Firmware may lock specific indices (often **0–7**). Prefer **16–23** for application measurements.
384
+ - **Windows standard user:** TBS returns `TPM_E_COMMAND_BLOCKED` → library maps to **`REQUIRES_ELEVATION`** (re-run Admin PowerShell). Not `COMMAND_BLOCKED`.
385
+ - **Windows Administrator:** Can extend on real client hardware (validated). Does not affect quotes that only select other PCRs (e.g. `[0,1,7]`).
382
386
 
383
387
  **Flat equivalent:** [`Tpm.pcrExtend(index, digest)`](#tpm-pcrextendindex-digest-promisevoid).
384
388
 
@@ -390,15 +394,81 @@ Returned by `Tpm.open()`. All sub-namespaces are plain objects with async method
390
394
 
391
395
  ---
392
396
 
393
- ### `tpm.nv.read(handle, offset?, size?)`
397
+ ### `tpm.nv.read(handle, offset?, size?, auth?)`
398
+
399
+ **Implemented.**
400
+
401
+ ```typescript
402
+ // handle: hex string ('0x01c00002') or number
403
+ await tpm.nv.read('0x01c00002');
404
+ await tpm.nv.read('0x01c00002', 0, 512);
405
+ await tpm.nv.read('0x01800001', 0, undefined, authBuffer);
406
+ ```
407
+
408
+ **Under the hood:** `NV_ReadPublic` → size/attribute check → `NV_Read` with owner or index auth (based on `TPMA_NV_PPREAD` / `TPMA_NV_AUTHREAD`). Optional `auth` supplies the index password for `AUTHREAD` indices.
409
+
410
+ **Safe indices on consumer hardware:**
411
+
412
+ | Index | Typical use | Writable |
413
+ |-------|-------------|----------|
414
+ | `0x01c00002`, `0x01c0000A` | EK certificate (RSA / ECC) | **Read-only** (firmware) |
415
+ | `0x01c0000B` | EK template | Read-only |
416
+ | User-defined (`0x01800001`+) | Application data | **`nv.define` then read/write** |
417
+
418
+ Prefer read-only access to well-known TCG indices. Writes fail with `TPM_RC` / `AUTH_FAILED` when the index is not writable or auth is wrong.
419
+
420
+ **Flat equivalent:** [`Tpm.nvRead(handle, offset?, size?, auth?)`](#tpm-nvread).
421
+
422
+ ---
423
+
424
+ ### `tpm.nv.readPublic(handle): Promise<{ dataSize, attributes }>`
394
425
 
395
- **NOT_SUPPORTED** throws `NOT_SUPPORTED`. Planned Phase 4. EK certificate today uses internal NV read only via `ekCertificate()`.
426
+ **Implemented.** Returns NV index metadata from `NV_ReadPublic` without reading data.
427
+
428
+ **Windows caveat:** Raw TBS often rejects this command for **owner-range** indices (`0x01800000`–`0x01BFFFFF`) with `MARSHALLING_ERROR` / `TPM_RC` ~`0xA6`, even when the index exists. Factory indices (e.g. EK cert `0x01c00002`) work. After [`tpm.nv.define`](#tpmnvdefineopts-promisevoid), use the known `size` for read/write — the library falls back to owner auth automatically.
429
+
430
+ **Flat equivalent:** [`Tpm.nvReadPublic(handle)`](#tpm-nvreadpublic).
431
+
432
+ ---
433
+
434
+ ### `tpm.nv.define(opts): Promise<void>`
435
+
436
+ **Implemented.** Creates an owner NV index (`TPM2_NV_DefineSpace`).
437
+
438
+ ```typescript
439
+ type NvDefineOptions = {
440
+ handle: string | number; // 0x01800000..0x01BFFFFF
441
+ size: number; // 1..65535 bytes
442
+ auth?: Buffer; // index password (if using AUTH* attributes)
443
+ ownerAuth?: Buffer; // owner hierarchy password (often empty)
444
+ };
445
+ ```
446
+
447
+ **Default attributes:** `OWNERREAD | OWNERWRITE | NO_DA` — read/write via owner auth on `TPM_RH_OWNER`.
448
+
449
+ **Destructive / privileged:** Consumes TPM NV space until [`tpm.nv.undefine`](#tpmnvundefinehandle-ownerauth). Refuses EK cert indices. **Not for production laptops without intent.** Windows standard user → **`REQUIRES_ELEVATION`** (re-run Admin PowerShell).
450
+
451
+ **Flat equivalent:** [`Tpm.nvDefine(opts)`](#tpm-nvdefine).
396
452
 
397
453
  ---
398
454
 
399
- ### `tpm.nv.write(handle, data, offset?)`
455
+ ### `tpm.nv.undefine(handle, ownerAuth?): Promise<void>`
456
+
457
+ **Implemented.** Deletes an owner NV index (`TPM2_NV_UndefineSpace`).
400
458
 
401
- **NOT_SUPPORTED** throws `NOT_SUPPORTED`. Planned Phase 4.
459
+ **Flat equivalent:** [`Tpm.nvUndefine(handle, ownerAuth?)`](#tpm-nvundefine).
460
+
461
+ ---
462
+
463
+ ### `tpm.nv.write(handle, data, offset?, auth?)`
464
+
465
+ **Implemented.**
466
+
467
+ **Under the hood:** `NV_ReadPublic` bounds check → `NV_Write` with owner or index auth (`TPMA_NV_PPWRITE` / `TPMA_NV_AUTHWRITE`).
468
+
469
+ **Caveats:** Most factory NV indices are read-only. User-defined indices must be created with [`tpm.nv.define`](#tpmnvdefineopts-promisevoid) first.
470
+
471
+ **Flat equivalent:** [`Tpm.nvWrite(handle, data, offset?, auth?)`](#tpm-nvwrite).
402
472
 
403
473
  ---
404
474
 
@@ -410,7 +480,7 @@ Returned by `Tpm.open()`. All sub-namespaces are plain objects with async method
410
480
  type KeyCreateOptions = {
411
481
  type: 'ecc' | 'rsa';
412
482
  sign?: boolean; // default true
413
- decrypt?: boolean; // default false; RSA only when implemented
483
+ decrypt?: boolean; // default false; RSA only
414
484
  };
415
485
  ```
416
486
 
@@ -448,7 +518,32 @@ At least one of `sign` or `decrypt` must be true. `decrypt: true` with `type: 'e
448
518
 
449
519
  ### `tpm.seal.seal(opts)` / `tpm.seal.unseal(blob)`
450
520
 
451
- **NOT_SUPPORTED** — throws `NOT_SUPPORTED`. Planned Phase 5.
521
+ **Implemented.**
522
+
523
+ ```typescript
524
+ type SealOptions = {
525
+ data: Buffer;
526
+ pcrSelection?: number[]; // SHA-256 bank; binds to current PCR values at seal time
527
+ };
528
+
529
+ const sealed = await tpm.seal.seal({ data: secret });
530
+ const plain = await tpm.seal.unseal(sealed);
531
+
532
+ // PCR-bound: unseal fails if PCR state changes
533
+ const bound = await tpm.seal.seal({ data: secret, pcrSelection: [7] });
534
+ ```
535
+
536
+ **Under the hood:**
537
+
538
+ 1. `CreatePrimary` — transient storage primary.
539
+ 2. Optional `PolicyPCR` session when `pcrSelection` is set; policy digest embedded in sealed object.
540
+ 3. `Create` — keyedhash sealed object (`fixedTPM | fixedParent | userWithAuth | noDA`).
541
+ 4. Export `SEAL` wire blob (public + private + PCR metadata).
542
+ 5. `unseal`: load + `Unseal` (with matching `PolicyPCR` when bound).
543
+
544
+ **Caveats:** PCR-bound seal requires the chosen PCRs to match at unseal time. `tpm.pcr.extend` on Windows needs elevation for many indices.
545
+
546
+ **Flat equivalents:** [`Tpm.seal(opts)`](#tpm-sealopts), [`Tpm.unseal(blob)`](#tpm-unsealblob).
452
547
 
453
548
  ---
454
549
 
@@ -460,7 +555,7 @@ At least one of `sign` or `decrypt` must be true. `decrypt: true` with `type: 'e
460
555
 
461
556
  **Use for:** Chain-of-trust to manufacturer EK cert in attestation verification.
462
557
 
463
- **Not for:** General NV index access ([planned](#tpm-nv)).
558
+ **Not for:** Attestation quotes (use `attest.provisionAk`). For general NV access use [`tpm.nv.read`](#tpmnvreadhandle-offset-size-auth).
464
559
 
465
560
  ---
466
561
 
@@ -559,7 +654,13 @@ Returned by `tpm.keys.create()` and `tpm.keys.load()`.
559
654
 
560
655
  ### `key.decrypt(cipher)`
561
656
 
562
- **NOT_SUPPORTED** throws `NOT_SUPPORTED`. RSA OAEP decrypt planned for keys created with `decrypt: true`.
657
+ **Implemented.** RSA OAEP (SHA-256) via `TPM2_RSA_Decrypt`. Key must have been created with `decrypt: true` (RSA only).
658
+
659
+ **Under the hood:** Regenerate storage primary → `Load` → `RSA_Decrypt` with explicit OAEP scheme → flush.
660
+
661
+ **Use for:** Decrypting ciphertext produced for the TPM RSA key's public half.
662
+
663
+ **Not for:** ECC keys or sign-only RSA keys (`INVALID_ARGUMENT`).
563
664
 
564
665
  ---
565
666
 
@@ -727,7 +828,7 @@ Native Rust errors serialize as `__tpm2__code|message|suggestion|tpmRc|hresult`
727
828
  | `ACCESS_DENIED` | OS denied device access (Linux permissions, container) |
728
829
  | `REQUIRES_ELEVATION` | Windows Admin/SYSTEM required (machine AK, activation) |
729
830
  | `COMMAND_BLOCKED` | Windows TBS driver blocked command ordinal |
730
- | `NOT_SUPPORTED` | Unimplemented JS stub or PCP/TBS capability gap |
831
+ | `NOT_SUPPORTED` | PCP/TBS capability gap on this platform | — | sometimes | — |
731
832
  | `INVALID_ARGUMENT` | Bad options (wrong digest size, invalid key type, empty `keyName`) |
732
833
  | `KEY_NOT_FOUND` | NCrypt persisted key missing |
733
834
  | `ALREADY_EXISTS` | NCrypt key name collision (`overwrite: false`) |
@@ -762,32 +863,32 @@ TPM response codes map by class: auth → `AUTH_FAILED`, format → `MARSHALLING
762
863
  | API | Linux user | Windows user | Windows Admin/SYSTEM |
763
864
  |-----|:----------:|:------------:|:--------------------:|
764
865
  | `Tpm.isAvailable()`, `open()`, `info()` | ✓ | ✓ | ✓ |
765
- | `tpm.random.bytes`, `tpm.pcr.read`, `tpm.pcr.extend` | ✓ | ✓ | ✓ |
766
- | `tpm.keys.create/load`, `key.sign` | ✓ | | ✓ |
866
+ | `tpm.random.bytes`, `tpm.pcr.read` | ✓ | ✓ | ✓ |
867
+ | `tpm.pcr.extend` | ✓ | → `REQUIRES_ELEVATION` | ✓ |
868
+ | `tpm.nv.read` / `tpm.nv.write` | ✓ ‡ | ✓ ‡ | ✓ |
869
+ | `tpm.nv.define` / `tpm.nv.undefine` | ✓ § | ✓ § | ✓ § |
870
+ | `tpm.keys.create/load`, `key.sign`, `key.decrypt` | ✓ | ✓ | ✓ |
871
+ | `tpm.seal.seal` / `tpm.seal.unseal` | ✓ | ✓ | ✓ |
767
872
  | `tpm.attest.provisionAk()` user scope | ✓ | ✓ | ✓ |
768
873
  | `tpm.attest.provisionAk({ scope: 'machine' })` | — | ✗ | ✓ |
769
874
  | `ak.quote` / `Tpm.quote` | ✓ | ✓ | ✓ |
770
875
  | `ak.activateCredential` | ✓ | ✗ | ✓ |
771
- | `tpm.pcr.extend` | ✓* | ✓* | ✓ |
772
- | `tpm.nv.*` (planned) | ✓* | ✓* | ✓ |
773
876
 
774
- \* Planned; firmware/policy may still deny.
877
+ **`pcr.extend`:** Linux user OK (avoid boot PCRs 0–7). Windows user blocked → **`REQUIRES_ELEVATION`**; Admin/SYSTEM OK. See [windows-pcp.md](./windows-pcp.md).
775
878
 
776
- Linux requires read/write on `/dev/tpmrm0`. Windows fleet pattern: provision machine AK once elevated standard users quote forever.
879
+ **`nv.read/write`:** Index permissions vary; EK cert indices are read-only. Writes to undefined indices fail at the TPM.
777
880
 
778
- ---
881
+ § **`nv.define/undefine`:** Owner authorization required; owner NV range only. Destructive on NV space. Windows standard user → **`REQUIRES_ELEVATION`**.
779
882
 
780
- ## Planned / not yet implemented
883
+ Linux requires read/write on `/dev/tpmrm0`. Windows fleet pattern: provision machine AK once elevated → standard users quote forever.
781
884
 
782
- These methods exist on `TpmHandle` but **throw `TpmError` with code `NOT_SUPPORTED`** at call time:
885
+ ---
783
886
 
784
- | Method | Phase | Notes |
785
- |--------|-------|-------|
786
- | `tpm.nv.read/write` | 4 | General NV; EK cert uses internal path today |
787
- | `tpm.seal.seal` / `tpm.seal.unseal` | 5 | PCR-bound sealed storage |
788
- | `key.decrypt(cipher)` | 2+ | RSA OAEP for decrypt keys |
887
+ ## Deferred / not in public API
789
888
 
790
- Roadmap detail: [roadmap.md](./roadmap.md).
889
+ | Feature | Notes |
890
+ |---------|-------|
891
+ | *(none — all planned namespaces are implemented)* | See [roadmap](./roadmap.md) for hardening / polish |
791
892
 
792
893
  ---
793
894
 
@@ -800,12 +901,16 @@ Roadmap detail: [roadmap.md](./roadmap.md).
800
901
  | Random | `tpm.random.bytes(n)` | `Tpm.randomBytes(n)` | `Buffer` |
801
902
  | PCR read | `tpm.pcr.read(sel, bank?)` | `Tpm.pcrRead(sel, bank?)` | `Record<number, string>` |
802
903
  | PCR extend | `tpm.pcr.extend(i, d)` | `Tpm.pcrExtend(i, d)` | `void` |
803
- | NV | `tpm.nv.read/write` | | **NOT_SUPPORTED** |
904
+ | NV read | `tpm.nv.read(h, off?, sz?, auth?)` | `Tpm.nvRead(...)` | `Buffer` |
905
+ | NV write | `tpm.nv.write(h, data, off?, auth?)` | `Tpm.nvWrite(...)` | `void` |
906
+ | NV readPublic | `tpm.nv.readPublic(h)` | `Tpm.nvReadPublic(h)` | `{ dataSize, attributes }` |
907
+ | NV define | `tpm.nv.define(opts)` | `Tpm.nvDefine(opts)` | `void` |
908
+ | NV undefine | `tpm.nv.undefine(h, ownerAuth?)` | `Tpm.nvUndefine(...)` | `void` |
804
909
  | Create key | `tpm.keys.create(opts)` | `Tpm.createKey(opts)` | `KeyHandle` / `{ publicKeyDer, keyBlob }` |
805
910
  | Load key | `tpm.keys.load(blob)` | — | `KeyHandle` |
806
911
  | Sign | `key.sign(digest)` | `Tpm.signKeyBlob({ keyBlob, digest })` | `Buffer` |
807
- | Decrypt | `key.decrypt(cipher)` | | **NOT_SUPPORTED** |
808
- | Seal | `tpm.seal.seal/unseal` | | **NOT_SUPPORTED** |
912
+ | Decrypt | `key.decrypt(cipher)` | `Tpm.decryptKeyBlob({ keyBlob, cipher })` | `Buffer` |
913
+ | Seal | `tpm.seal.seal/unseal` | `Tpm.seal` / `Tpm.unseal` | `Buffer` |
809
914
  | EK cert | `tpm.attest.ekCertificate()` | `Tpm.readEkCertificate()` | `Buffer \| null` |
810
915
  | Provision AK | `tpm.attest.provisionAk(opts)` | `Tpm.provisionAk(opts)` | `AkHandle` / `{ akPublicDer, akBlob }` |
811
916
  | Quote | `ak.quote(opts)` / `tpm.attest.quote(opts)` | `Tpm.quote({ akBlob, ... })` | `QuoteResult` |
@@ -886,17 +991,28 @@ All public exports from `'node-tpm2'`:
886
991
  | `Tpm.activateCredential` | function | Implemented |
887
992
  | `Tpm.createKey` | function | Implemented |
888
993
  | `Tpm.signKeyBlob` | function | Implemented |
994
+ | `Tpm.decryptKeyBlob` | function | Implemented |
995
+ | `Tpm.nvRead` | function | Implemented |
996
+ | `Tpm.nvWrite` | function | Implemented |
997
+ | `Tpm.nvReadPublic` | function | Implemented |
998
+ | `Tpm.nvDefine` | function | Implemented |
999
+ | `Tpm.nvUndefine` | function | Implemented |
1000
+ | `Tpm.seal` | function | Implemented |
1001
+ | `Tpm.unseal` | function | Implemented |
889
1002
  | `TpmHandle.info` | method | Implemented |
890
1003
  | `TpmHandle.readPublic` | method | Implemented |
891
1004
  | `TpmHandle.pcr.read` | method | Implemented |
892
1005
  | `TpmHandle.pcr.extend` | method | Implemented |
893
1006
  | `TpmHandle.random.bytes` | method | Implemented |
894
- | `TpmHandle.nv.read` | method | **NOT_SUPPORTED** |
895
- | `TpmHandle.nv.write` | method | **NOT_SUPPORTED** |
1007
+ | `TpmHandle.nv.read` | method | Implemented |
1008
+ | `TpmHandle.nv.write` | method | Implemented |
1009
+ | `TpmHandle.nv.readPublic` | method | Implemented |
1010
+ | `TpmHandle.nv.define` | method | Implemented |
1011
+ | `TpmHandle.nv.undefine` | method | Implemented |
896
1012
  | `TpmHandle.keys.create` | method | Implemented |
897
1013
  | `TpmHandle.keys.load` | method | Implemented |
898
- | `TpmHandle.seal.seal` | method | **NOT_SUPPORTED** |
899
- | `TpmHandle.seal.unseal` | method | **NOT_SUPPORTED** |
1014
+ | `TpmHandle.seal.seal` | method | Implemented |
1015
+ | `TpmHandle.seal.unseal` | method | Implemented |
900
1016
  | `TpmHandle.attest.ekCertificate` | method | Implemented |
901
1017
  | `TpmHandle.attest.provisionAk` | method | Implemented |
902
1018
  | `TpmHandle.attest.quote` | method | Implemented |
@@ -904,7 +1020,7 @@ All public exports from `'node-tpm2'`:
904
1020
  | `KeyHandle.export` | method | Implemented |
905
1021
  | `KeyHandle.publicKeyDer` | getter | Implemented |
906
1022
  | `KeyHandle.sign` | method | Implemented |
907
- | `KeyHandle.decrypt` | method | **NOT_SUPPORTED** |
1023
+ | `KeyHandle.decrypt` | method | Implemented |
908
1024
  | `AkHandle.export` | method | Implemented |
909
1025
  | `AkHandle.publicKeyDer` | getter | Implemented |
910
1026
  | `AkHandle.quote` | method | Implemented |