Compare commits
	
		
			2 Commits
		
	
	
		
			92c41bdb0c
			...
			18fb79e971
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 18fb79e971 | |||
| b301a414cb | 
							
								
								
									
										120
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								README.md
									
									
									
									
									
								
							@ -1,6 +1,13 @@
 | 
				
			|||||||
# Secret - Hierarchical Secret Manager
 | 
					# secret - Local Secret Manager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Secret is a command-line secret manager that implements a hierarchical key architecture for storing and managing sensitive data. It supports multiple vaults, various unlock mechanisms, and provides secure storage using the Age encryption library.
 | 
					secret is a command-line local secret manager that implements a hierarchical
 | 
				
			||||||
 | 
					key architecture for storing and managing sensitive data. It supports
 | 
				
			||||||
 | 
					multiple vaults, various unlock mechanisms, and provides secure storage
 | 
				
			||||||
 | 
					using the `age` encryption library.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It could be used as password manager, but was not designed as such.  I
 | 
				
			||||||
 | 
					created it to scratch an itch for a secure key/value store for replacing a
 | 
				
			||||||
 | 
					bunch of pgp-encrypted files in a directory structure.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Core Architecture
 | 
					## Core Architecture
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -8,9 +15,12 @@ Secret is a command-line secret manager that implements a hierarchical key archi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Secret implements a three-layer key architecture:
 | 
					Secret implements a three-layer key architecture:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide the foundation for all encryption
 | 
					1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide
 | 
				
			||||||
2. **Unlockers**: Short-term keys that encrypt the long-term keys, supporting multiple authentication methods
 | 
					   the foundation for all encryption
 | 
				
			||||||
3. **Version-specific Keys**: Per-version keys that encrypt individual secret values
 | 
					2. **Unlockers**: Short-term keys that encrypt the long-term keys,
 | 
				
			||||||
 | 
					   supporting multiple authentication methods
 | 
				
			||||||
 | 
					3. **Version-specific Keys**: Per-version keys that encrypt individual
 | 
				
			||||||
 | 
					   secret values
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Version Management
 | 
					### Version Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,7 +32,9 @@ Each secret maintains a history of versions, with each version having:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Vault System
 | 
					### Vault System
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vaults provide logical separation of secrets, each with its own long-term key and unlocker set. This allows for complete isolation between different contexts (work, personal, projects).
 | 
					Vaults provide logical separation of secrets, each with its own long-term
 | 
				
			||||||
 | 
					key and unlocker set. This allows for complete isolation between different
 | 
				
			||||||
 | 
					contexts (work, personal, projects).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Installation
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,7 +73,9 @@ make build
 | 
				
			|||||||
### Initialization
 | 
					### Initialization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret init`
 | 
					#### `secret init`
 | 
				
			||||||
Initializes the secret manager with a default vault. Prompts for a BIP39 mnemonic phrase and creates the initial directory structure.
 | 
					
 | 
				
			||||||
 | 
					Initializes the secret manager with a default vault. Prompts for a BIP39
 | 
				
			||||||
 | 
					mnemonic phrase and creates the initial directory structure.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Environment Variables:**
 | 
					**Environment Variables:**
 | 
				
			||||||
- `SB_SECRET_MNEMONIC`: Pre-set mnemonic phrase
 | 
					- `SB_SECRET_MNEMONIC`: Pre-set mnemonic phrase
 | 
				
			||||||
@ -70,23 +84,32 @@ Initializes the secret manager with a default vault. Prompts for a BIP39 mnemoni
 | 
				
			|||||||
### Vault Management
 | 
					### Vault Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret vault list [--json]` / `secret vault ls`
 | 
					#### `secret vault list [--json]` / `secret vault ls`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Lists all available vaults. The current vault is marked.
 | 
					Lists all available vaults. The current vault is marked.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret vault create <name>`
 | 
					#### `secret vault create <name>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Creates a new vault with the specified name.
 | 
					Creates a new vault with the specified name.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret vault select <name>`
 | 
					#### `secret vault select <name>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Switches to the specified vault for subsequent operations.
 | 
					Switches to the specified vault for subsequent operations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret vault remove <name> [--force]` / `secret vault rm` ⚠️ 🛑
 | 
					#### `secret vault remove <name> [--force]` / `secret vault rm` ⚠️ 🛑
 | 
				
			||||||
**DANGER**: Permanently removes a vault and all its secrets. Like Unix `rm`, this command does not ask for confirmation.
 | 
					
 | 
				
			||||||
Requires --force if the vault contains secrets. With --force, will automatically switch to another vault if removing the current one.
 | 
					**DANGER**: Permanently removes a vault and all its secrets. Like Unix `rm`,
 | 
				
			||||||
 | 
					this command does not ask for confirmation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requires --force if the vault contains secrets. With --force, will
 | 
				
			||||||
 | 
					automatically switch to another vault if removing the current one.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- `--force, -f`: Force removal even if vault contains secrets
 | 
					- `--force, -f`: Force removal even if vault contains secrets
 | 
				
			||||||
- **NO RECOVERY**: All secrets in the vault will be permanently deleted
 | 
					- **NO RECOVERY**: All secrets in the vault will be permanently deleted
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Secret Management
 | 
					### Secret Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret add <secret-name> [--force]`
 | 
					#### `secret add <secret-name> [--force]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Adds a secret to the current vault. Reads the secret value from stdin.
 | 
					Adds a secret to the current vault. Reads the secret value from stdin.
 | 
				
			||||||
- `--force, -f`: Overwrite existing secret
 | 
					- `--force, -f`: Overwrite existing secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -95,18 +118,23 @@ Adds a secret to the current vault. Reads the secret value from stdin.
 | 
				
			|||||||
- Examples: `database/password`, `api.key`, `ssh_private_key`
 | 
					- Examples: `database/password`, `api.key`, `ssh_private_key`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret get <secret-name> [--version <version>]`
 | 
					#### `secret get <secret-name> [--version <version>]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Retrieves and outputs a secret value to stdout.
 | 
					Retrieves and outputs a secret value to stdout.
 | 
				
			||||||
- `--version, -v`: Get a specific version (default: current)
 | 
					- `--version, -v`: Get a specific version (default: current)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret list [filter] [--json]` / `secret ls`
 | 
					#### `secret list [filter] [--json]` / `secret ls`
 | 
				
			||||||
Lists all secrets in the current vault. Optional filter for substring matching.
 | 
					
 | 
				
			||||||
 | 
					Lists all secrets in the current vault. Optional filter for substring
 | 
				
			||||||
 | 
					matching.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret remove <secret-name>` / `secret rm` ⚠️ 🛑
 | 
					#### `secret remove <secret-name>` / `secret rm` ⚠️ 🛑
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**DANGER**: Permanently removes a secret and ALL its versions. Like Unix `rm`, this command does not ask for confirmation.
 | 
					**DANGER**: Permanently removes a secret and ALL its versions. Like Unix `rm`, this command does not ask for confirmation.
 | 
				
			||||||
- **NO RECOVERY**: Once removed, the secret cannot be recovered
 | 
					- **NO RECOVERY**: Once removed, the secret cannot be recovered
 | 
				
			||||||
- **ALL VERSIONS DELETED**: Every version of the secret will be permanently deleted
 | 
					- **ALL VERSIONS DELETED**: Every version of the secret will be permanently deleted
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret move <source> <destination>` / `secret mv` / `secret rename`
 | 
					#### `secret move <source> <destination>` / `secret mv` / `secret rename`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Moves or renames a secret within the current vault.
 | 
					Moves or renames a secret within the current vault.
 | 
				
			||||||
- Fails if the destination already exists
 | 
					- Fails if the destination already exists
 | 
				
			||||||
- Preserves all versions and metadata
 | 
					- Preserves all versions and metadata
 | 
				
			||||||
@ -114,22 +142,29 @@ Moves or renames a secret within the current vault.
 | 
				
			|||||||
### Version Management
 | 
					### Version Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret version list <secret-name>` / `secret version ls`
 | 
					#### `secret version list <secret-name>` / `secret version ls`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Lists all versions of a secret showing creation time, status, and validity period.
 | 
					Lists all versions of a secret showing creation time, status, and validity period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret version promote <secret-name> <version>`
 | 
					#### `secret version promote <secret-name> <version>`
 | 
				
			||||||
Promotes a specific version to current by updating the symlink. Does not modify any timestamps, allowing for rollback scenarios.
 | 
					
 | 
				
			||||||
 | 
					Promotes a specific version to current by updating the symlink. Does not
 | 
				
			||||||
 | 
					modify any timestamps, allowing for rollback scenarios.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret version remove <secret-name> <version>` / `secret version rm` ⚠️ 🛑
 | 
					#### `secret version remove <secret-name> <version>` / `secret version rm` ⚠️ 🛑
 | 
				
			||||||
**DANGER**: Permanently removes a specific version of a secret. Like Unix `rm`, this command does not ask for confirmation.
 | 
					
 | 
				
			||||||
 | 
					**DANGER**: Permanently removes a specific version of a secret. Like Unix
 | 
				
			||||||
 | 
					`rm`, this command does not ask for confirmation.
 | 
				
			||||||
- **NO RECOVERY**: Once removed, this version cannot be recovered
 | 
					- **NO RECOVERY**: Once removed, this version cannot be recovered
 | 
				
			||||||
- Cannot remove the current version (must promote another version first)
 | 
					- Cannot remove the current version (must promote another version first)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Key Generation
 | 
					### Key Generation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret generate mnemonic`
 | 
					#### `secret generate mnemonic`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Generates a cryptographically secure BIP39 mnemonic phrase.
 | 
					Generates a cryptographically secure BIP39 mnemonic phrase.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret generate secret <name> [--length=16] [--type=base58] [--force]`
 | 
					#### `secret generate secret <name> [--length=16] [--type=base58] [--force]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Generates and stores a random secret.
 | 
					Generates and stores a random secret.
 | 
				
			||||||
- `--length, -l`: Length of generated secret (default: 16)
 | 
					- `--length, -l`: Length of generated secret (default: 16)
 | 
				
			||||||
- `--type, -t`: Type of secret (`base58`, `alnum`)
 | 
					- `--type, -t`: Type of secret (`base58`, `alnum`)
 | 
				
			||||||
@ -138,9 +173,11 @@ Generates and stores a random secret.
 | 
				
			|||||||
### Unlocker Management
 | 
					### Unlocker Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret unlocker list [--json]` / `secret unlocker ls`
 | 
					#### `secret unlocker list [--json]` / `secret unlocker ls`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Lists all unlockers in the current vault with their metadata.
 | 
					Lists all unlockers in the current vault with their metadata.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret unlocker add <type> [options]`
 | 
					#### `secret unlocker add <type> [options]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Creates a new unlocker of the specified type:
 | 
					Creates a new unlocker of the specified type:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Types:**
 | 
					**Types:**
 | 
				
			||||||
@ -152,29 +189,38 @@ Creates a new unlocker of the specified type:
 | 
				
			|||||||
- `--keyid <id>`: GPG key ID (optional for PGP type, uses default key if not specified)
 | 
					- `--keyid <id>`: GPG key ID (optional for PGP type, uses default key if not specified)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret unlocker remove <unlocker-id> [--force]` / `secret unlocker rm` ⚠️ 🛑
 | 
					#### `secret unlocker remove <unlocker-id> [--force]` / `secret unlocker rm` ⚠️ 🛑
 | 
				
			||||||
**DANGER**: Permanently removes an unlocker. Like Unix `rm`, this command does not ask for confirmation.
 | 
					
 | 
				
			||||||
Cannot remove the last unlocker if the vault has secrets unless --force is used.
 | 
					**DANGER**: Permanently removes an unlocker. Like Unix `rm`, this command
 | 
				
			||||||
 | 
					does not ask for confirmation.  Cannot remove the last unlocker if the vault
 | 
				
			||||||
 | 
					has secrets unless --force is used.
 | 
				
			||||||
- `--force, -f`: Force removal of last unlocker even if vault has secrets
 | 
					- `--force, -f`: Force removal of last unlocker even if vault has secrets
 | 
				
			||||||
- **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase, vault data will be PERMANENTLY INACCESSIBLE
 | 
					- **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase,
 | 
				
			||||||
- **NO RECOVERY**: Removing all unlockers without having your mnemonic means losing access to all secrets forever
 | 
					  vault data will be PERMANENTLY INACCESSIBLE
 | 
				
			||||||
 | 
					- **NO RECOVERY**: Removing all unlockers without having your mnemonic means
 | 
				
			||||||
 | 
					  losing access to all secrets forever
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret unlocker select <unlocker-id>`
 | 
					#### `secret unlocker select <unlocker-id>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Selects an unlocker as the current default for operations.
 | 
					Selects an unlocker as the current default for operations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Import Operations
 | 
					### Import Operations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret import <secret-name> --source <filename>`
 | 
					#### `secret import <secret-name> --source <filename>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Imports a secret from a file and stores it in the current vault under the given name.
 | 
					Imports a secret from a file and stores it in the current vault under the given name.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret vault import [vault-name]`
 | 
					#### `secret vault import [vault-name]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Imports a mnemonic phrase into the specified vault (defaults to "default").
 | 
					Imports a mnemonic phrase into the specified vault (defaults to "default").
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Encryption Operations
 | 
					### Encryption Operations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret encrypt <secret-name> [--input=file] [--output=file]`
 | 
					#### `secret encrypt <secret-name> [--input=file] [--output=file]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Encrypts data using an Age key stored as a secret. If the secret doesn't exist, generates a new Age key.
 | 
					Encrypts data using an Age key stored as a secret. If the secret doesn't exist, generates a new Age key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `secret decrypt <secret-name> [--input=file] [--output=file]`
 | 
					#### `secret decrypt <secret-name> [--input=file] [--output=file]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Decrypts data using an Age key stored as a secret.
 | 
					Decrypts data using an Age key stored as a secret.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Storage Architecture
 | 
					## Storage Architecture
 | 
				
			||||||
@ -215,12 +261,13 @@ Decrypts data using an Age key stored as a secret.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Key Management and Encryption Flow
 | 
					### Key Management and Encryption Flow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Long-term Keys
 | 
					#### 1: Long-term Keys
 | 
				
			||||||
- **Source**: Derived from BIP39 mnemonic phrases using hierarchical deterministic (HD) key derivation
 | 
					- **Source**: Derived from BIP39 mnemonic phrases using hierarchical deterministic (HD) key derivation
 | 
				
			||||||
- **Purpose**: Master keys for each vault, used to encrypt secret-specific keys
 | 
					- **Purpose**: Master keys for each vault, used to encrypt secret-specific keys
 | 
				
			||||||
- **Storage**: Public key stored as `pub.age`, private key encrypted by unlockers
 | 
					- **Storage**: Public key stored as `pub.age`, private key encrypted by unlockers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Unlockers
 | 
					#### 2: Unlockers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Unlockers provide different authentication methods to access the long-term keys:
 | 
					Unlockers provide different authentication methods to access the long-term keys:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. **Passphrase Unlockers**:
 | 
					1. **Passphrase Unlockers**:
 | 
				
			||||||
@ -247,8 +294,9 @@ Unlockers provide different authentication methods to access the long-term keys:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Each vault maintains its own set of unlockers and one long-term key. The long-term key is encrypted to each unlocker, allowing any authorized unlocker to access vault secrets.
 | 
					Each vault maintains its own set of unlockers and one long-term key. The long-term key is encrypted to each unlocker, allowing any authorized unlocker to access vault secrets.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Secret-specific Keys
 | 
					#### 3: Secret-specific Keys
 | 
				
			||||||
- Each secret has its own encryption key pair
 | 
					
 | 
				
			||||||
 | 
					- Each secret version has its own encryption key pair
 | 
				
			||||||
- Private key encrypted to the vault's long-term key
 | 
					- Private key encrypted to the vault's long-term key
 | 
				
			||||||
- Provides forward secrecy and granular access control
 | 
					- Provides forward secrecy and granular access control
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -262,29 +310,33 @@ Each vault maintains its own set of unlockers and one long-term key. The long-te
 | 
				
			|||||||
## Security Features
 | 
					## Security Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Encryption
 | 
					### Encryption
 | 
				
			||||||
- Uses the [Age encryption library](https://age-encryption.org/) with X25519 keys
 | 
					
 | 
				
			||||||
 | 
					- Uses the [age encryption library](https://age-encryption.org/) with X25519 keys
 | 
				
			||||||
- All private keys are encrypted at rest
 | 
					- All private keys are encrypted at rest
 | 
				
			||||||
- No plaintext secrets stored on disk
 | 
					- No plaintext secrets stored on disk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Access Control
 | 
					### Access Control
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Multiple authentication methods supported
 | 
					- Multiple authentication methods supported
 | 
				
			||||||
- Hierarchical key architecture provides defense in depth
 | 
					 | 
				
			||||||
- Vault isolation prevents cross-contamination
 | 
					- Vault isolation prevents cross-contamination
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Forward Secrecy
 | 
					### Forward Secrecy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Per-version encryption keys limit exposure if compromised
 | 
					- Per-version encryption keys limit exposure if compromised
 | 
				
			||||||
- Each version is independently encrypted
 | 
					- Each version is independently encrypted
 | 
				
			||||||
- Long-term keys protected by multiple unlocker layers
 | 
					 | 
				
			||||||
- Historical versions remain encrypted with their original keys
 | 
					- Historical versions remain encrypted with their original keys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Hardware Integration
 | 
					### Hardware Integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Hardware token support via PGP/GPG integration
 | 
					- Hardware token support via PGP/GPG integration
 | 
				
			||||||
- macOS Keychain integration for system-level security
 | 
					- macOS Keychain integration for system-level security
 | 
				
			||||||
- Secure Enclave support planned (requires Apple Developer Program)
 | 
					- Secure Enclave support planned (requires paid Apple Developer Program for
 | 
				
			||||||
 | 
					  signed entitlements to access the SEP and doxxing myself to Apple)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Examples
 | 
					## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Basic Workflow
 | 
					### Basic Workflow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
# Initialize with a new mnemonic
 | 
					# Initialize with a new mnemonic
 | 
				
			||||||
secret generate mnemonic  # Copy the output
 | 
					secret generate mnemonic  # Copy the output
 | 
				
			||||||
@ -305,6 +357,7 @@ secret remove ssh/servers/web01
 | 
				
			|||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Multi-vault Setup
 | 
					### Multi-vault Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
# Create separate vaults for different contexts
 | 
					# Create separate vaults for different contexts
 | 
				
			||||||
secret vault create work
 | 
					secret vault create work
 | 
				
			||||||
@ -344,6 +397,7 @@ secret unlocker remove <unlocker-id>
 | 
				
			|||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Version Management
 | 
					### Version Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
# List all versions of a secret
 | 
					# List all versions of a secret
 | 
				
			||||||
secret version list database/prod/password
 | 
					secret version list database/prod/password
 | 
				
			||||||
@ -356,6 +410,7 @@ secret version remove database/prod/password 20231214.001
 | 
				
			|||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Encryption/Decryption with Age Keys
 | 
					### Encryption/Decryption with Age Keys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
# Generate an Age key and store it as a secret
 | 
					# Generate an Age key and store it as a secret
 | 
				
			||||||
secret generate secret encryption/mykey
 | 
					secret generate secret encryption/mykey
 | 
				
			||||||
@ -372,33 +427,35 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
 | 
				
			|||||||
### Cryptographic Primitives
 | 
					### Cryptographic Primitives
 | 
				
			||||||
- **Key Derivation**: BIP32/BIP39 hierarchical deterministic key derivation
 | 
					- **Key Derivation**: BIP32/BIP39 hierarchical deterministic key derivation
 | 
				
			||||||
- **Encryption**: Age (X25519 + ChaCha20-Poly1305)
 | 
					- **Encryption**: Age (X25519 + ChaCha20-Poly1305)
 | 
				
			||||||
- **Key Exchange**: X25519 elliptic curve Diffie-Hellman
 | 
					 | 
				
			||||||
- **Authentication**: Poly1305 MAC
 | 
					- **Authentication**: Poly1305 MAC
 | 
				
			||||||
- **Hashing**: Double SHA-256 for public key identification
 | 
					- **Hashing**: Double SHA-256 for public key identification
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### File Formats
 | 
					### File Formats
 | 
				
			||||||
- **Age Files**: Standard Age encryption format (.age extension)
 | 
					- **age Files**: Standard age encryption format (.age extension)
 | 
				
			||||||
- **Metadata**: Unencrypted JSON format with timestamps and type information
 | 
					- **Metadata**: Unencrypted JSON format with timestamps and type information
 | 
				
			||||||
- **Vault Metadata**: JSON containing vault name, creation time, derivation index, and public key hash
 | 
					- **Vault Metadata**: JSON containing vault name, creation time, derivation index, and public key hash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Vault Management
 | 
					### Vault Management
 | 
				
			||||||
- **Derivation Index**: Each vault uses a unique derivation index from the mnemonic
 | 
					
 | 
				
			||||||
 | 
					- **Derivation Index**: Each vault uses a unique derivation index from the mnemonic, and thus a unique key pair
 | 
				
			||||||
- **Public Key Hash**: Double SHA-256 hash of the index-0 public key identifies vaults from the same mnemonic
 | 
					- **Public Key Hash**: Double SHA-256 hash of the index-0 public key identifies vaults from the same mnemonic
 | 
				
			||||||
- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived
 | 
					- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Cross-Platform Support
 | 
					### Cross-Platform Support
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- **macOS**: Full support including Keychain and planned Secure Enclave integration
 | 
					- **macOS**: Full support including Keychain and planned Secure Enclave integration
 | 
				
			||||||
- **Linux**: Full support (excluding macOS-specific features)
 | 
					- **Linux**: Full support (excluding macOS-specific features)
 | 
				
			||||||
- **Windows**: Basic support (filesystem operations only)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Security Considerations
 | 
					## Security Considerations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Threat Model
 | 
					### Threat Model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Protects against unauthorized access to secret values
 | 
					- Protects against unauthorized access to secret values
 | 
				
			||||||
- Provides defense against compromise of individual components
 | 
					- Provides defense against compromise of individual components
 | 
				
			||||||
- Supports hardware-backed authentication where available
 | 
					- Supports hardware-backed authentication where available
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Best Practices
 | 
					### Best Practices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Use strong, unique passphrases for unlockers
 | 
					1. Use strong, unique passphrases for unlockers
 | 
				
			||||||
2. Enable hardware authentication (Keychain, hardware tokens) when available
 | 
					2. Enable hardware authentication (Keychain, hardware tokens) when available
 | 
				
			||||||
3. Regularly audit unlockers and remove unused ones
 | 
					3. Regularly audit unlockers and remove unused ones
 | 
				
			||||||
@ -406,6 +463,7 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
 | 
				
			|||||||
5. Use separate vaults for different security contexts
 | 
					5. Use separate vaults for different security contexts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Limitations
 | 
					### Limitations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Requires access to unlockers for secret retrieval
 | 
					- Requires access to unlockers for secret retrieval
 | 
				
			||||||
- Mnemonic phrases must be securely stored and backed up
 | 
					- Mnemonic phrases must be securely stored and backed up
 | 
				
			||||||
- Hardware features limited to supported platforms
 | 
					- Hardware features limited to supported platforms
 | 
				
			||||||
@ -437,9 +495,11 @@ go test -tags=integration -v ./internal/cli  # Integration tests
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Author
 | 
					# Author
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Made with love and lots of expensive SOTA AI by [sneak](https://sneak.berlin) in Berlin in the summer of 2025.
 | 
					Made with love and lots of expensive SOTA AI by
 | 
				
			||||||
 | 
					[sneak](https://sneak.berlin) in Berlin in the summer of 2025.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Released as a free software gift to the world, no strings attached, under the [WTFPL](https://www.wtfpl.net/) license.
 | 
					Released as a free software gift to the world, no strings attached, under
 | 
				
			||||||
 | 
					the [WTFPL](https://www.wtfpl.net/) license.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Contact: [sneak@sneak.berlin](mailto:sneak@sneak.berlin)
 | 
					Contact: [sneak@sneak.berlin](mailto:sneak@sneak.berlin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@
 | 
				
			|||||||
package cli
 | 
					package cli
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.eeqj.de/sneak/secret/internal/secret"
 | 
						"git.eeqj.de/sneak/secret/internal/secret"
 | 
				
			||||||
	"github.com/spf13/afero"
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
@ -57,3 +59,8 @@ func (cli *Instance) SetStateDir(stateDir string) {
 | 
				
			|||||||
func (cli *Instance) GetStateDir() string {
 | 
					func (cli *Instance) GetStateDir() string {
 | 
				
			||||||
	return cli.stateDir
 | 
						return cli.stateDir
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Print outputs to the command's configured output writer
 | 
				
			||||||
 | 
					func (cli *Instance) Print(a ...interface{}) (n int, err error) {
 | 
				
			||||||
 | 
						return fmt.Fprint(cli.cmd.OutOrStdout(), a...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -278,6 +278,9 @@ func (cli *Instance) GetSecret(cmd *cobra.Command, secretName string) error {
 | 
				
			|||||||
func (cli *Instance) GetSecretWithVersion(cmd *cobra.Command, secretName string, version string) error {
 | 
					func (cli *Instance) GetSecretWithVersion(cmd *cobra.Command, secretName string, version string) error {
 | 
				
			||||||
	secret.Debug("GetSecretWithVersion called", "secretName", secretName, "version", version)
 | 
						secret.Debug("GetSecretWithVersion called", "secretName", secretName, "version", version)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Store the command for output
 | 
				
			||||||
 | 
						cli.cmd = cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get current vault
 | 
						// Get current vault
 | 
				
			||||||
	vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
 | 
						vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -302,8 +305,8 @@ func (cli *Instance) GetSecretWithVersion(cmd *cobra.Command, secretName string,
 | 
				
			|||||||
	secret.Debug("Got secret value", "valueLength", len(value))
 | 
						secret.Debug("Got secret value", "valueLength", len(value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Print the secret value to stdout
 | 
						// Print the secret value to stdout
 | 
				
			||||||
	cmd.Print(string(value))
 | 
						_, _ = cli.Print(string(value))
 | 
				
			||||||
	secret.Debug("Printed value to cmd")
 | 
						secret.Debug("Printed value to stdout")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Debug: Log what we're actually printing
 | 
						// Debug: Log what we're actually printing
 | 
				
			||||||
	secret.Debug("Secret retrieval debug info",
 | 
						secret.Debug("Secret retrieval debug info",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										72
									
								
								internal/cli/stdout_stderr_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								internal/cli/stdout_stderr_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					package cli_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestGetCommandOutputsToStdout tests that 'secret get' outputs the secret value to stdout, not stderr
 | 
				
			||||||
 | 
					func TestGetCommandOutputsToStdout(t *testing.T) {
 | 
				
			||||||
 | 
						// Create a temporary directory for our vault
 | 
				
			||||||
 | 
						tempDir := t.TempDir()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set environment variables for the test
 | 
				
			||||||
 | 
						t.Setenv("SB_SECRET_STATE_DIR", tempDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Find the secret binary path
 | 
				
			||||||
 | 
						wd, err := filepath.Abs("../..")
 | 
				
			||||||
 | 
						require.NoError(t, err, "should get working directory")
 | 
				
			||||||
 | 
						secretPath := filepath.Join(wd, "secret")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
 | 
				
			||||||
 | 
						testPassphrase := "test-passphrase"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize vault
 | 
				
			||||||
 | 
						cmd := exec.Command(secretPath, "init")
 | 
				
			||||||
 | 
						cmd.Env = []string{
 | 
				
			||||||
 | 
							"SB_SECRET_STATE_DIR=" + tempDir,
 | 
				
			||||||
 | 
							"SB_SECRET_MNEMONIC=" + testMnemonic,
 | 
				
			||||||
 | 
							"SB_UNLOCK_PASSPHRASE=" + testPassphrase,
 | 
				
			||||||
 | 
							"PATH=" + "/usr/bin:/bin",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						output, err := cmd.CombinedOutput()
 | 
				
			||||||
 | 
						require.NoError(t, err, "init should succeed: %s", string(output))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add a secret
 | 
				
			||||||
 | 
						cmd = exec.Command(secretPath, "add", "test/secret")
 | 
				
			||||||
 | 
						cmd.Env = []string{
 | 
				
			||||||
 | 
							"SB_SECRET_STATE_DIR=" + tempDir,
 | 
				
			||||||
 | 
							"SB_SECRET_MNEMONIC=" + testMnemonic,
 | 
				
			||||||
 | 
							"PATH=" + "/usr/bin:/bin",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						cmd.Stdin = strings.NewReader("test-secret-value")
 | 
				
			||||||
 | 
						output, err = cmd.CombinedOutput()
 | 
				
			||||||
 | 
						require.NoError(t, err, "add should succeed: %s", string(output))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test that 'secret get' outputs to stdout, not stderr
 | 
				
			||||||
 | 
						cmd = exec.Command(secretPath, "get", "test/secret")
 | 
				
			||||||
 | 
						cmd.Env = []string{
 | 
				
			||||||
 | 
							"SB_SECRET_STATE_DIR=" + tempDir,
 | 
				
			||||||
 | 
							"SB_SECRET_MNEMONIC=" + testMnemonic,
 | 
				
			||||||
 | 
							"PATH=" + "/usr/bin:/bin",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var stdout, stderr bytes.Buffer
 | 
				
			||||||
 | 
						cmd.Stdout = &stdout
 | 
				
			||||||
 | 
						cmd.Stderr = &stderr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = cmd.Run()
 | 
				
			||||||
 | 
						require.NoError(t, err, "get should succeed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The secret value should be in stdout
 | 
				
			||||||
 | 
						assert.Equal(t, "test-secret-value", strings.TrimSpace(stdout.String()), "secret value should be in stdout")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Nothing should be in stderr
 | 
				
			||||||
 | 
						assert.Empty(t, stderr.String(), "stderr should be empty")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user