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