# Encrypt documents for your users

Files are encrypted on the device before they are stored. A fresh AES-256-GCM key encrypts the file, and that AES key is encrypted with the user's RSA public key using RSA-OAEP (key wrapping). The backend stores the ciphertext, wrapped key and initialization vector. To decrypt, SecurySign decrypts the wrapped AES key in its hardware security module (HSM), then the device uses the recovered key to open the file.

## What you need

The examples require:

- A signed-in user's access token from [single sign-on](#/docs/sso). Every call below acts on that user.
- WebCrypto (`crypto.subtle`), available in every modern browser and in Node.js 20 or later.

## 1. Fetch the user's public key

Set `USER_ACCESS_TOKEN` to the access token obtained during sign-in. This request saves the user's public encryption key to `encryption-key.json`, which the Node.js example reads later:

```bash
curl --fail-with-body https://securysign.com/api/pki/encryption-key \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" -o encryption-key.json
```

```json
{
  "keyId": "user_enc_1042",
  "jwk": { "kty": "RSA", "n": "…", "e": "AQAB", "alg": "RSA-OAEP-256", "use": "enc", "key_ops": ["encrypt", "wrapKey"] }
}
```

Keep `keyId` for the storage and decryption requests. The `jwk` field contains the public key in JSON Web Key format and is passed to `encryptFor`.

## 2. Encrypt on the device

The `encryptFor` function imports the public key, creates a 256-bit AES key and a random 12-byte initialization vector (IV), and encrypts the file. It wraps the AES key with RSA-OAEP using SHA-256:

```js
// Chunked, so large files do not overflow the call stack.
function b64(buf) {
  const bytes = new Uint8Array(buf);
  let s = "";
  for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
  return btoa(s);
}

async function encryptFor(jwk, fileBytes) {
  const publicKey = await crypto.subtle.importKey("jwk", jwk, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["wrapKey"]);
  const aesKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const cipherText = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, fileBytes);
  const wrappedKey = await crypto.subtle.wrapKey("raw", aesKey, publicKey, "RSA-OAEP");
  return { cipherTextBase64: b64(cipherText), wrappedKeyBase64: b64(wrappedKey), ivBase64: b64(iv) };
}
```

WebCrypto appends a 16-byte GCM authentication tag to the ciphertext. Store the entire returned value, including that tag.

## 3. Store the encrypted document

Save `encryptFor` and `b64` in `encrypt-file.mjs`, then append the following code. It reads `encryption-key.json` and the document, and writes the storage request to `encrypted-upload.json`:

```javascript
import { readFile, writeFile } from "node:fs/promises";
const { keyId, jwk } = JSON.parse(await readFile("encryption-key.json", "utf8"));
const encrypted = await encryptFor(jwk, await readFile("Contract.pdf"));
await writeFile("encrypted-upload.json", JSON.stringify({
  recipientKeyId: keyId, ...encrypted, documentName: "Contract.pdf", mode: "symmetric"
}));
```

```bash
node encrypt-file.mjs
```

Submit the generated payload from the backend:

```bash
curl --fail-with-body -X POST https://securysign.com/api/encrypt/client \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @encrypted-upload.json -o encrypted-stored.json
```

| Field | Type | Required | Description |
|---|---|---|---|
| `recipientKeyId` | string | Yes | The `keyId` from step 1. |
| `cipherTextBase64` | string | Yes | `cipherTextBase64` from `encryptFor`: the encrypted file with its GCM tag. |
| `wrappedKeyBase64` | string | For `symmetric` | `wrappedKeyBase64` from `encryptFor`. |
| `ivBase64` | string | For `symmetric` | `ivBase64` from `encryptFor`. |
| `documentName` | string | No | The name you show for the document. |
| `mode` | string | No | `symmetric` for a document encrypted as in step 2, or `asymmetric` for a small payload; see [Encrypt small payloads directly](#/docs/api-encryption#encrypt-small-payloads-directly). |

```json
{ "id": 381, "recipientKeyId": "user_enc_1042", "documentName": "Contract.pdf", "status": "stored" }
```

The response's `id` identifies the stored document. `GET /encrypt/list` lists the user's documents and their encryption modes. To retrieve this document, use `GET /encrypt/{id}?mode=symmetric`; the record contains `encrypted_document`, `encrypted_aes_key` and `iv`. To remove it, use `DELETE /encrypt/{id}?mode=symmetric`, which returns `{ "status": "deleted", "id": 381 }`.

## 4. Decrypt

Submit the wrapped key to `POST /decrypt` using the access token of the user who owns the encryption key. In the example below, `encrypted-record.json` contains the full response from `GET /encrypt/{id}?mode=symmetric`. The fields `recipient_key_id`, `encrypted_aes_key`, `encrypted_document` and `iv` provide the inputs for unwrapping and decryption:

```bash
python3 - <<'JSON' > decrypt-key.json
import json
from pathlib import Path
record = json.loads(Path("encrypted-record.json").read_text())
print(json.dumps({
    "encryptedAesKeyBase64": record["encrypted_aes_key"],
    "keyId": record["recipient_key_id"], "mode": "OAEP_SHA256"
}))
JSON
curl --fail-with-body -X POST https://securysign.com/api/decrypt \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @decrypt-key.json -o decrypted-key.json
```

```json
{ "aesKey": "q3XhR7mPz…" }
```

```js
const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));

async function decrypt(cipherTextBase64, ivBase64, aesKeyBase64) {
  const key = await crypto.subtle.importKey("raw", unb64(aesKeyBase64), { name: "AES-GCM" }, false, ["decrypt"]);
  return crypto.subtle.decrypt({ name: "AES-GCM", iv: unb64(ivBase64) }, key, unb64(cipherTextBase64));
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `encryptedAesKeyBase64` | string | Yes | The wrapped AES key: `encrypted_aes_key` from `GET /encrypt/{id}`. |
| `keyId` | string | Yes | The signed-in user's own `keyId` from step 1. |
| `mode` | string | No | `OAEP_SHA256` for keys wrapped as in step 2. Default: `PKCS1`. |

The response contains the raw `aesKey` as base64. Pass it to `decrypt` with `encrypted_document` and `iv` from the stored record to recover the file bytes. Discard the temporary AES key when decryption is complete.

## Encrypt small payloads directly

Payloads of 446 bytes or less can be encrypted directly with the RSA public key using RSA-OAEP and SHA-256. Store them with `mode: "asymmetric"`, `recipientKeyId` and `cipherTextBase64`. To decrypt, send `cipherTextBase64`, `keyId` and `mode: "OAEP_SHA256"` to `POST /decrypt/asymmetric`. The response contains the recovered bytes in `plainText`, base64-encoded.

## Troubleshooting

| Status | Message | Fix |
|---|---|---|
| `400` | `recipientKeyId, cipherTextBase64, wrappedKeyBase64, and ivBase64 required` | Send every field for `symmetric` mode |
| `400` | `encryptedAesKeyBase64 and keyId required` | Send both fields to `/decrypt` |
| `403` | `You can decrypt only with your own encryption key. …` | Send the signed-in user's own `keyId` from `GET /pki/encryption-key` |
| `400` | `Invalid base64 in cipherTextBase64` | Encode the complete ciphertext bytes as base64. |
| `400` | `mode must be prf, symmetric or asymmetric` | Use the mode returned by the document list. |
| `409` | `Two of your documents have this id. Add ?mode= with the mode from the document list (prf, symmetric or asymmetric).` | Include the listed mode in the read or delete request. |
| `404` | `Encryption key not found. Please re-register.` | Call `GET /pki/encryption-key` again to create the user's key; if the error persists, contact SecurySign. |
| `404` | `Document not found` (read) or `Document not found or access denied` (delete) | Check the ID; you can reach only the signed-in user's own documents |
| `429` | `Rate limit exceeded. Please try again later.` | Back off. `/decrypt` allows 30 requests per minute and `/decrypt/asymmetric` 20. |
| `500` | `Decryption failed: send the keyId you encrypted to, and mode OAEP_SHA256 for data wrapped with RSA-OAEP SHA-256` | Send the `keyId` you wrapped the key with, and `mode: "OAEP_SHA256"` |

See the [endpoint schemas](#/docs/api-encryption#endpoints) for the full storage, retrieval and decryption fields, or the [Vault guide](#/docs/encryption) for encryption in the web application.
