@push.rocks/smartsecret

OS-backed secret storage plus strict Linux kernel-keyring, TPM2-backed sealed-file, and envelope APIs for Node.js.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

To install @push.rocks/smartsecret, use pnpm:

pnpm install @push.rocks/smartsecret

Usage

@push.rocks/smartsecret provides a unified API for storing and retrieving secrets. It automatically selects the best available backend for the current platform: macOS Keychain on macOS, secret-tool (libsecret / GNOME Keyring) on Linux, or an AES-256-GCM encrypted file as a universal fallback.

Basic Setup

import { SmartSecret } from '@push.rocks/smartsecret';

// Create an instance with default settings
const secretStore = new SmartSecret();

// Or specify a custom service name and vault path
const customSecretStore = new SmartSecret({
  service: 'my-application',
  vaultPath: '/path/to/custom/vault.json',
});

The service option acts as a namespace, isolating secrets so that different applications do not collide. It defaults to 'smartsecret' when omitted.

The vaultPath option only applies to the encrypted-file backend and controls where the vault JSON file is stored. It defaults to ~/.config/smartsecret/vault.json.

Storing a Secret

await secretStore.setSecret('api-key', 'sk-abc123xyz');

If a secret with the same account name already exists under the configured service, it is overwritten.

Retrieving a Secret

const apiKey = await secretStore.getSecret('api-key');

if (apiKey !== null) {
  console.log('Retrieved secret:', apiKey);
} else {
  console.log('Secret not found');
}

Returns null when no secret exists for the given account.

Deleting a Secret

const wasDeleted = await secretStore.deleteSecret('api-key');
console.log(wasDeleted); // true if the secret existed and was removed

Returns false if the secret did not exist.

Listing Accounts

const accounts = await secretStore.listAccounts();
console.log(accounts); // e.g. ['api-key', 'db-password', 'oauth-token']

Returns an array of account names that have stored secrets under the configured service.

Checking the Active Backend

const backendType = await secretStore.getBackendType();
console.log(backendType);
// 'macos-keychain' | 'linux-secret-service' | 'file-encrypted'

This is useful for logging or diagnostics to understand which storage mechanism is in use at runtime.

Service-Based Isolation

Different SmartSecret instances with different service names maintain completely separate secret namespaces, even when sharing the same underlying storage:

const appSecrets = new SmartSecret({ service: 'my-app' });
const ciSecrets = new SmartSecret({ service: 'ci-pipeline' });

await appSecrets.setSecret('token', 'app-token-value');
await ciSecrets.setSecret('token', 'ci-token-value');

const appToken = await appSecrets.getSecret('token');  // 'app-token-value'
const ciToken = await ciSecrets.getSecret('token');     // 'ci-token-value'

SmartSecret Keyring

SmartSecretKeyring is a separate Node.js API for DEK/KEK envelopes. It does not change SmartSecret or any backend API. The keyring is intentionally Linux-only and fails closed on other platforms.

The consumer supplies an ordered key configuration containing exactly one active KEK. Active and retired KEKs are read from absolute credential paths. Each credential must be a regular, non-symlinked, exact 32-byte file without group or world write permissions. Every path component is checked for symlinks, and the loaded key must match its configured SHA-256 fingerprint. Revoked descriptors never contain credential paths or fingerprints.

import { SmartSecretKeyring } from '@push.rocks/smartsecret';

const keyring = await SmartSecretKeyring.create({
  keyringId: 'billing.production',
  keys: [
    {
      version: 1,
      status: 'retired',
      credentialPath: '/run/credentials/billing-kek-v1',
      expectedFingerprint: 'sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
      createdAt: 1_700_000_000_000,
      activatedAt: 1_700_000_100_000,
      retiredAt: 1_710_000_000_000,
      escrowReference: 'escrow.billing-v1',
      escrowedAt: 1_700_000_050_000,
      recoveryTestedAt: 1_700_000_075_000,
    },
    {
      version: 2,
      status: 'active',
      credentialPath: '/run/credentials/billing-kek-v2',
      expectedFingerprint: 'sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
      createdAt: 1_709_000_000_000,
      activatedAt: 1_710_000_000_000,
    },
  ],
});

const context = new TextEncoder().encode('tenant:acme/secret:database-password');
const plaintext = new TextEncoder().encode('secret value');

const envelope = await keyring.encryptEnvelope({
  envelopeId: 'secret.database-password',
  plaintext,
  context,
});

const decrypted = await keyring.decryptEnvelope({
  envelope,
  expectedEnvelopeId: 'secret.database-password',
  context,
});

const rewrapped = await keyring.rewrapEnvelope({
  envelope,
  expectedEnvelopeId: 'secret.database-password',
  context,
  targetVersion: 2,
});

const recoveryResult = await keyring.recoverySelfTest(2);
const inspection = keyring.inspect();
keyring.destroy();

encryptEnvelope() always uses the sole active KEK. decryptEnvelope() permits active and retired KEKs, but rejects revoked and unknown versions. rewrapEnvelope() authenticates the current wrapper and context, then calls SmartCrypto's DEK rewrap operation without decrypting the payload. It preserves the payload nonce, ciphertext, and tag exactly.

The profile is fixed to smartsecret-aes-256-gcm-dek-kek-v1. Payloads may contain 0 through 524,288 bytes. Context is mandatory and may contain 1 through 65,536 bytes. Identifiers use the pattern ^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$ and are limited to 200 UTF-8 bytes.

Key loading is an immutable per-instance snapshot. Replacing a credential file does not reload an existing keyring; create a new instance and swap it at the application boundary. destroy() marks an instance unusable and best-effort wipes retained key arrays. Operation-owned key, plaintext, and context copies are also wiped in finally blocks. JavaScript runtimes, garbage collectors, and native cryptographic implementations can retain copies outside package control, so zeroization is best effort rather than a hard memory-erasure guarantee.

The package never persists envelopes. The consumer owns durable envelope storage, compare-and-swap coordination during rewrap, and any metadata updates after recovery testing.

SmartSecret Kernel Store

SmartSecretKernelStore is a separate, fail-closed API for small secrets in the Linux kernel keyring. It does not change the legacy SmartSecret backend selection or add a fallback to it. The store requires Node.js 24 or 25 on Linux x64 and starts only the package-owned static worker at dist_rust/smartsecret-kernel_linux_amd64. It never searches PATH, accepts an environment override, invokes a shell, probes Secret Service, or persists secret values to the filesystem.

import { SmartSecretKernelStore } from '@push.rocks/smartsecret';

const store = await SmartSecretKernelStore.create({
  service: 'example.application',
});

try {
  await store.setEntry('oidc-client-secret', new TextEncoder().encode('secret'));
  const value = await store.getEntry('oidc-client-secret');
  const deleted = await store.deleteEntry('oidc-client-secret');
  value?.fill(0);
} finally {
  await store.close();
}

Service and account strings must contain valid Unicode scalar values and encode to 1 through 1,024 UTF-8 bytes. Values may contain 0 through 16,384 bytes. The stored value is framed with a fixed version and exact length before publication. Existing service rings and entries are accepted only when their type, description, owner, and private permissions match the package contract. Kernel per-user quotas may still reject writes before this per-entry limit is reached; the store reports the kernel failure and never falls back to filesystem storage.

setEntry() copies its input before dispatch, and getEntry() returns a fresh byte array. deleteEntryIfValue() invalidates only an entry whose current framed value matches the supplied expected bytes; a replacement rejects with SOURCE_CHANGED. moveEntry() moves one exact framed entry to another account under the same service-wide lease: it verifies the destination before invalidating the source, and matching source/destination values converge to the destination. A differing destination rejects with TARGET_CONFLICT. When the source is already absent, alreadyMoved reports only that a structurally valid destination entry exists; callers that did not observe the original source must independently bind the destination value to their durable state. The caller still owns its input and returned copies and should wipe them when no longer needed. JavaScript and operating-system buffers can retain additional copies, so this remains best-effort zeroization rather than guaranteed memory erasure.

The store uses one persistent-user-owned service ring and stages unpublished objects in the process keyring. An inherited session link is preserved and revalidated, but it cannot override or conflict with the persistent root. A revoked inherited session keyring, which can remain after PAM logout, is treated as absent because it is optional and unusable; any failure to establish or validate the required process and persistent roots still fails closed. Publication is verified before staging ownership is removed. Every operation also acquires a service-wide NamedMutex, which coordinates cooperating processes running as the same OS identity on the same machine.

Generic and legacy searches classify missing, revoked, or expired matches as absent only after the authoritative root revalidates. An unusable root remains a fail-closed kernel error rather than an absent entry.

Kernel keyring permissions are an isolation boundary between OS identities, not between processes running under the same UID. Same-UID processes with access to the relevant keyrings may be able to read entries. Persistent keyrings can expire under kernel policy and are cleared by reboot. Applications must treat null as absence, not as evidence that a secret existed previously.

Operation timeouts default to 5 seconds and may be set from 1 through 60,000 milliseconds. A timeout or abort covers queueing, mutex acquisition, and the worker request under one monotonic deadline. If a mutating request is interrupted after dispatch, or publication/release cannot be confirmed, the store rejects with a SmartSecretKernelStoreError whose code is MUTATION_OUTCOME_UNKNOWN, terminates the worker, and remains poisoned. Read-side worker integrity failures also poison and terminate the store. Always await close(); it waits for already-reserved operations and confirms worker termination.

Sealed file store

The kernel-backed SmartSecretSealedFileStore.create() mode is a Linux-only API that composes a caller-owned SmartSecretKernelStore for data sets that exceed the kernel's per-user key quota. It stores one 32-byte master key in the kernel keyring and one authenticated AES-256-GCM envelope per account in a dedicated private directory supplied by the caller. The TPM2-backed mode described below replaces only the master-key backend.

import {
  SmartSecretKernelStore,
  SmartSecretSealedFileStore,
} from '@push.rocks/smartsecret';

const kernelStore = await SmartSecretKernelStore.create({
  service: 'example.application',
});
try {
  const sealedStore = await SmartSecretSealedFileStore.create({
    kernelStore,
    storeId: 'oauth-credentials',
    directoryPath: '/absolute/private/application/directory/credentials',
  });
  try {
    await sealedStore.setEntry('account-a', new TextEncoder().encode('secret'));
    const value = await sealedStore.getEntry('account-a');
    value?.fill(0);
  } finally {
    await sealedStore.close();
  }
} finally {
  await kernelStore.close();
}

The directory is required to be owned by the effective user and mode 0700; envelope and manifest files are mode 0600. Every directory component must be owned by root or the effective user and must not be group- or world-writable, except for root-owned sticky directories such as /tmp. Directory traversal, creation, and store I/O are anchored to open directory descriptors, and final components use O_NOFOLLOW. Writes use random exclusive temporary files, file and directory fsync, and atomic replacement. Reads reject unexpected owners or permissions, malformed envelopes, identity mismatches, oversized ciphertext, and authentication failures. The filesystem boundary, like the kernel keyring, trusts other processes running under the same UID.

The non-secret manifest binds the directory to the kernel master-key fingerprint. If the kernel key is lost or expires while any manifest, envelope, or temporary artifact remains, create() fails with MASTER_KEY_UNAVAILABLE and never silently generates a replacement. SmartSecretSealedFileStore.reset() is an explicit destructive recovery operation: after verifying any existing manifest belongs to the requested service and store ID, it removes only the store-owned manifest, envelope, and temporary files, removes the old kernel entry if present, and creates a new empty store.

SmartSecretSealedFileStore.relocate() moves an initialized store to another absolute path on the same filesystem and rebinds the exact master key to the destination path-derived account. It never decrypts or rewrites the manifest or envelopes. Both private parent directories must already exist, while the destination store directory must not exist before the first attempt. The operation locks both paths in deterministic order, writes an inode- and request-bound relocation receipt, atomically renames the directory, fsyncs both parents, moves the kernel entry, removes the receipt, and returns a ready destination store. The receipt allows the same call to resume its own interrupted rename; arbitrary external moves and copied destinations are rejected. A completed call remains idempotent because the destination key is verified against the unchanged manifest.

The caller must durably record the exact service, store ID, source path, and destination path before quiescing writers and calling relocate(). Preserve that intent after every rejected call: KERNEL_UNAVAILABLE, FILESYSTEM_FAILED, master-key errors, and MUTATION_OUTCOME_UNKNOWN can all be reported after the directory was renamed, so an error code does not establish which path currently exists. Recovery must call relocate() again with the same path pair; create a fresh kernel store first when the prior store was poisoned or its worker integrity is uncertain. Do not call create() or reset() at either path while the intent or relocation receipt may remain, and never initialize a new store at the retired source path. Clear the caller-owned intent only after relocate() returns a ready destination store and the application has durably committed the destination as authoritative.

Each store instance admits at most 64 operations at once. Each admitted operation has one 60-second monotonic deadline covering the local operation queue and cross-process mutex acquisition. The mutex is directory-wide, so different service or store identities targeting the same directory serialize with each other. Work already running after mutex acquisition is not interrupted by that admission deadline. close() rejects new operations, drains admitted operations, and then best-effort wipes the retained master-key copy.

TPM2 credential bytes

sealSmartSecretTpm2Credential(name, plaintext, options?) and unsealSmartSecretTpm2Credential(name, ciphertext, options?) expose TPM2 sealing without a filesystem store. Both accept and return Uint8Array; the caller owns persistence of the ciphertext, for example in a database, and must erase plaintext buffers after use. Caller-provided buffers are not modified.

import { sealSmartSecretTpm2Credential, unsealSmartSecretTpm2Credential } from '@push.rocks/smartsecret';

const name = 'myapp.connection.v1.unique-profile-id';
const plaintext = new TextEncoder().encode('example-secret');
try {
  const ciphertext = await sealSmartSecretTpm2Credential(name, plaintext);
  // Store ciphertext through the application's persistence layer.
  const recovered = await unsealSmartSecretTpm2Credential(name, ciphertext);
  try { /* Use recovered bytes without logging them. */ }
  finally { recovered.fill(0); }
} finally { plaintext.fill(0); }

The name must contain 1200 ASCII letters, digits, underscores, dots or hyphens. Choose an immutable application and record identity; decryption requires the same name and local TPM. Input and subprocess output are each capped at 64 KiB, including the credential envelope, so callers should keep plaintext well below that limit. The default command is /usr/bin/systemd-creds with a 15-second deadline. The optional ISmartSecretTpm2CommandOptions accepts an absolute executablePath and integer timeoutMs from 1 through 60,000; executable overrides must be trusted. No shell, plaintext files, application-state files, or fallback backend are used.

These functions use the same TPM-only, empty-PCR policy and runtime requirements as the store below. Failures use SmartSecretSealedFileStoreError with INVALID_ARGUMENT or TPM_UNAVAILABLE, without secret-bearing causes. Clearing or replacing the TPM makes existing ciphertext unavailable; retain an independent credential recovery method if needed.

TPM2-backed sealed file store

SmartSecretSealedFileStore.createTpm2() keeps the same authenticated envelope and manifest format while sealing the 32-byte master key to the local TPM2 through /usr/bin/systemd-creds. Plaintext key material is passed only through bounded process pipes. The TPM ciphertext is stored in the same private store directory with mode 0600; no plaintext key file or fallback backend is used.

const store = await SmartSecretSealedFileStore.createTpm2({
  service: 'example.application',
  storeId: 'oauth-credentials',
  directoryPath: '/absolute/private/application/directory/credentials',
});

On Linux, the runtime user needs access to the TPM resource-manager device, normally through the tss group. systemd-creds and its TPM2 runtime libraries must be installed. Missing tools, libraries, device permissions, TPM access, and unsealing failures reject with TPM_UNAVAILABLE; there is no kernel-keyring or file-key fallback.

The systemd credential name is derived from the exact service, store ID, and absolute directory. Encryption uses --with-key=tpm2, --tpm2-device=auto, and an intentionally empty --tpm2-pcrs= policy. This binds the key to the local TPM while allowing normal reboot, kernel, bootloader, and firmware updates. It does not provide measured-boot PCR binding. Clearing, replacing, or detaching the TPM makes the sealed master key unavailable.

An existing kernel-backed store can be migrated without rewriting its manifest or encrypted entries:

const kernelStore = await SmartSecretKernelStore.create({
  service: 'example.application',
});
try {
  const store = await SmartSecretSealedFileStore.createTpm2({
    service: kernelStore.service,
    storeId: 'oauth-credentials',
    directoryPath: '/absolute/private/application/directory/credentials',
    legacyKernelStore: kernelStore,
  });
  // The matching legacy master key has been durably copied, verified, and removed.
  await store.close();
} finally {
  await kernelStore.close();
}

Migration runs under the sealed directory mutex. It verifies the TPM destination against the manifest before conditionally deleting the exact observed legacy key through deleteEntryIfValue(), and interrupted source-only, destination-only, duplicate, pending-file, and source-deletion states converge on retry. Cooperating kernel-store writers share the service mutex; uncooperative same-UID keyutils writers remain inside the documented trusted boundary and must be quiescent. Conflicting keys preserve both copies and fail closed. An initialized store with neither key rejects with MASTER_KEY_UNAVAILABLE and is never reset by createTpm2().

resetTpm2() is the only destructive TPM recovery operation. It first confirms that all TPM and optional legacy key artifacts are durably absent, then removes envelopes and the manifest, and finally initializes a fresh TPM key. Call it only after explicit operator confirmation that the old ciphertext may be discarded. Once deletion begins, any later failure is reported as MUTATION_OUTCOME_UNKNOWN: the old store may already be destroyed even when no new store was returned, so recovery must retry the same explicit reset.

TPM credential names are path-bound, so SmartSecretSealedFileStore.relocate() does not support TPM-backed directories. The legacy kernel-backed create(), reset(), and relocate() methods reject directories containing TPM artifacts instead of moving or deleting them.

DevIdP v1 keyutils migration

The two DevIdP migration methods are intentionally narrow. They are available only on a store created with service global.idp.devidp and accept only accounts matching v1:[a-f0-9]{64}:

const source = await store.readDevIdpV1Legacy(legacyAccount);
if (source) {
  // Parse the DevIdP envelope and durably verify every destination first.
  await store.deleteDevIdpV1Legacy(source.receipt);
}

readDevIdpV1Legacy() returns 1 through 16,384 opaque bytes plus a one-use, store-bound receipt. Deletion reacquires the same mutex and atomically rechecks the source serial, root membership, byte length, and SHA-256 digest inside one worker command. A replacement or changed value is not deleted and rejects with a SmartSecretKernelStoreError whose code is SOURCE_CHANGED. Linux keyutils does not provide compare-and-delete against an uncooperative writer that updates the same serial, so the legacy writer must be stopped or otherwise operationally quiescent before migration.

This migration surface covers only a legacy record proven to have been written through the @napi-rs/keyring keyutils fallback. It does not probe or migrate Secret Service. If Secret Service could own the source, that source-specific migration must be implemented at its owning layer before treating a kernel-keyring miss as authoritative.

API Reference

SmartSecret

The main class. Instantiate it to store and retrieve secrets.

Constructor

new SmartSecret(options?: ISmartSecretOptions)
Option Type Default Description
service string 'smartsecret' Namespace for secret isolation
vaultPath string ~/.config/smartsecret/vault.json Path to the encrypted vault file (file backend only)

Methods

Method Signature Description
setSecret (account: string, secret: string) => Promise<void> Store or overwrite a secret
getSecret (account: string) => Promise<string | null> Retrieve a secret, or null if not found
deleteSecret (account: string) => Promise<boolean> Delete a secret; returns true if it existed
listAccounts () => Promise<string[]> List all account names for the configured service
getBackendType () => Promise<TBackendType> Returns the active backend identifier

SmartSecretKeyring

Create keyrings through the asynchronous factory. The constructor is not public.

SmartSecretKeyring.create(config: ISmartSecretKeyringConfig): Promise<SmartSecretKeyring>
Method Signature Description
encryptEnvelope (options: IEncryptSmartSecretEnvelopeOptions) => Promise<ISmartSecretEnvelopeV1> Encrypt with a random DEK and wrap it under the active KEK
decryptEnvelope (options: IDecryptSmartSecretEnvelopeOptions) => Promise<Uint8Array> Strictly parse, authenticate, and decrypt an envelope
rewrapEnvelope (options: IRewrapSmartSecretEnvelopeOptions) => Promise<ISmartSecretEnvelopeV1> Rewrap only the DEK under the active KEK
recoverySelfTest (version: number) => Promise<ISmartSecretRecoverySelfTestResult> Wrap and unwrap a synthetic random DEK for an active or retired version
inspect () => ISmartSecretKeyringInspection Return cloned, value-free metadata without credential paths or key bytes
destroy () => void Mark the instance unusable and best-effort wipe retained key arrays

All keyring failures are SmartSecretKeyringError instances with a stable TSmartSecretKeyringErrorCode. Their message, JSON representation, Node.js inspection output, and stack contain only the code and fixed package boilerplate. Filesystem and SmartCrypto causes are not retained.

SmartSecretKernelStore

Create kernel stores through the asynchronous factory. The constructor is not public.

SmartSecretKernelStore.create(
  options: ISmartSecretKernelStoreOptions,
): Promise<SmartSecretKernelStore>
Method Signature Description
getEntry (account: string, options?) => Promise<Uint8Array | null> Read a copied raw value or return null when absent
setEntry (account: string, value: Uint8Array, options?) => Promise<void> Create or replace one framed entry
deleteEntry (account: string, options?) => Promise<boolean> Delete an entry and report whether it existed
deleteEntryIfValue (account: string, expectedValue: Uint8Array, options?) => Promise<'deleted' | 'alreadyAbsent'> Delete only the exact currently observed framed value; replacements reject with SOURCE_CHANGED
moveEntry (sourceAccount: string, destinationAccount: string, options?) => Promise<'moved' | 'alreadyMoved' | 'sourceAbsent'> Idempotently move one exact framed entry without exposing its value
readDevIdpV1Legacy (account: string, options?) => Promise<IDevIdpV1LegacyRead | null> Read one verified, opaque keyutils legacy value
deleteDevIdpV1Legacy (receipt, options?) => Promise<'deleted' | 'alreadyAbsent'> Revalidate and consume one legacy receipt
close () => Promise<void> Wait for reserved operations and confirm worker termination

Operation options contain optional timeoutMs and signal: AbortSignal properties. Failures are code-only SmartSecretKernelStoreError instances. Stable codes distinguish invalid input, unsupported runtime, unavailable kernel/worker/mutex resources, root conflicts, corrupt entries, changed sources, target conflicts, pre-dispatch aborts/timeouts, unknown mutation outcomes, poisoned/closed lifecycle state, and worker integrity failures. Secret values and underlying causes are never retained on these errors.

SmartSecretSealedFileStore

Create a kernel-backed sealed store with a caller-owned kernel store and a dedicated absolute private directory:

SmartSecretSealedFileStore.create(
  options: ISmartSecretSealedFileStoreOptions,
): Promise<SmartSecretSealedFileStore>

This API is available only on Linux. directoryPath must be an absolute normalized path. The exported options and kernel-store contract are:

interface ISmartSecretSealedFileKernelStore {
  readonly service: string;
  getEntry(
    account: string,
    options?: ISmartSecretKernelOperationOptions,
  ): Promise<Uint8Array | null>;
  setEntry(
    account: string,
    value: Uint8Array,
    options?: ISmartSecretKernelOperationOptions,
  ): Promise<void>;
  deleteEntry(
    account: string,
    options?: ISmartSecretKernelOperationOptions,
  ): Promise<boolean>;
}

interface ISmartSecretSealedFileStoreOptions {
  kernelStore: ISmartSecretSealedFileKernelStore;
  storeId: string;
  directoryPath: string;
}

interface ISmartSecretSealedFileConditionalDeleteKernelStore
  extends ISmartSecretSealedFileKernelStore {
  deleteEntryIfValue(
    account: string,
    expectedValue: Uint8Array,
    options?: ISmartSecretKernelOperationOptions,
  ): Promise<'deleted' | 'alreadyAbsent'>;
}

interface ISmartSecretSealedFileTpm2StoreOptions {
  service: string;
  storeId: string;
  directoryPath: string;
  legacyKernelStore?: ISmartSecretSealedFileConditionalDeleteKernelStore;
}

interface ISmartSecretSealedFileRelocationKernelStore
  extends ISmartSecretSealedFileKernelStore {
  moveEntry(
    sourceAccount: string,
    destinationAccount: string,
    options?: ISmartSecretKernelOperationOptions,
  ): Promise<'moved' | 'alreadyMoved' | 'sourceAbsent'>;
}

interface ISmartSecretSealedFileStoreRelocationOptions {
  kernelStore: ISmartSecretSealedFileRelocationKernelStore;
  storeId: string;
  sourceDirectoryPath: string;
  destinationDirectoryPath: string;
}

Instances expose readonly service, storeId, and directoryPath properties. The exported smartSecretSealedFileMaximumEntryBytes constant is 524288.

Method Signature Description
create (options) => Promise<SmartSecretSealedFileStore> Open or initialize a store without replacing missing key material
createTpm2 (options) => Promise<SmartSecretSealedFileStore> Open a TPM2-backed store or migrate its matching legacy kernel master key
relocate (options) => Promise<SmartSecretSealedFileStore> Move one initialized store, rebind its exact master key, and resume completed or interrupted moves
reset (options) => Promise<SmartSecretSealedFileStore> Destructively discard store-owned ciphertext and create a new empty store
resetTpm2 (options) => Promise<SmartSecretSealedFileStore> Explicitly discard TPM/legacy key artifacts and ciphertext, then create a fresh TPM2-backed store
getEntry (account: string) => Promise<Uint8Array | null> Authenticate and decrypt a copied value or return null when absent
setEntry (account: string, value: Uint8Array) => Promise<void> Atomically encrypt and replace an entry up to 524,288 bytes
deleteEntry (account: string) => Promise<boolean> Delete an encrypted entry and report whether it existed
close () => Promise<void> Drain admitted operations and best-effort wipe the retained master-key copy

Failures are code-only SmartSecretSealedFileStoreError instances. TSmartSecretSealedFileStoreErrorCode is the exact union AUTHENTICATION_FAILED | ENVELOPE_INVALID | FILESYSTEM_FAILED | INVALID_ARGUMENT | KERNEL_UNAVAILABLE | MASTER_KEY_CONFLICT | MASTER_KEY_UNAVAILABLE | MUTATION_OUTCOME_UNKNOWN | MUTEX_FAILED | OPERATION_LIMIT_REACHED | SIZE_LIMIT_EXCEEDED | STORE_CLOSED | TPM_UNAVAILABLE. ISmartSecretSealedFileStoreErrorJson describes the stable { name, code, message } JSON and inspection shape. The caller retains ownership of an optional legacy kernel store and must close it separately.

Types

type TBackendType = 'macos-keychain' | 'linux-secret-service' | 'file-encrypted';

interface ISmartSecretOptions {
  service?: string;
  vaultPath?: string;
}

interface ISecretBackend {
  readonly backendType: TBackendType;
  isAvailable(): Promise<boolean>;
  setSecret(account: string, secret: string): Promise<void>;
  getSecret(account: string): Promise<string | null>;
  deleteSecret(account: string): Promise<boolean>;
  listAccounts(): Promise<string[]>;
}

Backend Classes

Each backend implements ISecretBackend and can be used directly if needed:

  • MacosKeychainBackend -- macOS Keychain via the security CLI
  • LinuxSecretServiceBackend -- Linux Secret Service via secret-tool
  • FileEncryptedBackend -- AES-256-GCM encrypted JSON vault file

Backends

SmartSecret tries each backend in order and uses the first one that reports itself as available.

macOS Keychain (macos-keychain)

Used automatically on macOS when the security command-line tool is present (ships with macOS by default). Secrets are stored as generic password items in the user's default keychain. The service option maps to the keychain service name, and the account parameter maps to the keychain account name.

Linux Secret Service (linux-secret-service)

Used automatically on Linux when secret-tool is installed. This integrates with GNOME Keyring, KDE Wallet, or any other provider that implements the freedesktop.org Secret Service D-Bus API. Install the tool on Debian/Ubuntu with:

sudo apt install libsecret-tools

Secrets are stored with service and account as lookup attributes.

Encrypted File (file-encrypted)

The universal fallback that works on all platforms. Secrets are encrypted with AES-256-GCM and stored in a JSON vault file. A random 32-byte key is generated on first use and stored alongside the vault at ~/.config/smartsecret/.keyfile (with 0600 permissions). The encryption key is derived from the keyfile using PBKDF2 with 100,000 iterations of SHA-512, salted with the service name.

Vault writes are atomic (write to a temporary file, then rename) to prevent corruption. Both the keyfile and the vault file are created with restrictive file permissions.

File locations (defaults):

File Path
Vault ~/.config/smartsecret/vault.json
Keyfile ~/.config/smartsecret/.keyfile

Both paths can be influenced by providing a custom vaultPath in the constructor options. The keyfile is always stored in the same directory as the vault.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.

The packaged Linux kernel worker contains statically linked third-party Rust components. Their copyright and license notices are reproduced in third-party-notices.md.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
979 KiB
Languages
TypeScript 81.3%
Rust 18.7%