Files
suaveplan/openspec/changes/archive/2026-05-03-crypto-completion/specs/crypto-keys/spec.md
2026-05-27 23:29:53 +01:00

6.7 KiB

Capability Spec Delta: crypto-keys (crypto-completion)

Package: @suaveplan/crypto-keys Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time.

ADDED Requirements

Requirement: Device key creation

The package SHALL expose createDeviceKey(options?) from keypair/device/device.ts that generates a new DeviceKey carrying BaseKeySchema metadata (s secret bytes, v version, m master version, u user id, d depth, o key offset). Device keys SHALL always have o === 7. Generated keys SHALL pass isValidDeviceKey; an internally-failed validation SHALL throw SuaveplanError with code CRYPTO_KEYS.DEVICE_INVALID_KEY.

Scenario: round-trip valid key

  • WHEN createDeviceKey() is called with no options
  • THEN the result satisfies isValidDeviceKey(key) === true AND key.o === 7 AND key.s is a 32-byte Uint8Array

Scenario: caller-supplied metadata respected

  • WHEN createDeviceKey({ version: 3, masterVersion: 2, userId: 42, depth: 4 }) is called
  • THEN the result has key.v === 3, key.m === 2, key.u === 42, key.d === 4, key.o === 7

Verified by: packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"createDeviceKey produces a valid device key with offset 7" AND packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"createDeviceKey honors version / masterVersion / userId / depth options"


Requirement: Device key derivation from master

The package SHALL expose deriveDeviceKeyFromMaster(options) that derives a per-device DeviceKey from a MasterKey plus a numeric deviceId. Derivation SHALL use the existing generateDerivedKey (from crypto-keys/keypair/derivation) with key offset 7 and a context derived from the device id. The same (masterKey, deviceId) SHALL always produce the same DeviceKey.s byte sequence. Invalid master keys SHALL throw CRYPTO_KEYS.DEVICE_INVALID_MASTER.

Scenario: deterministic derivation

  • WHEN deriveDeviceKeyFromMaster({ masterKey, deviceId: 1 }) is called twice
  • THEN both results have byte-equal .s fields

Scenario: different device ids produce different keys

  • WHEN deriveDeviceKeyFromMaster({ masterKey, deviceId: 1 }) and deriveDeviceKeyFromMaster({ masterKey, deviceId: 2 }) are called
  • THEN the two results have different .s fields

Scenario: invalid master key rejected

  • WHEN deriveDeviceKeyFromMaster({ masterKey: { s: shortBytes } as MasterKey, deviceId: 1 }) is called with a master key that fails validateMasterKey
  • THEN it throws SuaveplanError with code CRYPTO_KEYS.DEVICE_INVALID_MASTER

Verified by: packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"deriveDeviceKeyFromMaster is deterministic for fixed master and deviceId" AND packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"different deviceIds derive different keys" AND packages/crypto/crypto-keys/src/keypair/device/device.spec-conformance.test.ts::"throws DEVICE_INVALID_MASTER for an invalid master key"


Requirement: Device UUID generation

The package SHALL expose generateDeviceUUID(options) returning an RFC 4122-compliant UUID v4 string deterministically derived from a DeviceKey. The version nibble (bits 12-15 of time_hi_and_version) SHALL be set to 4; the variant bits (bits 6-7 of clock_seq_hi) SHALL be set to 10. The same DeviceKey plus context SHALL always produce the same UUID.

Scenario: UUID structure conforms to v4

  • WHEN generateDeviceUUID({ deviceMasterKey }) is called
  • THEN the result matches /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/

Scenario: deterministic for fixed inputs

  • WHEN generateDeviceUUID(opts) is called twice with the same opts
  • THEN both calls return the same string

Verified by: packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"generateDeviceUUID returns an RFC 4122 v4 UUID" AND packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"generateDeviceUUID is deterministic for fixed inputs"


Requirement: Device metadata extraction and comparison

The package SHALL expose extractDeviceMetadata(deviceKey) returning DeviceMetadata (version, masterVersion, userId, depth, keyOffset) and compareDeviceKeys(a, b) returning true only when the two DeviceKey instances have identical s, v, m, u, d, o fields. The s-byte comparison SHALL be constant-time. The package SHALL also expose isValidDeviceKey(deviceKey) returning true only for keys passing structural validation (correct field types and o === 7).

Scenario: extractDeviceMetadata round-trips

  • WHEN extractDeviceMetadata(createDeviceKey({ version: 5, depth: 3 })) is called
  • THEN the result has version === 5 and depth === 3

Scenario: compareDeviceKeys equals on identical keys

  • WHEN compareDeviceKeys(k, k) is called for any DeviceKey k
  • THEN the result is true

Scenario: compareDeviceKeys differs on differing s

  • WHEN compareDeviceKeys(k1, k2) is called for keys with different .s bytes
  • THEN the result is false

Scenario: compareDeviceKeys is constant-time on s

  • WHEN the .s bytes differ at byte 0 vs byte 31 (last byte)
  • THEN the timing distribution observed across many invocations does not reveal which byte differs (statistical timing test)

Verified by: packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"extractDeviceMetadata round-trips a created device key" AND packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"compareDeviceKeys is true for identical keys" AND packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"compareDeviceKeys is false for keys with differing s" AND packages/crypto/crypto-keys/src/keypair/device/device.spec-conformance.test.ts::"compareDeviceKeys runs in constant time over s"


Requirement: generateDerivedDeviceId back-compat alias

The package SHALL expose generateDerivedDeviceId(options) as a thin alias for generateDeviceUUID(options), retained for upstream-API back-compat. The alias SHALL NOT use @deprecated JSDoc (banned in genesis per CLAUDE.md §2). Its sibling .md SHALL document it with prose: "Backwards-compatibility alias for generateDeviceUUID."

Scenario: alias produces identical output

  • WHEN generateDerivedDeviceId(opts) and generateDeviceUUID(opts) are called with the same options
  • THEN both return the same string

Verified by: packages/crypto/crypto-keys/src/keypair/device/device.test.ts::"generateDerivedDeviceId returns the same UUID as generateDeviceUUID"