Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cachekit-core-ts/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ export declare class TenantKeys {
* current key only, turning every pre-rotation entry into a miss.
*/
keyringEntryCount(): number
/**
* Whether cachekit-core detected AES hardware support on this host.
*
* Forwards `ZeroKnowledgeEncryptor::hardware_acceleration_enabled()`.
* Informational only — `ring` dispatches independently of it.
*/
hardwareAccelerationEnabled(): boolean
}

/**
Expand Down
9 changes: 9 additions & 0 deletions packages/cachekit-core-ts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ impl TenantKeys {
pub fn keyring_entry_count(&self) -> u32 {
self.keyring_entries
}

/// Whether cachekit-core detected AES hardware support on this host.
///
/// Forwards `ZeroKnowledgeEncryptor::hardware_acceleration_enabled()`.
/// Informational only — `ring` dispatches independently of it.
#[napi]
pub fn hardware_acceleration_enabled(&self) -> bool {
self.encryptor.hardware_acceleration_enabled()
}
}

/// Derive per-tenant keys using HKDF-SHA256.
Expand Down
2 changes: 2 additions & 0 deletions packages/cachekit-core-wasm/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export declare class TenantKeys {
* keys) — SDK attestation that rotation config survived the boundary.
*/
keyringEntryCount(): number;
/** cachekit-core's AES hardware detection — `false` on wasm32; matches the NAPI binding. */
hardwareAccelerationEnabled(): boolean;
}

/** Derive a 32-byte domain key using HKDF-SHA256 (RFC 5869). */
Expand Down
7 changes: 7 additions & 0 deletions packages/cachekit-core-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ impl TenantKeys {
pub fn keyring_entry_count(&self) -> u32 {
self.keyring_entries
}

/// Forwards cachekit-core's AES hardware detection — `false` on wasm32
/// (no AES instructions); present so the handle matches the NAPI binding.
#[wasm_bindgen(js_name = hardwareAccelerationEnabled)]
pub fn hardware_acceleration_enabled(&self) -> bool {
self.encryptor.hardware_acceleration_enabled()
}
}

/// Derive per-tenant keys using HKDF-SHA256.
Expand Down
21 changes: 21 additions & 0 deletions packages/cachekit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,27 @@ With the CachekitIO backend, the `X-CacheKit-L1-*` telemetry headers are wired
automatically from the cache's live L1/L2 hit and miss counters; pass your own
`metricsProvider` in the backend config to override.

**Is AES hardware-accelerated on this host?** `isHardwareAccelerated()` on the
encryption manager forwards cachekit-core's detection. Informational only: the
crypto backend picks its implementation independently, so use it to explain
`.secure` latency, not to change behaviour. It initialises the bindings if
needed, and returns `null` (unknown) only when the installed binding
predates the accessor. The per-architecture semantics are core's — as of
cachekit-core 0.6 a runtime AES-NI probe on x86/x86_64, `true` on every aarch64
build (a NEON check, not the Crypto Extension), and `false` on Cloudflare
Workers (wasm32 has no AES instructions).

```typescript
import { EncryptionManager } from '@cachekit-io/cachekit';

const manager = new EncryptionManager(process.env.CACHEKIT_MASTER_KEY!, 'tenant-123');
try {
console.log('AES hardware acceleration:', await manager.isHardwareAccelerated());
} finally {
manager.dispose();
}
```

## Cloudflare Workers

The SDK ships a Workers-native entrypoint: `@cachekit-io/cachekit/workers`
Expand Down
41 changes: 41 additions & 0 deletions packages/cachekit/src/encryption/manager-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function mockBindings(overrides?: Partial<EncryptionBindings>) {
encryptionFingerprint: () => new Uint8Array(16),
getNonceCounter: () => 0,
keyringEntryCount: () => 1 + (previousMasterKeys?.length ?? 0),
hardwareAccelerationEnabled: () => true,
free() {
freed.push(keys);
},
Expand Down Expand Up @@ -247,4 +248,44 @@ describe('EncryptionManagerCore keyring config (previousMasterKeys)', () => {
expect(freed.length).toBe(1);
manager.dispose();
});

it('reports hardware acceleration from the binding, initialising on demand', async () => {
const { bindings, derived } = mockBindings();
const manager = new TestManager(async () => bindings);

// Answers at startup, before any encrypt — and derives exactly once.
expect(await manager.isHardwareAccelerated()).toBe(true);
expect(derived.length).toBe(1);
manager.dispose();
});

it('reports null (unknown), not false, when the binding predates the accessor', async () => {
const { bindings } = mockBindings();
vi.mocked(bindings.deriveTenantKeys).mockImplementation(
(_masterKey: Uint8Array, tenantId: string) => ({
tenantId,
encryptionFingerprint: () => new Uint8Array(16),
getNonceCounter: () => 0,
// no hardwareAccelerationEnabled — older binding
})
);
const manager = new TestManager(async () => bindings);

expect(await manager.isHardwareAccelerated()).toBeNull();
manager.dispose();
});

it('rejects with EncryptionError, not TypeError, when dispose races an initialised read', async () => {
const { bindings } = mockBindings();
const manager = new TestManager(async () => bindings);

// Initialise first, so ensureInitialized() takes its early-return path and
// the read below resumes only after dispose() has nulled tenantKeys.
expect(await manager.isHardwareAccelerated()).toBe(true);

const inFlight = manager.isHardwareAccelerated();
manager.dispose();

await expect(inFlight).rejects.toThrow(EncryptionError);
});
});
25 changes: 25 additions & 0 deletions packages/cachekit/src/encryption/manager-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export interface EncryptionTenantKeys {
* rather than silently decrypting with the current key only.
*/
keyringEntryCount?(): number;
/**
* cachekit-core's AES hardware detection (informational). Optional: older
* binding binaries predate it, and the manager reports `null` for those.
*/
hardwareAccelerationEnabled?(): boolean;
/**
* Deterministic zeroize-and-release (wasm bindings). NAPI handles zeroize
* via GC finalizer instead and don't expose this.
Expand Down Expand Up @@ -336,6 +341,26 @@ export class EncryptionManagerCore {
return this.tenantKeys.encryptionFingerprint();
}

/**
* Whether AES-256-GCM is hardware-accelerated on this host, per
* cachekit-core's detection (informational — see the README). Initialises
* the bindings if needed, so it answers at startup. `null` = unknown: the
* installed binding predates the accessor.
*
* @throws {EncryptionError} if the manager is disposed or bindings fail to load
*/
async isHardwareAccelerated(): Promise<boolean | null> {
await this.ensureInitialized();
// ensureInitialized() returns early on an already-initialised manager, so
// dispose() can land while this call is suspended at the await. encrypt()
// and decrypt() convert the resulting null read inside their catch; this
// path has none, so it checks explicitly rather than trusting a `!`.
if (this.disposed || !this.tenantKeys) {
throw new EncryptionError('EncryptionManager has been disposed');
}
return this.tenantKeys.hardwareAccelerationEnabled?.() ?? null;
}

/**
* Dispose of the encryption manager and zeroize keys.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ describe('Real Crypto Integration (No Mocks)', () => {
expect(freshKeys.getNonceCounter()).toBe(0);
});

it('reports AES hardware acceleration as a boolean from the real binding', () => {
// The installed binding carries the accessor: a boolean, never undefined.
expect(typeof tenantKeys.hardwareAccelerationEnabled()).toBe('boolean');
});

it('produces different keys for different tenants', () => {
const keys1 = deriveTenantKeys(TEST_MASTER_KEY, 'tenant-1');
const keys2 = deriveTenantKeys(TEST_MASTER_KEY, 'tenant-2');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ describe('encryption vectors — raw wasm bindings', () => {
const plaintext = hexToBytes(vectors[0].plaintext_hex);

expect(tk.getNonceCounter()).toBe(0);
// wasm32 has no AES instructions: detection is honestly false, never undefined.
expect(tk.hardwareAccelerationEnabled()).toBe(false);
const ct1 = encryptWithTenantKeys(plaintext, aad, tk);
expect(tk.getNonceCounter()).toBe(1);
const ct2 = encryptWithTenantKeys(plaintext, aad, tk);
Expand Down
Loading