> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.konfidant.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.konfidant.app/_mcp/server.

# Zero-Knowledge Encryption

> How Konfidant encrypts on the client, what the server can and cannot see, and the KNF1 format

## How it works

Konfidant encrypts every share **on the sender's device**, before anything is sent to the API. The API stores and
delivers ciphertext only.

1. The client generates a fresh random 256-bit key for the share and encrypts the content with AES-256-GCM in the
   [KNF1 format](#the-knf1-format). For files, the file name and MIME type are encrypted together with the content.
2. The client sends only the ciphertext to the API (`POST /api/v1/texts`, or the three-step file flow). The API
   returns a `download_url` that carries a server-issued, single-use token in the URL fragment: `#t=<token>`.
3. The client appends the key to the fragment and sends the resulting **share link** to the recipient:

```
https://download.konfidant.app/#t=<urlencoded token>&k=<base64url key>
```

4. The recipient opens the link. The page reads the fragment, sends **only the token** to Konfidant, receives the
   ciphertext once and decrypts it locally. The ciphertext is deleted after it has been delivered.

The URL fragment (everything after `#`) is never sent to a server by browsers, so the key never reaches Konfidant.
Organizations with a custom download domain receive links on that domain instead of `download.konfidant.app`.

> **Warning**
>
> The `download_url` returned by the API is **not** a working share link on its own. Without `&k=<key>` the recipient
> cannot decrypt the content. Conversely, the full share link is the secret: anyone holding it can open the share once.
> Do not log it.

## What Konfidant can and cannot see

| Konfidant never receives           | Konfidant does learn                                 |
| ---------------------------------- | ---------------------------------------------------- |
| The plaintext text or file content | The ciphertext size (roughly the content size)       |
| The file name and MIME type        | When the share was created, by which API key or user |
| The decryption key                 | When the share was opened, and when it expires       |

Consequences:

* **Lost links cannot be recovered**, not even by Konfidant support: there is no copy of the key.
* The single-use token is issued by Konfidant and is not a secret from Konfidant. It only controls access to the
  ciphertext; the key is what protects the content.
* **Web clients trust the JavaScript served by Konfidant.** When you share or open a link in the browser, the
  encryption code is delivered by Konfidant, so a compromised or malicious deployment could in principle serve code
  that leaks keys. Download pages load no third-party scripts and send a strict Content Security Policy to reduce
  this risk. If this matters for your threat model, create shares with an official SDK, whose code you install and
  can review, and open them with the SDK's `openShare` method.

## Use an official SDK

The official SDKs implement KNF1, the upload flow and link handling, and are tested against the official KNF1 test
vectors. We strongly recommend them over raw HTTP calls.

| Language              | Package                       | Share text   | Share file   | Open a link  |
| --------------------- | ----------------------------- | ------------ | ------------ | ------------ |
| JavaScript/TypeScript | `@konfidant/sdk`              | `shareText`  | `shareFile`  | `openShare`  |
| Go                    | `github.com/konfidant/sdk-go` | `ShareText`  | `ShareFile`  | `OpenShare`  |
| Python                | `konfidant`                   | `share_text` | `share_file` | `open_share` |
| Ruby                  | `konfidant`                   | `share_text` | `share_file` | `open_share` |

**`TypeScript`**

```ts title="TypeScript"
import { KonfidantClient } from '@konfidant/sdk';

const client = new KonfidantClient({ apiKey: process.env.KONFIDANT_API_KEY! });

const { shareUrl } = await client.shareText('db-password: hunter2', { ttlHours: 24 });
// https://download.konfidant.app/#t=<token>&k=<key>
```

**`Go`**

```go title="Go"
client, err := konfidant.New(konfidant.ClientOptions{APIKey: os.Getenv("KONFIDANT_API_KEY")})
if err != nil {
    log.Fatal(err)
}

result, err := client.ShareText(ctx, "db-password: hunter2", konfidant.ShareOptions{TTLHours: 24})
if err != nil {
    log.Fatal(err)
}
fmt.Println(result.ShareURL)
```

**`Python`**

```python title="Python"
from konfidant import KonfidantClient

with KonfidantClient(api_key="your-api-key") as client:
    result = client.share_text("db-password: hunter2", ttl_hours=24)
    print(result.share_url)
```

**`Ruby`**

```ruby title="Ruby"
require 'konfidant'

client = Konfidant::Client.new(api_key: ENV['KONFIDANT_API_KEY'])
puts client.share_text(text: 'db-password: hunter2', ttl_hours: 24).share_url
```

## Calling the API without an SDK

The API does not accept plaintext. A raw HTTP client must implement KNF1 itself:

1. Generate a 32-byte key with a cryptographically secure random generator. Never reuse a key.
2. Encrypt the content as KNF1 (below).
3. Create the share:
   * **Text:** `POST /api/v1/texts` with `{"ciphertext": "<standard base64 of the KNF1 bytes>", "ttl_hours": 24}`.
   * **File:** follow [Sharing an Encrypted File](https://docs.konfidant.app/documentation/sharing-an-encrypted-file).
4. Build the share link: `download_url + "&k=" + base64url(key)`, where the key is unpadded base64url
   (43 characters). Send the key only in the fragment, never in a request body, query string or header.

`ttl_hours` is optional; when omitted, the share uses the maximum lifetime allowed by your plan.

### Opening a link

To open a share link programmatically, take the origin of the link and send the token from the fragment:

```sh
curl --request POST 'https://download.konfidant.app/api/download' \
  --header 'Content-Type: application/json' \
  --data '{"t": "<token from #t=, URL-decoded>"}' \
  --output share.knf1
```

The response is the KNF1 ciphertext (`application/octet-stream`), or `410` if the share was already opened or has
expired. Opening a share consumes it. Decrypt the response locally with the key from `&k=`. No API key is needed.

## The KNF1 format

KNF1 is a chunked AES-256-GCM format (the STREAM construction): every chunk has its own nonce, which prevents
reordering, duplication and truncation of chunks.

```
ciphertext = header (16 bytes) || sealed_chunk_0 || sealed_chunk_1 || ... || sealed_chunk_n
```

### Header

The header is plaintext and is authenticated as additional data (AAD) of every chunk.

| Offset | Size | Field          | Value                                                                                      |
| ------ | ---- | -------------- | ------------------------------------------------------------------------------------------ |
| 0      | 4    | `magic`        | ASCII `KNF1` (`4B 4E 46 31`)                                                               |
| 4      | 4    | `chunk_size`   | uint32 big-endian, plaintext bytes per chunk. Default 1 048 576; valid 4 096 to 16 777 216 |
| 8      | 7    | `nonce_prefix` | 7 random bytes, fresh per share                                                            |
| 15     | 1    | `reserved`     | `00`                                                                                       |

### Plaintext stream

```
stream = uint32_be(len(meta)) || meta || content
meta   = kind (1 byte) || uint16_be(len(name)) || name || uint16_be(len(mime)) || mime
```

* `kind`: `01` for text, `02` for file.
* `name`: UTF-8 file name, at most 1 024 bytes. Empty for text.
* `mime`: UTF-8 MIME type, at most 255 bytes. Empty for text; may be empty for files.
* `content`: the raw file bytes, or the UTF-8 text.

### Chunks

Split `stream` into chunks of `chunk_size` bytes; the last chunk has 1 to `chunk_size` bytes. With `N` chunks,
for chunk index `i` (0-based):

```
nonce_i        = nonce_prefix (7) || uint32_be(i) (4) || last_flag (1)    ; last_flag = 01 if i == N-1, else 00
sealed_chunk_i = AES-256-GCM-Encrypt(key, nonce_i, plaintext = chunk_i, aad = header)   ; ciphertext || 16-byte tag
```

### Size

```
S    = 4 + len(meta) + len(content)
size = 16 + S + 16 * ceil(S / chunk_size)
```

For files, `size` is the `ciphertext_size` you declare in `POST /api/v1/files`. The plan's file size limit is
applied to the content; the server allows for the KNF1 overhead.

### Decryption

1. Check `magic`, `reserved == 0` and that `chunk_size` is in range. Reject anything else.
2. Read sealed chunks of `chunk_size + 16` bytes; the final sealed chunk is the remainder (17 to `chunk_size + 16`
   bytes) and is opened with `last_flag = 01`.
3. Treat any authentication failure as fatal and discard all output: it means a wrong key, or modified, reordered
   or truncated data.
4. Parse `meta` from the start of the decrypted stream and reject unknown `kind` values or inconsistent lengths.
   The remaining bytes are the content.

> **Note**
>
> Test your implementation against the official KNF1 test vectors, which the official SDKs reproduce byte for byte.
> If you are not sure, use an SDK.