node-tpm2 0.0.4-beta.4 → 0.0.5-beta.0

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,13 @@ 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.
227
+
228
+ **† `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
229
 
222
230
  ---
223
231
 
@@ -268,7 +276,31 @@ const reloaded = await tpm.keys.load(saved);
268
276
  await reloaded.sign(digest);
269
277
  ```
270
278
 
271
- Flat: `Tpm.createKey()`, `Tpm.signKeyBlob({ keyBlob, digest })`. RSA `decrypt` is not yet implemented.
279
+ const rsaKey = await tpm.keys.create({ type: 'rsa', sign: true, decrypt: true });
280
+ const plain = await rsaKey.decrypt(ciphertext);
281
+
282
+ Flat: `Tpm.createKey()`, `Tpm.signKeyBlob({ keyBlob, digest })`, `Tpm.decryptKeyBlob({ keyBlob, cipher })`.
283
+
284
+ ### NV
285
+
286
+ ```javascript
287
+ await tpm.nv.read('0x01c00002'); // EK cert index (read-only on most hardware)
288
+ await tpm.nv.readPublic('0x01800042'); // metadata before read/write
289
+ await tpm.nv.define({ handle: '0x01800042', size: 64 }); // owner NV — test machines only
290
+ await tpm.nv.write('0x01800042', data, 0);
291
+ await tpm.nv.undefine('0x01800042');
292
+ ```
293
+
294
+ Flat: `Tpm.nvRead`, `Tpm.nvWrite`, `Tpm.nvReadPublic`, `Tpm.nvDefine`, `Tpm.nvUndefine`. See `examples/nv-smoke.mjs`.
295
+
296
+ ### Seal
297
+
298
+ ```javascript
299
+ const sealed = await tpm.seal.seal({ data: secret, pcrSelection: [23] });
300
+ const plain = await tpm.seal.unseal(sealed);
301
+ ```
302
+
303
+ Flat: `Tpm.seal`, `Tpm.unseal`.
272
304
 
273
305
  ### Attestation
274
306
 
@@ -347,8 +379,8 @@ try {
347
379
  |------|------|:-------:|:---------:|----------------------|
348
380
  | `TPM_UNAVAILABLE` | No TPM, no native binary, macOS, or backend not built | — | — | Install platform package / check TPM |
349
381
  | `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) |
382
+ | `REQUIRES_ELEVATION` | Windows operation needs Admin/SYSTEM | — | ✓ | Re-run enrollment elevated or as SYSTEM; **`pcr.extend` from standard user** |
383
+ | `COMMAND_BLOCKED` | Windows TBS blocked raw ordinal (e.g. ActivateCredential) | ✓ | — | Use NCrypt PCP elevation does not help |
352
384
  | `NOT_SUPPORTED` | Feature or PCP capability missing on this platform | — | sometimes | — |
353
385
  | `INVALID_ARGUMENT` | Bad JS/Rust option (e.g. empty machine `keyName`) | — | sometimes | Fix caller input |
354
386
  | `KEY_NOT_FOUND` | NCrypt key / blob locator not found | — | ✓ | Check persisted blob / key name |
@@ -367,7 +399,9 @@ When the TPM returns a non-zero response code, the library classifies it:
367
399
  | Success | `rc === 0` | (no error) | `0` |
368
400
  | Auth | `(rc & 0x0300) === 0x0300` | `AUTH_FAILED` | `0x38E` (`TPM_RC_AUTH_FAIL`) |
369
401
  | Format | `(rc & 0xFF00) === 0x0100` or FMT1 bit set | `MARSHALLING_ERROR` | `0x125` (`TPM_RC_ASYMMETRIC`) |
370
- | Windows TBS blocked | `rc === 0x80280400` | `COMMAND_BLOCKED` | `0x80280400` |
402
+ | Windows TBS blocked | `rc === 0x80280400` | `COMMAND_BLOCKED` * | `0x80280400` |
403
+
404
+ \* **`PCR_Extend`:** mapped to **`REQUIRES_ELEVATION`** (same `hresult` `0x80280400`) — Administrator can extend on Windows client; standard user should re-run elevated.
371
405
  | Other | everything else | `TPM_RC` | vendor-specific |
372
406
 
373
407
  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 +428,17 @@ PCP / NCrypt failures on Windows map through `classify_ncrypt` (`src/tbs/ncrypt.
394
428
 
395
429
  ---
396
430
 
397
- ## API reference (planned)
431
+ ## API reference
398
432
 
399
- Subsystem namespaces not yet on `TpmHandle`. See [docs/roadmap.md](./docs/roadmap.md) for phases and acceptance criteria.
433
+ Subsystem namespaces on `TpmHandle`. See [docs/api-reference.md](./docs/api-reference.md) for full detail.
400
434
 
401
435
  | Namespace | Methods |
402
436
  |-----------|---------|
403
437
  | `tpm.random` | `bytes(n)` ✅ |
404
- | `tpm.keys` | `create`, `load`, `KeyHandle.sign` ✅ · `decrypt` planned |
405
- | `tpm.pcr` | `extend(index, digest)` |
406
- | `tpm.seal` | `seal`, `unseal` |
438
+ | `tpm.keys` | `create`, `load`, `KeyHandle.sign`, `KeyHandle.decrypt` |
439
+ | `tpm.pcr` | `read`, `extend` |
440
+ | `tpm.nv` | `read`, `write`, `readPublic`, `define`, `undefine` ✅ |
441
+ | `tpm.seal` | `seal`, `unseal` ✅ |
407
442
 
408
443
  ---
409
444
 
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,79 @@ 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 }>`
425
+
426
+ **Implemented.** Returns NV index metadata from `NV_ReadPublic` without reading data.
427
+
428
+ **Flat equivalent:** [`Tpm.nvReadPublic(handle)`](#tpm-nvreadpublic).
429
+
430
+ ---
431
+
432
+ ### `tpm.nv.define(opts): Promise<void>`
433
+
434
+ **Implemented.** Creates an owner NV index (`TPM2_NV_DefineSpace`).
435
+
436
+ ```typescript
437
+ type NvDefineOptions = {
438
+ handle: string | number; // 0x01800000..0x01BFFFFF
439
+ size: number; // 1..65535 bytes
440
+ auth?: Buffer; // index password (if using AUTH* attributes)
441
+ ownerAuth?: Buffer; // owner hierarchy password (often empty)
442
+ };
443
+ ```
444
+
445
+ **Default attributes:** `OWNERREAD | OWNERWRITE | NO_DA` — read/write via owner auth on `TPM_RH_OWNER`.
446
+
447
+ **Destructive / privileged:** Consumes TPM NV space until [`tpm.nv.undefine`](#tpmnvundefinehandle-ownerauth). Refuses EK cert indices. **Not for production laptops without intent.**
448
+
449
+ **Flat equivalent:** [`Tpm.nvDefine(opts)`](#tpm-nvdefine).
450
+
451
+ ---
452
+
453
+ ### `tpm.nv.undefine(handle, ownerAuth?): Promise<void>`
454
+
455
+ **Implemented.** Deletes an owner NV index (`TPM2_NV_UndefineSpace`).
394
456
 
395
- **NOT_SUPPORTED** throws `NOT_SUPPORTED`. Planned Phase 4. EK certificate today uses internal NV read only via `ekCertificate()`.
457
+ **Flat equivalent:** [`Tpm.nvUndefine(handle, ownerAuth?)`](#tpm-nvundefine).
396
458
 
397
459
  ---
398
460
 
399
- ### `tpm.nv.write(handle, data, offset?)`
461
+ ### `tpm.nv.write(handle, data, offset?, auth?)`
462
+
463
+ **Implemented.**
464
+
465
+ **Under the hood:** `NV_ReadPublic` bounds check → `NV_Write` with owner or index auth (`TPMA_NV_PPWRITE` / `TPMA_NV_AUTHWRITE`).
400
466
 
401
- **NOT_SUPPORTED** throws `NOT_SUPPORTED`. Planned Phase 4.
467
+ **Caveats:** Most factory NV indices are read-only. User-defined indices must be created with [`tpm.nv.define`](#tpmnvdefineopts-promisevoid) first.
468
+
469
+ **Flat equivalent:** [`Tpm.nvWrite(handle, data, offset?, auth?)`](#tpm-nvwrite).
402
470
 
403
471
  ---
404
472
 
@@ -410,7 +478,7 @@ Returned by `Tpm.open()`. All sub-namespaces are plain objects with async method
410
478
  type KeyCreateOptions = {
411
479
  type: 'ecc' | 'rsa';
412
480
  sign?: boolean; // default true
413
- decrypt?: boolean; // default false; RSA only when implemented
481
+ decrypt?: boolean; // default false; RSA only
414
482
  };
415
483
  ```
416
484
 
@@ -448,7 +516,32 @@ At least one of `sign` or `decrypt` must be true. `decrypt: true` with `type: 'e
448
516
 
449
517
  ### `tpm.seal.seal(opts)` / `tpm.seal.unseal(blob)`
450
518
 
451
- **NOT_SUPPORTED** — throws `NOT_SUPPORTED`. Planned Phase 5.
519
+ **Implemented.**
520
+
521
+ ```typescript
522
+ type SealOptions = {
523
+ data: Buffer;
524
+ pcrSelection?: number[]; // SHA-256 bank; binds to current PCR values at seal time
525
+ };
526
+
527
+ const sealed = await tpm.seal.seal({ data: secret });
528
+ const plain = await tpm.seal.unseal(sealed);
529
+
530
+ // PCR-bound: unseal fails if PCR state changes
531
+ const bound = await tpm.seal.seal({ data: secret, pcrSelection: [7] });
532
+ ```
533
+
534
+ **Under the hood:**
535
+
536
+ 1. `CreatePrimary` — transient storage primary.
537
+ 2. Optional `PolicyPCR` session when `pcrSelection` is set; policy digest embedded in sealed object.
538
+ 3. `Create` — keyedhash sealed object (`fixedTPM | fixedParent | userWithAuth | noDA`).
539
+ 4. Export `SEAL` wire blob (public + private + PCR metadata).
540
+ 5. `unseal`: load + `Unseal` (with matching `PolicyPCR` when bound).
541
+
542
+ **Caveats:** PCR-bound seal requires the chosen PCRs to match at unseal time. `tpm.pcr.extend` on Windows needs elevation for many indices.
543
+
544
+ **Flat equivalents:** [`Tpm.seal(opts)`](#tpm-sealopts), [`Tpm.unseal(blob)`](#tpm-unsealblob).
452
545
 
453
546
  ---
454
547
 
@@ -460,7 +553,7 @@ At least one of `sign` or `decrypt` must be true. `decrypt: true` with `type: 'e
460
553
 
461
554
  **Use for:** Chain-of-trust to manufacturer EK cert in attestation verification.
462
555
 
463
- **Not for:** General NV index access ([planned](#tpm-nv)).
556
+ **Not for:** Attestation quotes (use `attest.provisionAk`). For general NV access use [`tpm.nv.read`](#tpmnvreadhandle-offset-size-auth).
464
557
 
465
558
  ---
466
559
 
@@ -559,7 +652,13 @@ Returned by `tpm.keys.create()` and `tpm.keys.load()`.
559
652
 
560
653
  ### `key.decrypt(cipher)`
561
654
 
562
- **NOT_SUPPORTED** throws `NOT_SUPPORTED`. RSA OAEP decrypt planned for keys created with `decrypt: true`.
655
+ **Implemented.** RSA OAEP (SHA-256) via `TPM2_RSA_Decrypt`. Key must have been created with `decrypt: true` (RSA only).
656
+
657
+ **Under the hood:** Regenerate storage primary → `Load` → `RSA_Decrypt` with explicit OAEP scheme → flush.
658
+
659
+ **Use for:** Decrypting ciphertext produced for the TPM RSA key's public half.
660
+
661
+ **Not for:** ECC keys or sign-only RSA keys (`INVALID_ARGUMENT`).
563
662
 
564
663
  ---
565
664
 
@@ -727,7 +826,7 @@ Native Rust errors serialize as `__tpm2__code|message|suggestion|tpmRc|hresult`
727
826
  | `ACCESS_DENIED` | OS denied device access (Linux permissions, container) |
728
827
  | `REQUIRES_ELEVATION` | Windows Admin/SYSTEM required (machine AK, activation) |
729
828
  | `COMMAND_BLOCKED` | Windows TBS driver blocked command ordinal |
730
- | `NOT_SUPPORTED` | Unimplemented JS stub or PCP/TBS capability gap |
829
+ | `NOT_SUPPORTED` | PCP/TBS capability gap on this platform | — | sometimes | — |
731
830
  | `INVALID_ARGUMENT` | Bad options (wrong digest size, invalid key type, empty `keyName`) |
732
831
  | `KEY_NOT_FOUND` | NCrypt persisted key missing |
733
832
  | `ALREADY_EXISTS` | NCrypt key name collision (`overwrite: false`) |
@@ -762,32 +861,32 @@ TPM response codes map by class: auth → `AUTH_FAILED`, format → `MARSHALLING
762
861
  | API | Linux user | Windows user | Windows Admin/SYSTEM |
763
862
  |-----|:----------:|:------------:|:--------------------:|
764
863
  | `Tpm.isAvailable()`, `open()`, `info()` | ✓ | ✓ | ✓ |
765
- | `tpm.random.bytes`, `tpm.pcr.read`, `tpm.pcr.extend` | ✓ | ✓ | ✓ |
766
- | `tpm.keys.create/load`, `key.sign` | ✓ | | ✓ |
864
+ | `tpm.random.bytes`, `tpm.pcr.read` | ✓ | ✓ | ✓ |
865
+ | `tpm.pcr.extend` | ✓ | → `REQUIRES_ELEVATION` | ✓ |
866
+ | `tpm.nv.read` / `tpm.nv.write` | ✓ ‡ | ✓ ‡ | ✓ |
867
+ | `tpm.nv.define` / `tpm.nv.undefine` | ✓ § | ✓ § | ✓ § |
868
+ | `tpm.keys.create/load`, `key.sign`, `key.decrypt` | ✓ | ✓ | ✓ |
869
+ | `tpm.seal.seal` / `tpm.seal.unseal` | ✓ | ✓ | ✓ |
767
870
  | `tpm.attest.provisionAk()` user scope | ✓ | ✓ | ✓ |
768
871
  | `tpm.attest.provisionAk({ scope: 'machine' })` | — | ✗ | ✓ |
769
872
  | `ak.quote` / `Tpm.quote` | ✓ | ✓ | ✓ |
770
873
  | `ak.activateCredential` | ✓ | ✗ | ✓ |
771
- | `tpm.pcr.extend` | ✓* | ✓* | ✓ |
772
- | `tpm.nv.*` (planned) | ✓* | ✓* | ✓ |
773
874
 
774
- \* Planned; firmware/policy may still deny.
875
+ **`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
876
 
776
- Linux requires read/write on `/dev/tpmrm0`. Windows fleet pattern: provision machine AK once elevated standard users quote forever.
877
+ **`nv.read/write`:** Index permissions vary; EK cert indices are read-only. Writes to undefined indices fail at the TPM.
777
878
 
778
- ---
879
+ § **`nv.define/undefine`:** Owner authorization required; owner NV range only. Destructive on NV space.
779
880
 
780
- ## Planned / not yet implemented
881
+ Linux requires read/write on `/dev/tpmrm0`. Windows fleet pattern: provision machine AK once elevated → standard users quote forever.
781
882
 
782
- These methods exist on `TpmHandle` but **throw `TpmError` with code `NOT_SUPPORTED`** at call time:
883
+ ---
783
884
 
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 |
885
+ ## Deferred / not in public API
789
886
 
790
- Roadmap detail: [roadmap.md](./roadmap.md).
887
+ | Feature | Notes |
888
+ |---------|-------|
889
+ | *(none — all planned namespaces are implemented)* | See [roadmap](./roadmap.md) for hardening / polish |
791
890
 
792
891
  ---
793
892
 
@@ -800,12 +899,16 @@ Roadmap detail: [roadmap.md](./roadmap.md).
800
899
  | Random | `tpm.random.bytes(n)` | `Tpm.randomBytes(n)` | `Buffer` |
801
900
  | PCR read | `tpm.pcr.read(sel, bank?)` | `Tpm.pcrRead(sel, bank?)` | `Record<number, string>` |
802
901
  | PCR extend | `tpm.pcr.extend(i, d)` | `Tpm.pcrExtend(i, d)` | `void` |
803
- | NV | `tpm.nv.read/write` | | **NOT_SUPPORTED** |
902
+ | NV read | `tpm.nv.read(h, off?, sz?, auth?)` | `Tpm.nvRead(...)` | `Buffer` |
903
+ | NV write | `tpm.nv.write(h, data, off?, auth?)` | `Tpm.nvWrite(...)` | `void` |
904
+ | NV readPublic | `tpm.nv.readPublic(h)` | `Tpm.nvReadPublic(h)` | `{ dataSize, attributes }` |
905
+ | NV define | `tpm.nv.define(opts)` | `Tpm.nvDefine(opts)` | `void` |
906
+ | NV undefine | `tpm.nv.undefine(h, ownerAuth?)` | `Tpm.nvUndefine(...)` | `void` |
804
907
  | Create key | `tpm.keys.create(opts)` | `Tpm.createKey(opts)` | `KeyHandle` / `{ publicKeyDer, keyBlob }` |
805
908
  | Load key | `tpm.keys.load(blob)` | — | `KeyHandle` |
806
909
  | Sign | `key.sign(digest)` | `Tpm.signKeyBlob({ keyBlob, digest })` | `Buffer` |
807
- | Decrypt | `key.decrypt(cipher)` | | **NOT_SUPPORTED** |
808
- | Seal | `tpm.seal.seal/unseal` | | **NOT_SUPPORTED** |
910
+ | Decrypt | `key.decrypt(cipher)` | `Tpm.decryptKeyBlob({ keyBlob, cipher })` | `Buffer` |
911
+ | Seal | `tpm.seal.seal/unseal` | `Tpm.seal` / `Tpm.unseal` | `Buffer` |
809
912
  | EK cert | `tpm.attest.ekCertificate()` | `Tpm.readEkCertificate()` | `Buffer \| null` |
810
913
  | Provision AK | `tpm.attest.provisionAk(opts)` | `Tpm.provisionAk(opts)` | `AkHandle` / `{ akPublicDer, akBlob }` |
811
914
  | Quote | `ak.quote(opts)` / `tpm.attest.quote(opts)` | `Tpm.quote({ akBlob, ... })` | `QuoteResult` |
@@ -886,17 +989,28 @@ All public exports from `'node-tpm2'`:
886
989
  | `Tpm.activateCredential` | function | Implemented |
887
990
  | `Tpm.createKey` | function | Implemented |
888
991
  | `Tpm.signKeyBlob` | function | Implemented |
992
+ | `Tpm.decryptKeyBlob` | function | Implemented |
993
+ | `Tpm.nvRead` | function | Implemented |
994
+ | `Tpm.nvWrite` | function | Implemented |
995
+ | `Tpm.nvReadPublic` | function | Implemented |
996
+ | `Tpm.nvDefine` | function | Implemented |
997
+ | `Tpm.nvUndefine` | function | Implemented |
998
+ | `Tpm.seal` | function | Implemented |
999
+ | `Tpm.unseal` | function | Implemented |
889
1000
  | `TpmHandle.info` | method | Implemented |
890
1001
  | `TpmHandle.readPublic` | method | Implemented |
891
1002
  | `TpmHandle.pcr.read` | method | Implemented |
892
1003
  | `TpmHandle.pcr.extend` | method | Implemented |
893
1004
  | `TpmHandle.random.bytes` | method | Implemented |
894
- | `TpmHandle.nv.read` | method | **NOT_SUPPORTED** |
895
- | `TpmHandle.nv.write` | method | **NOT_SUPPORTED** |
1005
+ | `TpmHandle.nv.read` | method | Implemented |
1006
+ | `TpmHandle.nv.write` | method | Implemented |
1007
+ | `TpmHandle.nv.readPublic` | method | Implemented |
1008
+ | `TpmHandle.nv.define` | method | Implemented |
1009
+ | `TpmHandle.nv.undefine` | method | Implemented |
896
1010
  | `TpmHandle.keys.create` | method | Implemented |
897
1011
  | `TpmHandle.keys.load` | method | Implemented |
898
- | `TpmHandle.seal.seal` | method | **NOT_SUPPORTED** |
899
- | `TpmHandle.seal.unseal` | method | **NOT_SUPPORTED** |
1012
+ | `TpmHandle.seal.seal` | method | Implemented |
1013
+ | `TpmHandle.seal.unseal` | method | Implemented |
900
1014
  | `TpmHandle.attest.ekCertificate` | method | Implemented |
901
1015
  | `TpmHandle.attest.provisionAk` | method | Implemented |
902
1016
  | `TpmHandle.attest.quote` | method | Implemented |
@@ -904,7 +1018,7 @@ All public exports from `'node-tpm2'`:
904
1018
  | `KeyHandle.export` | method | Implemented |
905
1019
  | `KeyHandle.publicKeyDer` | getter | Implemented |
906
1020
  | `KeyHandle.sign` | method | Implemented |
907
- | `KeyHandle.decrypt` | method | **NOT_SUPPORTED** |
1021
+ | `KeyHandle.decrypt` | method | Implemented |
908
1022
  | `AkHandle.export` | method | Implemented |
909
1023
  | `AkHandle.publicKeyDer` | getter | Implemented |
910
1024
  | `AkHandle.quote` | method | Implemented |