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

Apply Settings to export #51

Merged
merged 4 commits into from
Aug 28, 2024
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ cd import && npm test
```

# Running Local Wallet Import/Export
Start the server. This command will run a simple static server on port 8080.
Start the server. This command will run a simple static server on port 3000.
```sh
npm start
```
Expand All @@ -55,7 +55,7 @@ Clone the `sdk` repo.
git clone [email protected]:tkhq/sdk.git
```

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.
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/index.template"` in the example's environment variables configuration. The `wallet-export` example embeds this page as an iframe.
```sh
cd sdk/examples/wallet-export
```
Expand Down
165 changes: 162 additions & 3 deletions export/index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ <h2>Message log</h2>
>
</p>
<div id="message-log"></div>
<div id="key-div"></div>

<!-- Script containing ED25519 library -->
<script src="noble-hashes.js"></script>
Expand All @@ -172,6 +173,7 @@ <h2>Message log</h2>
window.TKHQ = (function () {
/** constant for LocalStorage */
const TURNKEY_EMBEDDED_KEY = "TURNKEY_EMBEDDED_KEY";
const TURNKEY_SETTINGS = "TURNKEY_SETTINGS";
/** 48 hours in milliseconds */
const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 48;

Expand Down Expand Up @@ -246,6 +248,25 @@ <h2>Message log</h2>
window.localStorage.removeItem(TURNKEY_EMBEDDED_KEY);
}

/**
* Gets the current settings.
*/
function getSettings() {
const settings = window.localStorage.getItem(TURNKEY_SETTINGS);
return settings ? JSON.parse(settings) : null;
}

/**
* Sets the settings object.
* @param {Object} settings
*/
function setSettings(settings) {
window.localStorage.setItem(
TURNKEY_SETTINGS,
JSON.stringify(settings)
);
}

/**
* Set an item in localStorage with an expiration time
* @param {string} key
Expand Down Expand Up @@ -671,6 +692,111 @@ <h2>Message log</h2>
return nobleEd25519.getPublicKey(privateKeyHex);
}

/**
* Function to validate and sanitize the styles object using the accepted map of style keys and values (as regular expressions).
* Any invalid style throws an error. Returns an object of valid styles.
* @param {Object} styles
* @return {Object}
*/
function validateStyles(styles, element) {
const validStyles = {};

const cssValidationRegex = {
padding: "^(\\d+(px|em|%|rem) ?){1,4}$",
Copy link
Contributor

Choose a reason for hiding this comment

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

Might make sense to put these valid, modifiable css properties in a doc somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good idea, can add something to the UI customization page

margin: "^(\\d+(px|em|%|rem) ?){1,4}$",
borderWidth: "^(\\d+(px|em|rem) ?){1,4}$",
borderStyle:
"^(none|solid|dashed|dotted|double|groove|ridge|inset|outset)$",
borderColor:
"^(transparent|inherit|initial|#[0-9a-f]{3,8}|rgba?\\(\\d{1,3}, \\d{1,3}, \\d{1,3}(, \\d?(\\.\\d{1,2})?)?\\)|hsla?\\(\\d{1,3}, \\d{1,3}%, \\d{1,3}%(, \\d?(\\.\\d{1,2})?)?\\))$",
borderRadius: "^(\\d+(px|em|%|rem) ?){1,4}$",
fontSize:
"^(\\d+(px|em|rem|%|vh|vw|in|cm|mm|pt|pc|ex|ch|vmin|vmax))$",
fontWeight: "^(normal|bold|bolder|lighter|\\d{3})$",
fontFamily: '^[^";<>]*$', // checks for the absence of some characters that could lead to CSS/HTML injection
color:
"^(transparent|inherit|initial|#[0-9a-f]{3,8}|rgba?\\(\\d{1,3}, \\d{1,3}, \\d{1,3}(, \\d?(\\.\\d{1,2})?)?\\)|hsla?\\(\\d{1,3}, \\d{1,3}%, \\d{1,3}%(, \\d?(\\.\\d{1,2})?)?\\))$",
backgroundColor:
"^(transparent|inherit|initial|#[0-9a-f]{3,8}|rgba?\\(\\d{1,3}, \\d{1,3}, \\d{1,3}(, \\d?(\\.\\d{1,2})?)?\\)|hsla?\\(\\d{1,3}, \\d{1,3}%, \\d{1,3}%(, \\d?(\\.\\d{1,2})?)?\\))$",
width:
"^(\\d+(px|em|rem|%|vh|vw|in|cm|mm|pt|pc|ex|ch|vmin|vmax)|auto)$",
height:
"^(\\d+(px|em|rem|%|vh|vw|in|cm|mm|pt|pc|ex|ch|vmin|vmax)|auto)$",
maxWidth:
"^(\\d+(px|em|rem|%|vh|vw|in|cm|mm|pt|pc|ex|ch|vmin|vmax)|none)$",
maxHeight:
"^(\\d+(px|em|rem|%|vh|vw|in|cm|mm|pt|pc|ex|ch|vmin|vmax)|none)$",
lineHeight:
"^(\\d+(\\.\\d+)?(px|em|rem|%|vh|vw|in|cm|mm|pt|pc|ex|ch|vmin|vmax)|normal)$",
boxShadow:
"^(none|(\\d+(px|em|rem) ?){2,4} (#[0-9a-f]{3,8}|rgba?\\(\\d{1,3}, \\d{1,3}, \\d{1,3}(, \\d?(\\.\\d{1,2})?)?\\)) ?(inset)?)$",
textAlign: "^(left|right|center|justify|initial|inherit)$",
overflowWrap: "^(normal|break-word|anywhere)$",
wordWrap: "^(normal|break-word)$",
resize: "^(none|both|horizontal|vertical|block|inline)$",
};

Object.entries(styles).forEach(([property, value]) => {
const styleProperty = property.trim();
if (styleProperty.length === 0) {
throw new Error("css style property cannot be empty");
}
const styleRegexStr = cssValidationRegex[styleProperty];
if (!styleRegexStr) {
throw new Error(
`invalid or unsupported css style property: "${styleProperty}"`
);
}
const styleRegex = new RegExp(styleRegexStr);
const styleValue = value.trim();
if (styleValue.length == 0) {
throw new Error(`css style for "${styleProperty}" is empty`);
}
const isValidStyle = styleRegex.test(styleValue);
if (!isValidStyle) {
throw new Error(
`invalid css style value for property "${styleProperty}"`
);
}
validStyles[styleProperty] = styleValue;
});

return validStyles;
}

/**
* Function to apply settings on this page. For now, the only settings that can be applied
* are for "styles". Upon successful application, return the valid, sanitized settings JSON string.
* @param {string} settings
* @return {string}
*/
function applySettings(settings) {
const validSettings = {};
if (!settings) {
return JSON.stringify(validSettings);
}
const settingsObj = JSON.parse(settings);
if (settingsObj.styles) {
// Valid styles will be applied the "key-div" div HTML element.
const keyDivTextarea = document.getElementById("key-div");
if (!keyDivTextarea) {
throw new Error(
"no key-div HTML element found to apply settings to."
);
}

// Validate, sanitize, and apply the styles to the "key-div" div element.
const validStyles = TKHQ.validateStyles(settingsObj.styles);
Object.entries(validStyles).forEach(([key, value]) => {
keyDivTextarea.style[key] = value;
});

validSettings["styles"] = validStyles;
}

return JSON.stringify(validSettings);
}

return {
initEmbeddedKey,
generateTargetKey,
Expand All @@ -693,6 +819,10 @@ <h2>Message log</h2>
additionalAssociatedData,
verifyEnclaveSignature,
getEd25519PublicKey,
applySettings,
validateStyles,
getSettings,
setSettings,
};
})();
</script>
Expand All @@ -709,6 +839,11 @@ <h2>Message log</h2>
document.addEventListener(
"DOMContentLoaded",
async () => {
// If styles are saved in local storage, sanitize and apply them.
Copy link
Contributor

Choose a reason for hiding this comment

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

You may have already done this, and nothing indicates the contrary, but would be nice to do a sanity check to make sure these loaded settings can be overridden if the user calls applySettings again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this works as exepected!

const styleSettings = TKHQ.getSettings();
if (styleSettings) {
TKHQ.applySettings(styleSettings);
}
await TKHQ.initEmbeddedKey();
const embeddedKeyJwk = await TKHQ.getEmbeddedKey();
const targetPubBuf = await TKHQ.p256JWKPrivateToPublic(
Expand Down Expand Up @@ -764,6 +899,13 @@ <h2>Message log</h2>
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
if (event.data && event.data["type"] == "APPLY_SETTINGS") {
try {
await onApplySettings(event.data["value"]);
} catch (e) {
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
},
false
);
Expand Down Expand Up @@ -820,7 +962,7 @@ <h2>Message log</h2>
*/
function displayKey(key) {
Array.from(document.body.children).forEach((child) => {
if (child.tagName !== "SCRIPT") {
if (child.tagName !== "SCRIPT" && child.id !== "key-div") {
child.style.display = "none";
}
});
Expand All @@ -835,13 +977,13 @@ <h2>Message log</h2>
};

// Create a new div with the key material and append the new div to the body
const keyDiv = document.createElement("div");
keyDiv.id = "key-div";
const keyDiv = document.getElementById("key-div");
keyDiv.innerText = key;
for (let styleKey in style) {
keyDiv.style[styleKey] = style[styleKey];
}
document.body.appendChild(keyDiv);
TKHQ.applySettings(TKHQ.getSettings());
}

/**
Expand Down Expand Up @@ -1018,6 +1160,23 @@ <h2>Message log</h2>
TKHQ.sendMessageUp("BUNDLE_INJECTED", true);
}

/**
* Function triggered when APPLY_SETTINGS event is received.
* For now, the only settings that can be applied are for "styles".
* Persist them in local storage so they can be applied on every
* page load.
*/
async function onApplySettings(settings) {
// Apply settings
const validSettings = TKHQ.applySettings(settings);

// Persist in local storage
TKHQ.setSettings(validSettings);

// Send up SETTINGS_APPLIED message
TKHQ.sendMessageUp("SETTINGS_APPLIED", true);
}

/**
* Decrypt the ciphertext (ArrayBuffer) given an encapsulation key (ArrayBuffer)
* and the receivers private key (JSON Web Key).
Expand Down
64 changes: 64 additions & 0 deletions export/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,68 @@ describe("TKHQ", () => {
)
).rejects.toThrow("cannot create uint8array from invalid hex string");
});

it("validates styles", async () => {
let simpleValid = { padding: "10px" };
expect(TKHQ.validateStyles(simpleValid)).toEqual(simpleValid);

simpleValid = { padding: "10px", margin: "10px", fontSize: "16px" };
expect(TKHQ.validateStyles(simpleValid)).toEqual(simpleValid);

let simpleValidPadding = {
"padding ": "10px",
margin: "10px",
fontSize: "16px",
};
expect(TKHQ.validateStyles(simpleValidPadding)).toEqual(simpleValid);

let simpleInvalidCase = {
padding: "10px",
margin: "10px",
"font-size": "16px",
};
expect(() => TKHQ.validateStyles(simpleInvalidCase)).toThrow(
`invalid or unsupported css style property: "font-size"`
);

let fontFamilyInvalid = { fontFamily: "<script>malicious</script>" };
expect(() => TKHQ.validateStyles(fontFamilyInvalid)).toThrow(
`invalid css style value for property "fontFamily"`
);

fontFamilyInvalid = { fontFamily: '"Courier"' };
expect(() => TKHQ.validateStyles(fontFamilyInvalid)).toThrow(
`invalid css style value for property "fontFamily"`
);

fontFamilyInvalid = { fontFamily: "San Serif;" };
expect(() => TKHQ.validateStyles(fontFamilyInvalid)).toThrow(
`invalid css style value for property "fontFamily"`
);

let allStylesValid = {
padding: "10px",
margin: "10px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "transparent",
borderRadius: "5px",
fontSize: "16px",
fontWeight: "bold",
fontFamily: "SFMono-Regular, Menlo, Monaco, Consolas, monospace",
color: "#000000",
backgroundColor: "rgb(128, 0, 128)",
width: "100%",
height: "auto",
maxWidth: "100%",
maxHeight: "100%",
lineHeight: "1.25rem",
boxShadow: "0px 0px 10px #aaa",
textAlign: "center",
overflowWrap: "break-word",
wordWrap: "break-word",
resize: "none",
};
expect(TKHQ.validateStyles(allStylesValid)).toEqual(allStylesValid);
});
});
Loading