Sharing API
Endpoints for vault sharing, invitations, and shared vault access.
Overview
Vault sharing uses ECDH key exchange:
- Owner invites user by email
- Owner's device encrypts vault key with recipient's public key
- Recipient accepts and decrypts vault key
- Both users can access shared vault
Share a Vault
Create Share Invitation
POST /vault/:name/share
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "recipient@example.com",
"role": "write",
"wrappedKeyForRecipient": "base64-ecdh-wrapped-key"
}| Field | Type | Description |
|---|---|---|
email | string | Recipient's email address |
role | string | Access level: admin, write, or read |
wrappedKeyForRecipient | string | Vault key encrypted with ECDH shared secret |
{
"invitationId": "invitation-uuid",
"status": "pending",
"createdAt": "2024-01-15T12:00:00Z"
}Invitations
List Invitations
GET /invitations
Authorization: Bearer <token>{
"invitations": [
{
"id": "invitation-uuid",
"vaultName": "work-passwords",
"ownerEmail": "owner@example.com",
"role": "write",
"status": "pending",
"createdAt": "2024-01-15T12:00:00Z"
}
]
}Get Invitation Details
GET /invitations/:id
Authorization: Bearer <token>{
"id": "invitation-uuid",
"vaultName": "work-passwords",
"ownerEmail": "owner@example.com",
"ownerId": "owner-uuid",
"role": "write",
"status": "pending",
"wrappedKey": "base64-ecdh-wrapped-key",
"createdAt": "2024-01-15T12:00:00Z"
}Accept Invitation
POST /invitations/:id/accept
Authorization: Bearer <token>{
"success": true,
"vaultName": "work-passwords",
"ownerId": "owner-uuid",
"role": "write"
}Shared Vaults
List Shared Vaults
GET /shared
Authorization: Bearer <token>{
"sharedVaults": [
{
"name": "work-passwords",
"ownerId": "owner-uuid",
"ownerEmail": "owner@example.com",
"role": "write",
"version": 8,
"updatedAt": "2024-01-14T10:00:00Z"
},
{
"name": "family-vault",
"ownerId": "owner-uuid-2",
"ownerEmail": "family@example.com",
"role": "read",
"version": 3,
"updatedAt": "2024-01-13T15:30:00Z"
}
]
}Get Shared Vault
GET /shared/:ownerId/:name
Authorization: Bearer <token>{
"encryptedData": "base64-encrypted-entries",
"iv": "base64-iv",
"wrappedKeyForUser": "base64-ecdh-wrapped-key",
"role": "write",
"version": 8,
"updatedAt": "2024-01-14T10:00:00Z"
}Update Shared Vault
Requires write or admin role:
PUT /shared/:ownerId/:name
Authorization: Bearer <token>
Content-Type: application/json
{
"encryptedData": "base64-new-encrypted-entries",
"iv": "base64-new-iv",
"version": 8
}{
"version": 9,
"updatedAt": "2024-01-15T12:30:00Z"
}Access Roles
| Role | Read | Write | Delete | Share | Manage Users |
|---|---|---|---|---|---|
read | ✅ | ❌ | ❌ | ❌ | ❌ |
write | ✅ | ✅ | ❌ | ❌ | ❌ |
admin | ✅ | ✅ | ✅ | ✅ | ✅ |
Invitation States
| State | Description |
|---|---|
pending | Waiting for recipient to accept |
accepted | Recipient has accepted |
revoked | Owner revoked the invitation |
expired | TTL exceeded (7 days default) |
ECDH Key Wrapping
Client-Side Share Process
import { deriveECDHSecret, wrapKeyWithSecret } from "@pwm/shared";
// 1. Get recipient's public key
const { publicKey: recipientPublicKey } = await api.auth.user[":email"]["public-key"].$get({
param: { email: recipientEmail }
});
// 2. Derive shared secret using ECDH
const sharedSecret = await deriveECDHSecret(
myPrivateKey,
recipientPublicKey
);
// 3. Wrap vault key with shared secret
const wrappedKey = await wrapKeyWithSecret(vaultKey, sharedSecret);
// 4. Create invitation
await api.vault[":name"].share.$post({
param: { name: vaultName },
json: {
email: recipientEmail,
role: "write",
wrappedKeyForRecipient: wrappedKey
}
});Client-Side Accept Process
// 1. Get invitation with wrapped key
const invitation = await api.invitations[":id"].$get({
param: { id: invitationId }
});
// 2. Get owner's public key
const { publicKey: ownerPublicKey } = await api.auth.user[":email"]["public-key"].$get({
param: { email: invitation.ownerEmail }
});
// 3. Derive same shared secret
const sharedSecret = await deriveECDHSecret(
myPrivateKey,
ownerPublicKey
);
// 4. Unwrap vault key
const vaultKey = await unwrapKeyWithSecret(
invitation.wrappedKey,
sharedSecret
);
// 5. Accept invitation
await api.invitations[":id"].accept.$post({
param: { id: invitationId }
});
// 6. Now can decrypt shared vault
const sharedVault = await api.shared[":ownerId"][":name"].$get({
param: { ownerId: invitation.ownerId, name: invitation.vaultName }
});Error Responses
User Not Found
{
"error": "User not found",
"code": "USER_NOT_FOUND"
}Invitation Not Found
{
"error": "Invitation not found",
"code": "NOT_FOUND"
}Insufficient Permissions
{
"error": "Insufficient permissions",
"code": "FORBIDDEN"
}Already Shared
{
"error": "Vault already shared with this user",
"code": "ALREADY_SHARED"
}Related
- Security: ECDH Sharing - Cryptographic details
- CLI: Vault Sharing - Command-line sharing
- Vaults API - Owned vault operations