Skip to content

Commit

Permalink
test(api): add tests for key recovery not enabled scenario
Browse files Browse the repository at this point in the history
feat(api): add support for recoverable keys in key creation
docs(security): update documentation for recovering keys and key creation
  • Loading branch information
chronark committed Aug 6, 2024
1 parent c09b217 commit 75c504f
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 5 deletions.
28 changes: 28 additions & 0 deletions apps/api/src/routes/v1_keys_createKey.error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,31 @@ test("reject invalid ratelimit config", async (t) => {
expect(res.status).toEqual(400);
expect(res.body.error.code).toEqual("BAD_REQUEST");
});

test("when key recovery is not enabled", async (t) => {
const h = await IntegrationHarness.init(t);

const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);
/* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */

const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
url: "/v1/keys.createKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
byteLength: 16,
apiId: h.resources.userApi.id,
recoverable: true,
},
});
expect(res.status).toEqual(412);
expect(res.body).toMatchObject({
error: {
code: "PRECONDITION_FAILED",
docs: "https://unkey.dev/docs/api-reference/errors/code/PRECONDITION_FAILED",
message: `api ${h.resources.userApi.id} does not support recoverable keys`,
},
});
});
1 change: 1 addition & 0 deletions apps/api/src/routes/v1_keys_createKey.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ describe("with encryption", () => {
},
body: {
apiId: h.resources.userApi.id,
recoverable: true,
},
});

Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1_keys_createKey.security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ test("cannot encrypt without permissions", async (t) => {
},
body: {
apiId: h.resources.userApi.id,
recoverable: true,
},
});

Expand Down
23 changes: 21 additions & 2 deletions apps/api/src/routes/v1_keys_createKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ When validating a key, we will return this back to you, so you can clearly ident
description: "Sets if key is enabled or disabled. Disabled keys are not valid.",
example: false,
}),
recoverable: z
.boolean()
.default(false)
.optional()
.openapi({
description: `You may want to show keys again later. While we do not recommend this, we leave this option open for you.
In addition to storing the key's hash, recoverable keys are stored in an encrypted vault, allowing you to retrieve and display the plaintext later.
https://www.unkey.com/docs/security/recovering-keys for more information.`,
}),
environment: z
.string()
.max(256)
Expand Down Expand Up @@ -271,12 +282,20 @@ export const registerV1KeysCreateKey = (app: App) =>
});
}

if (!api.keyAuthId) {
if (!api.keyAuth) {
throw new UnkeyApiError({
code: "PRECONDITION_FAILED",
message: `api ${req.apiId} is not setup to handle keys`,
});
}

if (req.recoverable && !api.keyAuth.storeEncryptedKeys) {
throw new UnkeyApiError({
code: "PRECONDITION_FAILED",
message: `api ${req.apiId} does not support recoverable keys`,
});
}

if (req.remaining === 0) {
throw new UnkeyApiError({
code: "BAD_REQUEST",
Expand Down Expand Up @@ -345,7 +364,7 @@ export const registerV1KeysCreateKey = (app: App) =>
identityId: identity?.id,
});

if (api.keyAuth?.storeEncryptedKeys) {
if (req.recoverable && api.keyAuth?.storeEncryptedKeys) {
const perm = rbac.evaluatePermissions(
buildUnkeyQuery(({ or }) => or("*", "api.*.encrypt_key", `api.${api.id}.encrypt_key`)),
auth.permissions,
Expand Down
21 changes: 18 additions & 3 deletions apps/docs/security/recovering-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ To learn more about how it works under the hood, you can head over to our [engin

## Opting in

By default we only store key hashes, not encrypted keys.
By default we only store key hashes, not encrypted keys.

If you want us to store keys in a way that we can recover them, you need to opt in:

Expand All @@ -52,6 +52,22 @@ Send us the email from the email address associated with your workspace and incl
</Note>


## Creating keys

When creating a key, you can set the `recoverable` field to `true`. This will store the key in a way that we can recover it later.


```shell
curl --request POST \
--url https://api.unkey.dev/v1/keys.createKey \
--header 'Authorization: Bearer {ROOT_KEY}' \
--header 'Content-Type: application/json' \
-d '{
"apiId": "{API_ID}",
"recoverable": true
}'
```



## Recovering plaintext keys
Expand All @@ -78,8 +94,7 @@ curl --request GET \


<Info>
If you have any questions about recovery, please reach out to us at [[email protected]](mailto:[email protected]).
If you have any questions about recovery, please reach out to us at [[email protected]](mailto:[email protected]).

For security concerns, please disclose them responsibly by emailing [[email protected]](mailto:[email protected]) instead.
</Info>

0 comments on commit 75c504f

Please sign in to comment.