194 lines
6.7 KiB
Markdown
194 lines
6.7 KiB
Markdown
|
|
# @push.rocks/smartsecret
|
||
|
|
|
||
|
|
OS keychain-based secret storage with encrypted-file fallback for Node.js.
|
||
|
|
|
||
|
|
## Install
|
||
|
|
|
||
|
|
To install `@push.rocks/smartsecret`, use pnpm:
|
||
|
|
|
||
|
|
```shell
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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 secretStore = 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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'
|
||
|
|
```
|
||
|
|
|
||
|
|
## API Reference
|
||
|
|
|
||
|
|
### `SmartSecret`
|
||
|
|
|
||
|
|
The main class. Instantiate it to store and retrieve secrets.
|
||
|
|
|
||
|
|
#### Constructor
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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 |
|
||
|
|
|
||
|
|
### Types
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:
|
||
|
|
|
||
|
|
```shell
|
||
|
|
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.
|
||
|
|
|
||
|
|
## License and Legal Information
|
||
|
|
|
||
|
|
This project is licensed under the MIT license. For more details, see the `license` file in the repository.
|
||
|
|
|
||
|
|
By using this package, you agree to the licensing terms.
|