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

Embedded key fixes #6

Merged
merged 7 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
67 changes: 56 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
font-weight: bold;
text-align: center;
}
#clear {
#reset {
color: white;
width: 7em;
font-size: 1em;
Expand Down Expand Up @@ -80,7 +80,7 @@ <h2>Init Recovery</h2>
<form>
<label>Embedded key</label>
<input type="text" name="embedded-key" id="embedded-key" disabled/>
<button id="clear">Reset Key</button>
<button id="reset">Reset Key</button>
</form>
<br>
<br>
Expand Down Expand Up @@ -116,7 +116,8 @@ <h2>Message log</h2>
<script>
window.TKHQ = function() {
/** constant for LocalStorage */
var TURNKEY_EMBEDDED_KEY = "TURNKEY_EMBEDDED_KEY"
var TURNKEY_EMBEDDED_KEY = "TURNKEY_EMBEDDED_KEY";
var TURNKEY_EMBEDDED_KEY_TTL = 1000 * 60 * 60 * 48; // 48 hours in seconds
oliviathet marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates a new public/private key pair and persists it in localStorage
Expand All @@ -136,9 +137,9 @@ <h2>Message log</h2>
*/
var generateTargetKey = async function() {
var p256key = await crypto.subtle.generateKey({
name: 'ECDSA',
name: 'ECDH',
namedCurve: 'P-256',
}, true, ['sign',]);
}, true, ['deriveBits',]);
oliviathet marked this conversation as resolved.
Show resolved Hide resolved

return await crypto.subtle.exportKey("jwk", p256key.privateKey);
}
Expand All @@ -147,7 +148,7 @@ <h2>Message log</h2>
* Gets the current embedded private key JWK. Returns `null` if not found.
*/
var getEmbeddedKey = function() {
var jwtKey = window.localStorage.getItem(TURNKEY_EMBEDDED_KEY)
var jwtKey = getItemWithExpiry(TURNKEY_EMBEDDED_KEY)
if (!jwtKey) {
return null
} else {
Expand All @@ -156,13 +157,51 @@ <h2>Message log</h2>
}

var setEmbeddedKey = function(targetKey) {
return window.localStorage.setItem(TURNKEY_EMBEDDED_KEY, JSON.stringify(targetKey))
return setItemWithExpiry(TURNKEY_EMBEDDED_KEY, JSON.stringify(targetKey), TURNKEY_EMBEDDED_KEY_TTL);
}

var clearEmbeddedKey = function() {
var resetEmbeddedKey = function() {
window.localStorage.removeItem(TURNKEY_EMBEDDED_KEY)
}

/**
* Set an item in localStorage with an expiration time.
*/
var setItemWithExpiry = function(key, value, ttl) {
const now = new Date();
const item = {
value: value,
expiry: now.getTime() + ttl,
r-n-o marked this conversation as resolved.
Show resolved Hide resolved
};
window.localStorage.setItem(key, JSON.stringify(item));
};

/**
* Get an item from localStorage. If it has expired, remove
* the item from localStorage and return null.
*/
const getItemWithExpiry = (key) => {
const itemStr = window.localStorage.getItem(key);

if (!itemStr) {
return null;
}

const item = JSON.parse(itemStr);

if (!item.hasOwnProperty("expiry") || !item.hasOwnProperty("value")) {
window.localStorage.removeItem(key);
return null;
}

const now = new Date();
if (now.getTime() > item.expiry) {
window.localStorage.removeItem(key);
return null;
}
return item.value;
};

/**
* Takes a hex string (e.g. "e4567ab") and returns an array buffer (Uint8Array)
* @param {string} hexString
Expand Down Expand Up @@ -707,9 +746,11 @@ <h2>Message log</h2>
return {
initEmbeddedKey,
generateTargetKey,
setItemWithExpiry,
getItemWithExpiry,
getEmbeddedKey,
setEmbeddedKey,
clearEmbeddedKey,
resetEmbeddedKey,
importRecoveryCredential,
compressRawPublicKey,
uncompressRawPublicKey,
Expand Down Expand Up @@ -759,6 +800,10 @@ <h2>Message log</h2>
TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`);
onStampRequest(event.data["value"]);
}
if (event.data && event.data["type"] == "RESET_EMBEDDED_KEY") {
TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}`);
resetEmbeddedKey();
}
}, false);

/**
Expand All @@ -782,9 +827,9 @@ <h2>Message log</h2>
}, false);

/**
* DEBUG functionality only to clear the page's localStorage
* DEBUG functionality only to reset the page's localStorage
*/
document.getElementById("clear").addEventListener("click", TKHQ.clearEmbeddedKey);
document.getElementById("reset").addEventListener("click", TKHQ.resetEmbeddedKey);
}, false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we want an actual event to drive the reset let's have the button click trigger an event on the page like we do for other events:
https://github.com/tkhq/recovery/blob/ce33591723435a64be97830a2a492ef6f7e519a7/index.html#L776-L782

And then you can add another if statement in here to handle the reset case:
https://github.com/tkhq/recovery/blob/ce33591723435a64be97830a2a492ef6f7e519a7/index.html#L753-L762

That way the parent will be able to post a RESET message to an iframe, or we can trigger this even in the standalone page, and the exact same onReset handler will be called.


/**
Expand Down
32 changes: 29 additions & 3 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,40 @@ describe("TKHQ", () => {
TKHQ = dom.window.TKHQ
})

it("gets, sets, and removes values in localStorage", async () => {
it("gets and sets items with expiry localStorage", async () => {
oliviathet marked this conversation as resolved.
Show resolved Hide resolved
// Set a TTL of 1000ms
TKHQ.setItemWithExpiry("k", "v", 1000);
let item = JSON.parse(dom.window.localStorage.getItem("k"));
expect(item.value).toBe("v");
expect(item.expiry).toBeTruthy();

// Get item that has not expired yet
item = TKHQ.getItemWithExpiry("k");
expect(item).toBe("v");

// Set a TTL of 500ms
TKHQ.setItemWithExpiry("a", "b", 500);
setTimeout(() => {
const expiredItem = TKHQ.getItemWithExpiry("a");
expect(expiredItem).toBeNull();
done();
}, 600); // Wait for 600ms to ensure the item has expired

// Returns null if getItemWithExpiry is called for item without expiry
dom.window.localStorage.setItem("k", JSON.stringify({ value: "v" }));
item = TKHQ.getItemWithExpiry("k");
expect(item).toBeNull();
})

it("gets, sets and resets embedded key in localStorage", async () => {
expect(TKHQ.getEmbeddedKey()).toBe(null);

// Set a dummy "key"
TKHQ.setEmbeddedKey({"foo": "bar"});
expect(TKHQ.getEmbeddedKey()).toEqual({"foo": "bar"});

// Now clear and assert we're back to no embedded key
TKHQ.clearEmbeddedKey();
// Now reset and assert we're back to no embedded key
TKHQ.resetEmbeddedKey();
expect(TKHQ.getEmbeddedKey()).toBe(null);
})

Expand All @@ -56,6 +81,7 @@ describe("TKHQ", () => {
expect(key.kty).toEqual("EC");
expect(key.ext).toBe(true);
expect(key.crv).toBe("P-256");
expect(key.key_ops).toContain("deriveBits");
})

it("imports recovery credentials without errors", async () => {
Expand Down
13 changes: 6 additions & 7 deletions package-lock.json

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

Loading