# corteq-onepassword
This is a 1Password SDK wrapper for Rust applications. This does NOT use the 1Password CLI!
Providing a safe interface to 1Password secrets using FFI bindings for the official 1Password SDK Core library.
## Features
- **Secure by default** - Secrets wrapped in `SecretString` with automatic memory zeroization
- **Simple API** - Retrieve secrets with a single function call
- **Thread-safe** - Client is `Send + Sync` for use in async applications
- **Builder pattern** - Flexible configuration with sensible defaults
- **Type-safe** - Compile-time guarantees for secret handling
## Quick Start
```rust
use corteq_onepassword::{OnePassword, ExposeSecret};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client from OP_SERVICE_ACCOUNT_TOKEN environment variable
let client = OnePassword::from_env()?
.integration("my-app", "1.0.0") // Name of your app for audit purposes on 1password side
.connect()
.await?;
// Resolve a secret
let api_key = client.secret("op://vault/item/api-key").await?;
// Use the secret (expose only when needed)
println!("API key length: {}", api_key.expose_secret().len());
Ok(())
}
```
Refer to the [Implementation Guide](docs/IMPLEMENTATION_GUIDE.md) for detailed instructions.
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
corteq-onepassword = "0.1"
```
## Installing from crates.io
When you install this crate from crates.io, the native library is **not** included
(due to crates.io's 10MB size limit). The library is automatically downloaded during
`cargo build`:
1. **First build** - Downloads the library from PyPI (~15-18MB)
2. **Subsequent builds** - Uses the cached library in your target directory
### Requirements
- Network access during first build to:
- `pypi.org` (package metadata)
- `files.pythonhosted.org` (library download)
### Offline Builds
For environments without network access:
1. Download the library on a connected machine:
```bash
curl -L "https://pypi.org/pypi/onepassword-sdk/json" | \
jq -r '.urls[] | select(.filename | contains("manylinux")) | .url' | \
head -1 | xargs curl -L -o sdk.whl
unzip sdk.whl "onepassword/*.so" -d extracted/
```
2. Set the library path before building:
```bash
export ONEPASSWORD_LIB_PATH="/path/to/libop_uniffi_core.so"
cargo build
```
## Authentication
This crate uses 1Password service account tokens. Personal account tokens are not supported.
### Environment Variable (Recommended)
```bash
export OP_SERVICE_ACCOUNT_TOKEN="ops_..."
```
```rust
let client = OnePassword::from_env()?.connect().await?;
```
or use `dotenvy`
```rust
dotenvy::dotenv().ok();
```
### Explicit Token (Not recommended for production use!)
```rust
let client = OnePassword::from_token("ops_...").connect().await?;
```
## Secret References
Secrets are referenced using the `op://vault/item/field` format:
- `op://Production/Database/password` - Simple reference
- `op://Production/Database/admin/password` - Section-scoped reference
See https://developer.1password.com/docs/cli/secret-reference-syntax/
## API
### Single Secret
```rust
let api_key = client.secret("op://prod/stripe/api-key").await?;
```
### Batch Resolution
```rust
let secrets = client.secrets(&[
"op://prod/db/host",
"op://prod/db/user",
"op://prod/db/pass",
]).await?;
let host = secrets[0].expose_secret();
let user = secrets[1].expose_secret();
let pass = secrets[2].expose_secret();
```
### Named Resolution
```rust
let secrets = client.secrets_named(&[
("host", "op://prod/db/host"),
("user", "op://prod/db/user"),
("pass", "op://prod/db/pass"),
]).await?;
let host = secrets.get("host").unwrap().expose_secret();
```
## Sharing the Client
The client is thread-safe and can be shared via `Arc`:
```rust
use std::sync::Arc;
let client = Arc::new(OnePassword::from_env()?.connect().await?);
let client1 = Arc::clone(&client);
let client2 = Arc::clone(&client);
tokio::join!(
async move { client1.secret("op://vault/item/field1").await },
async move { client2.secret("op://vault/item/field2").await },
);
```
## Feature Flags
- `blocking` - Enable synchronous API via `connect_blocking()`
- `tracing` - Enable tracing spans for observability
```toml
[dependencies]
corteq-onepassword = { version = "0.1", features = ["blocking"] }
```
## Platform Support
| Linux | x86_64 | ✅ Supported |
| Linux | aarch64 | ✅ Supported |
| macOS | x86_64 | ✅ Supported |
| macOS | aarch64 | ✅ Supported |
| Windows | - | ❌ Not supported |
| Alpine | - | ❌ Not supported (musl) |
## Build Process
The build script looks for the 1Password SDK native library in this order:
1. **`ONEPASSWORD_LIB_PATH`** - Custom path via environment variable
2. **Bundled libraries** - Pre-downloaded in `src/libs/{platform}/`
3. **PyPI download** - Automatic download at build time (requires network)
### Bundled Libraries (Git LFS)
This repository includes pre-downloaded libraries for all supported platforms in `src/libs/`:
```
src/libs/
├── linux-x86_64/libop_uniffi_core.so (~18MB)
├── linux-aarch64/libop_uniffi_core.so (~17MB)
├── macos-x86_64/libop_uniffi_core.dylib (~16MB)
└── macos-aarch64/libop_uniffi_core.dylib (~15MB)
```
These files are tracked with **Git LFS** due to their size. After cloning:
```bash
git lfs pull # Download the actual library files
```
**Why bundle libraries?**
- **crates.io size limit**: crates.io enforces a 10MB limit per crate, so we can't include libraries there
- **Offline builds**: No network access required when using bundled libraries
- **Build reproducibility**: Known library versions with verified checksums
### Refreshing Bundled Libraries
To update the bundled libraries (e.g., for a new SDK version):
```bash
./scripts/download-libs.sh
```
This script fetches all 4 platform libraries from PyPI with SHA256 verification.
### PyPI Fallback
If bundled libraries are not found, the build script downloads from PyPI:
1. Fetches metadata from PyPI's JSON API
2. Downloads the appropriate wheel for your target platform
3. Verifies the SHA256 checksum
4. Extracts the native library
### Network Requirements
When downloading from PyPI, network access is required to:
- `pypi.org` - Package metadata and checksums
- `files.pythonhosted.org` - Library downloads
### Custom Library Path
For custom library locations:
```bash
export ONEPASSWORD_LIB_PATH="/path/to/libop_uniffi_core.so"
```
## Security
- Tokens wrapped in `SecretString` and zeroized on drop
- Secrets never appear in logs or error messages
- Debug implementations redact sensitive data
- Native library verified via SHA256 checksum at build time
## Error Handling
All errors are typed and implement `std::error::Error`:
```rust
use corteq_onepassword::Error;
match client.secret("op://vault/item/field").await {
Ok(secret) => { /* use secret */ },
Err(Error::SecretNotFound { reference }) => {
eprintln!("Secret not found: {}", reference);
},
Err(Error::AccessDenied { vault }) => {
eprintln!("Access denied to vault: {}", vault);
},
Err(e) => {
eprintln!("Error: {}", e);
}
}
```
## Troubleshooting
### "Could not find libop_uniffi_core.so"
This error occurs when the native library cannot be located at runtime.
**Solutions:**
1. **Rebuild the crate** - The build script downloads the library automatically:
```bash
cargo clean && cargo build
```
2. **Check network access** - The build script needs to reach PyPI:
```bash
curl -I https://pypi.org/pypi/onepassword-sdk/json
```
3. **Set custom path** - If you have the library elsewhere:
```bash
export ONEPASSWORD_LIB_PATH="/path/to/libop_uniffi_core.so"
```
### Build Script Download Failed
If the automatic download fails during build:
1. Check your network connection
2. Check if PyPI is accessible: `curl https://pypi.org`
3. Try setting `ONEPASSWORD_SKIP_DOWNLOAD=1` and provide the library manually via `ONEPASSWORD_LIB_PATH`