Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Sharing API

Endpoints for vault sharing, invitations, and shared vault access.

Overview

Vault sharing uses ECDH key exchange:

  1. Owner invites user by email
  2. Owner's device encrypts vault key with recipient's public key
  3. Recipient accepts and decrypts vault key
  4. 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"
}
Request Body:
FieldTypeDescription
emailstringRecipient's email address
rolestringAccess level: admin, write, or read
wrappedKeyForRecipientstringVault key encrypted with ECDH shared secret
Response:
{
  "invitationId": "invitation-uuid",
  "status": "pending",
  "createdAt": "2024-01-15T12:00:00Z"
}

Invitations

List Invitations

GET /invitations
Authorization: Bearer <token>
Response:
{
  "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>
Response:
{
  "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>
Response:
{
  "success": true,
  "vaultName": "work-passwords",
  "ownerId": "owner-uuid",
  "role": "write"
}

Shared Vaults

List Shared Vaults

GET /shared
Authorization: Bearer <token>
Response:
{
  "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>
Response:
{
  "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
}
Response:
{
  "version": 9,
  "updatedAt": "2024-01-15T12:30:00Z"
}

Access Roles

RoleReadWriteDeleteShareManage Users
read
write
admin

Invitation States

StateDescription
pendingWaiting for recipient to accept
acceptedRecipient has accepted
revokedOwner revoked the invitation
expiredTTL 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