# Key export

`export_private_key` removes a key from Kryard's custody — but the plaintext private
key is **never** returned. You supply a recipient public key; the signer decrypts the
key inside its boundary, **HPKE-seals** it to your recipient key, and returns only the
sealed bundle. Only the holder of the matching recipient private key can open it.

::: warning Security-sensitive and gated
Export is disabled by default in the signer (`ALLOW_EXPORT`) — a disabled request
fails with `SIGNER_ERROR` (403). Once exported, the key's `exported` flag is set as
audit metadata. Export still goes through policy + the signer's `evaluatedInputHash`
re-check.
:::

## The flow

1. **Generate a recipient keypair** on the caller side — a P-256 keypair. Keep the
   private key local; it never leaves you.
2. **Submit** `export_private_key` with the recipient **public** key as
   `targetPublicKey`.
3. The signer envelope-decrypts the key, HPKE-seals it to `targetPublicKey`, zeroizes
   the plaintext, and returns the **export bundle**.
4. **HPKE-open** the bundle with your recipient private key to recover the raw key.

## Request

`POST /public/v1/submit/export_private_key`

| Parameter | Type | Notes |
| --- | --- | --- |
| `privateKeyId` _or_ `signWith` | string | The key to export (id, or address via `signWith`). **Required.** |
| `targetPublicKey` | string | Recipient P-256 public key, hex — uncompressed (65-byte `0x04`-prefixed) **or** compressed (33-byte `0x02`/`0x03`). **Required.** |

### Example — SDK

**All-in-one** — the SDK generates an ephemeral P-256 recipient, submits the export,
opens the bundle, and zeroizes the recipient, returning the raw key:

```ts
const { privateKey } = await client.exportPrivateKey({
  signWith: "b3f1…",   // private-key id or address
});
// privateKey: raw key hex (no 0x) — handle securely and discard when done.
```

**Bring your own recipient** — keep the recipient private key under your control and
open the bundle yourself (see [Decrypting the bundle](#decrypting-the-bundle)):

```ts
const { privateKeyId, exportBundle } = await client.exportPrivateKeyBundle({
  signWith: "b3f1…",
  targetPublicKey: recipientPublicKeyHex,  // your P-256 recipient public key, SEC1 hex
});
```

### Example — raw HTTP

```json
{
  "type": "ACTIVITY_TYPE_EXPORT_PRIVATE_KEY",
  "timestampMs": "1748956800000",
  "organizationId": "org-…",
  "parameters": {
    "privateKeyId": "b3f1…",
    "targetPublicKey": "04a1b2…"
  }
}
```

## Response — the export bundle

`result.exportPrivateKeyResult.exportBundle` is an RFC-9180 HPKE bundle. It contains
**no** plaintext — only the HPKE encapsulated key and ciphertext:

```json
{
  "activity": {
    "status": "ACTIVITY_STATUS_COMPLETED",
    "result": {
      "exportPrivateKeyResult": {
        "privateKeyId": "b3f1…",
        "exportBundle": {
          "encappedPublic": "BASE64(enc)",
          "ciphertext": "BASE64(ct)",
          "info": "kryard-export-v1",
          "kemKdfAead": "P256_HKDF_SHA256/HKDF_SHA256/AES256GCM"
        }
      }
    }
  }
}
```

| Field | Meaning |
| --- | --- |
| `encappedPublic` | base64 of the HPKE encapsulated key (`enc`). |
| `ciphertext` | base64 of the sealed private key (`ct`). |
| `info` | the HPKE `info` label, `kryard-export-v1` — binds the bundle to the export-protocol version. |
| `kemKdfAead` | the HPKE suite identifier (below). |

## HPKE suite

The bundle is sealed under a fixed RFC-9180 suite (Turnkey's suite):

| Component | Value |
| --- | --- |
| KEM | `KEM_P256_HKDF_SHA256` |
| KDF | `KDF_HKDF_SHA256` |
| AEAD | `AEAD_AES256GCM` |
| `info` | `kryard-export-v1` |
| `aad` (additional authenticated data) | the `privateKeyId` |

The `aad` binds the ciphertext to the exact key id, so a bundle cannot be silently
retargeted to a different key.

::: info Clean RFC-9180, not Turnkey's wrapper
This is a **standard RFC-9180 HPKE bundle** using Turnkey's cryptographic suite —
open it with Kryard's own `decryptExportBundle` (below) or any RFC-9180 HPKE library.
It is **not** byte-compatible with Turnkey's client wrapper, which expects their exact
enclave-signed format.
:::

## Decrypting the bundle

The SDK ships the matching opener. `generateRecipientKeyPair` gives you a P-256
recipient (use its `publicKeyHex` as `targetPublicKey`); `decryptExportBundle` opens
the bundle with the suite, `info`, and `aad = privateKeyId` already wired:

```ts
import { generateRecipientKeyPair, decryptExportBundle } from "@kryard/sdk";

const recipient = await generateRecipientKeyPair();

const { privateKeyId, exportBundle } = await client.exportPrivateKeyBundle({
  signWith: "b3f1…",
  targetPublicKey: recipient.publicKeyHex,
});

const keyBytes = await decryptExportBundle(exportBundle, recipient.privateKey, privateKeyId);
// keyBytes: the raw private key — handle securely, then keyBytes.fill(0).
```

::: tip
`client.exportPrivateKey({ signWith })` does exactly this for you — generate →
export → decrypt → zeroize — and returns `{ privateKey }`. Use the manual flow above
only when the recipient key must stay under your control.
:::

Under the hood it's a plain RFC-9180 open, so it's equivalent to doing it by hand
with any HPKE library (same suite, `info = "kryard-export-v1"`, `aad = privateKeyId`):

```ts
// Equivalent manual open with @hpke/core.
import { CipherSuite, Aes256Gcm, HkdfSha256, DhkemP256HkdfSha256 } from "@hpke/core";

const suite = new CipherSuite({
  kem: new DhkemP256HkdfSha256(),
  kdf: new HkdfSha256(),
  aead: new Aes256Gcm(),
});

const recipientCtx = await suite.createRecipientContext({
  recipientKey: recipientPrivateKey,                  // your P-256 private key
  enc: base64Decode(exportBundle.encappedPublic),
  info: new TextEncoder().encode(exportBundle.info),  // "kryard-export-v1"
});

const privateKeyBytes = await recipientCtx.open(
  base64Decode(exportBundle.ciphertext),
  new TextEncoder().encode(privateKeyId),             // aad
);
// privateKeyBytes is the raw key — handle it securely and zeroize when done.
```
