Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for auth (via email) #19

Merged
merged 5 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
directory: ["recovery", "export"]
directory: ["auth", "export"]

steps:
- name: Checkout
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ jobs:
- name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2

- name: Publish Recovery
- name: Publish Auth
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca #v1.5.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: recovery
directory: recovery
projectName: auth
directory: auth
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
Comment on lines +17 to 24
Copy link
Collaborator

@r-n-o r-n-o Nov 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this will fail on the cloudflare side, I'll need to edit the project to be named "auth". done!


- name: Publish Export
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ LABEL org.opencontainers.image.source https://github.com/tkhq/frames

COPY nginx.conf /etc/nginx/nginx.conf

COPY recovery /usr/share/nginx/recovery
# maintain recovery for backwards-compatibility
COPY auth /usr/share/nginx/auth
COPY auth /usr/share/nginx/recovery
COPY export /usr/share/nginx/export

EXPOSE 8080/tcp
EXPOSE 8081/tcp
EXPOSE 8082/tcp

CMD ["nginx"]
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Frames

This repository contains code for the recovery and export components of Turnkey. These components can be embedded as iframes by users to support end-users in recovery and export.
This repository contains code for the auth (which includes recovery) and export components of Turnkey. These components can be embedded as iframes by users to support end-users.

## Email Recovery
This self-contained HTML page is meant to be used as a standalone document to help first-party Turnkey root users. It's also going to be embedded as an iframe to help with sub-org root recovery.
## Auth
This self-contained HTML page is meant to be used for the following use cases:
- As a standalone document to enable first-party Turnkey root users to perform recovery and auth
- Embedded as an iframe for sub-org root recovery and auth

This page is hosted at https://recovery.turnkey.com/
This page is hosted at https://auth.turnkey.com/, but we will retain https://recovery.turnkey.com/for compatibility.

## Key and Wallet Export
This self-contained HTML page is meant to be used as either a standalone document or to be embedded as an iframe.
Expand All @@ -28,15 +30,15 @@ nvm use

Install dependencies:
```sh
cd recovery && npm install
cd auth && npm install
cd export && npm install
```

# Unit Testing

The export and recovery pages have tests. They run on CI automatically. If you want to run them locally:
The auth and recovery pages each have tests. They run on CI automatically. If you want to run them locally:
```sh
cd recovery && npm test
cd auth && npm test
cd export && npm test
```

Expand All @@ -51,19 +53,35 @@ Clone the `sdk` repo.
git clone [email protected]:tkhq/sdk.git
```

Follow the README.md for the `key-export` example. Set the `NEXT_PUBLIC_EXPORT_IFRAME_URL="http://localhost:3000/export"` in the example's environment variables configuration. The `wallet-export` example embeds this page as an iframe.
Follow the README.md for the `wallet-export` [example](https://github.com/tkhq/sdk/tree/main/examples/wallet-export). Set the `NEXT_PUBLIC_EXPORT_IFRAME_URL="http://localhost:3000/"` in the example's environment variables configuration. The `wallet-export` example embeds this page as an iframe.
```sh
cd sdk/examples/wallet-export
```

# Running Local Auth
Start the server. This command will run a simple static server on port 8080.
```sh
npm start
```

Clone the `sdk` repo.
```sh
git clone [email protected]:tkhq/sdk.git
```

Follow the README.md for the `email-auth` [example](https://github.com/tkhq/sdk/tree/main/examples/email-auth). Set the `NEXT_PUBLIC_AUTH_IFRAME_URL="http://localhost:3000/"` in the example's environment variables configuration. The `email-auth` example embeds this page as an iframe.
```sh
cd sdk/examples/email-auth
```

# Building and running in Docker

To build:
```
docker build . -t frames
```

To run (mapping 8080 and 8081 to 18080/18081 because they're often busy):
To run (mapping `[8080, 8081]` to `[18080, 18081]` because they're often busy):
```
docker run -p18080:8080 -p18081:8081 -t frames
```
Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions auth/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 25 additions & 24 deletions recovery/index.html → auth/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
<html class="no-js" lang="">

<head>
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<meta charset="utf-8">
<title>Turnkey Recovery</title>
<title>Turnkey Recovery and Auth</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
Expand Down Expand Up @@ -75,8 +76,8 @@
</head>

<body>
<h2>Init Recovery</h2>
<p><em>This public key will be sent along with your email inside of a new <code>INIT_USER_EMAIL_RECOVERY</code> activity</em></p>
<h2>Init Recovery or Auth</h2>
<p><em>This public key will be sent along with your email inside of a new <code>INIT_USER_EMAIL_RECOVERY</code> or <code>EMAIL_AUTH</code> activity</em></p>
<form>
<label>Embedded key</label>
<input type="text" name="embedded-key" id="embedded-key" disabled/>
Expand All @@ -85,18 +86,18 @@ <h2>Init Recovery</h2>
<br>
<br>
<br>
<h2>Inject Recovery Bundle</h2>
<p><em>The recovery bundle comes from your email. We can also simulate this locally: see instructions <a href="https://github.com/tkhq/recovery#running-a-fake-recovery" target="_blank">here</a>. A recovery bundle is composed of a public key and an encrypted payload. The payload is encrypted to this document's embedded key (stored in local storage and displayed above). The scheme relies on <a target="_blank" href="https://datatracker.ietf.org/doc/rfc9180/">HPKE (RFC 9180)</a></em>.</p>
<h2>Inject Credential Bundle</h2>
<p><em>The credential bundle will come from your email. This bundle can then be used for email recovery or auth. We can simulate this locally: see instructions <a href="https://github.com/tkhq/frames#running-local-auth" target="_blank">here</a>. A credential bundle is composed of a public key and an encrypted payload. The payload is encrypted to this document's embedded key (stored in local storage and displayed above). The scheme relies on <a target="_blank" href="https://datatracker.ietf.org/doc/rfc9180/">HPKE (RFC 9180)</a></em>.</p>
<form>
<label>Bundle</label>
<input type="text" name="recovery-bundle" id="recovery-bundle"/>
<input type="text" name="credential-bundle" id="credential-bundle"/>
<button id="inject">Inject Bundle</button>
</form>
<br>
<br>
<br>
<h2>Stamp</h2>
<p><em>Once you've injected the recovery bundle, the recovery credential is ready to sign. A new <code>RECOVER</code> activity for example. This iframe doesn't know anything about Turnkey activity however, it's a simple stamper!</em></p>
<p><em>Once you've injected the credential bundle, the credential is ready to sign. A new <code>RECOVER</code> activity for example. This iframe doesn't know anything about Turnkey activity however, it's a simple stamper!</em></p>
<form>
<label>Payload</label>
<input type="text" name="payload" id="payload"/>
Expand Down Expand Up @@ -464,7 +465,7 @@ <h2>Message log</h2>
* when performing `crypto.subtle.importKey` operations.
* @param {Uint8Array} privateKeyBytes
*/
var importRecoveryCredential = async function(privateKeyBytes) {
var importCredential = async function(privateKeyBytes) {
var privateKeyHexString = uint8arrayToHexString(privateKeyBytes);
var privateKey = BigInt('0x' + privateKeyHexString);
var publicKeyPoint = P256Generator.multiply(privateKey);
Expand Down Expand Up @@ -537,7 +538,7 @@ <h2>Message log</h2>
}

/**
* Accepts a public key array buffer, and returns a buffer with the uncomrpessed version of the public key
* Accepts a public key array buffer, and returns a buffer with the uncompressed version of the public key
* @param {Uint8Array} rawPublicKey
* @return {Uint8Array} the uncompressed bytes
*/
Expand Down Expand Up @@ -629,7 +630,7 @@ <h2>Message log</h2>
* ----
* IMPORTANT NOTE: below we implement basic field arithmetic for P256
* This is only used to compute public point from a secret key inside of
* `importRecoveryCredential` above. If something goes wrong with the code below
* `importCredential` above. If something goes wrong with the code below
* the web crypto API will simply refuse to import the key.
* None of the functions below are returned from the closure to minimize the risk of misuse.
*********************************************************************************************/
Expand Down Expand Up @@ -821,7 +822,7 @@ <h2>Message log</h2>
getEmbeddedKey,
setEmbeddedKey,
onResetEmbeddedKey,
importRecoveryCredential,
importCredential,
compressRawPublicKey,
uncompressRawPublicKey,
p256JWKPrivateToPublic,
Expand Down Expand Up @@ -849,8 +850,8 @@ <h2>Message log</h2>
// TODO: this should be bundled at build time or replaced with code written by Turnkey entirely.
import * as hpke from "https://esm.sh/@hpke/core";

// In memory spot for the recovery credential to live. We do NOT persist it to localStorage.
var RECOVERY_CREDENTIAL_BYTES = null;
// In memory spot for the credential to live. We do NOT persist it to localStorage.
var CREDENTIAL_BYTES = null;

document.addEventListener("DOMContentLoaded", async function () {
await TKHQ.initEmbeddedKey();
Expand All @@ -863,7 +864,7 @@ <h2>Message log</h2>
// TODO: find a way to filter messages and ensure they're coming from the parent window?
// We do not want to arbitrarily receive messages from all origins.
window.addEventListener("message", async function(event) {
if (event.data && event.data["type"] == "INJECT_RECOVERY_BUNDLE") {
if (event.data && (event.data["type"] == "INJECT_CREDENTIAL_BUNDLE" || event.data["type"] == "INJECT_RECOVERY_BUNDLE")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`);
try {
await onInjectBundle(event.data["value"])
Expand All @@ -890,15 +891,15 @@ <h2>Message log</h2>
}, false);

/**
* Event handlers to power the recovery flow in standalone mode
* Event handlers to power the recovery and auth flows in standalone mode
* Instead of receiving events from the parent page, forms trigger them.
* This is useful for debugging as well.
*/
document.getElementById("inject").addEventListener("click", async function(e) {
e.preventDefault();
window.postMessage({
"type": "INJECT_RECOVERY_BUNDLE",
"value": document.getElementById("recovery-bundle").value,
"type": "INJECT_CREDENTIAL_BUNDLE",
"value": document.getElementById("credential-bundle").value,
})
}, false);
document.getElementById("stamp").addEventListener("click", async function(e) {
Expand All @@ -915,7 +916,7 @@ <h2>Message log</h2>
}, false);

/**
* Function triggered when INJECT_RECOVERY_BUNDLE event is received.
* Function triggered when INJECT_CREDENTIAL_BUNDLE event is received.
* The `bundle` param is the concatenation of a public key and an encrypted payload, and then base64 encoded
* Example: A6ZPGAlxBRZhjKWky4RpXnHVceGzJjTuBrzKvMGnIgZ3r6JD4D1iiSg_m-y_u0BgJKI397Xjn0wgu17w9wuRooEp-F38m4ql57FgQ7sX9nQA
* @param {string} bundle
Expand Down Expand Up @@ -952,31 +953,31 @@ <h2>Message log</h2>
// Decompress the compressed key
var encappedKeyBuf = TKHQ.uncompressRawPublicKey(compressedEncappedKeyBuf);

var recoveryCredentialBytes = await HpkeDecrypt(
var credentialBytes = await HpkeDecrypt(
{
ciphertextBuf,
encappedKeyBuf,
receiverPrivJwk: embeddedKeyJwk,
});

RECOVERY_CREDENTIAL_BYTES = new Uint8Array(recoveryCredentialBytes);
CREDENTIAL_BYTES = new Uint8Array(credentialBytes);
TKHQ.sendMessageUp("BUNDLE_INJECTED", true)
}
/**
* Function triggered when STAMP_REQUEST event is received.
* @param {string} payload to sign
*/
var onStampRequest = async function(payload) {
if (RECOVERY_CREDENTIAL_BYTES === null) {
if (CREDENTIAL_BYTES === null) {
throw new Error("cannot sign payload without credential. Credential bytes are null");
}
var recoveryKey = await TKHQ.importRecoveryCredential(RECOVERY_CREDENTIAL_BYTES)
var key = await TKHQ.importCredential(CREDENTIAL_BYTES)
var signatureIeee1363 = await window.crypto.subtle.sign(
{
name: "ECDSA",
hash: {name: "SHA-256"},
},
recoveryKey,
key,
new TextEncoder().encode(payload)
);

Expand All @@ -988,7 +989,7 @@ <h2>Message log</h2>
// - Then imported without the private "d" component, and exported to get the public key
// ^^ (that's what `p256JWKPrivateToPublic` does)
// - Finally, compress the public key.
var jwkKey = await crypto.subtle.exportKey("jwk", recoveryKey);
var jwkKey = await crypto.subtle.exportKey("jwk", key);
var publicKey = await TKHQ.p256JWKPrivateToPublic(jwkKey);
var compressedPublicKey = TKHQ.compressRawPublicKey(publicKey);

Expand Down
8 changes: 4 additions & 4 deletions recovery/index.test.js → auth/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ describe("TKHQ", () => {
expect(key.key_ops).toContain("deriveBits");
})

it("imports recovery credentials without errors", async () => {
let key = await TKHQ.importRecoveryCredential(TKHQ.uint8arrayFromHexString("7632de7338577bc12c1731fa29f08019206af381f74af60f4d5e0395218f205c"));
it("imports credentials (for recovery or auth) without errors", async () => {
let key = await TKHQ.importCredential(TKHQ.uint8arrayFromHexString("7632de7338577bc12c1731fa29f08019206af381f74af60f4d5e0395218f205c"));
expect(key.constructor.name).toEqual("CryptoKey");
expect(key.algorithm).toEqual({ name: "ECDSA", namedCurve: "P-256"});
})

it("imports recovery credentials correctly", async () => {
let key = await TKHQ.importRecoveryCredential(TKHQ.uint8arrayFromHexString("7632de7338577bc12c1731fa29f08019206af381f74af60f4d5e0395218f205c"));
it("imports credentials (for recovery or auth) correctly", async () => {
let key = await TKHQ.importCredential(TKHQ.uint8arrayFromHexString("7632de7338577bc12c1731fa29f08019206af381f74af60f4d5e0395218f205c"));
let jwkPrivateKey = await crypto.subtle.exportKey("jwk", key);
let publicKey = await TKHQ.p256JWKPrivateToPublic(jwkPrivateKey);
let compressedPublicKey = TKHQ.compressRawPublicKey(publicKey);
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions recovery/package-lock.json → auth/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions recovery/package.json → auth/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "recovery-tests",
"name": "auth-tests",
"version": "1.0.0",
"main": "index.test.js",
"scripts": {
"start": "serve",
"test": "jest"
},
"repository": "[email protected]:tkhq/recovery.git",
"repository": "[email protected]:tkhq/frames.git",
"author": "Turnkey <[email protected]>",
"description": "This package is only here to help us test the main html file",
"license": "MIT",
Expand Down
10 changes: 10 additions & 0 deletions export/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion export/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<html class="no-js" lang="">

<head>
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<meta charset="utf-8">
<title>Turnkey Export</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
Expand Down
Loading
Loading