diff --git a/README.md b/README.md
index 1e586b9..1d96e83 100644
--- a/README.md
+++ b/README.md
@@ -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
```
@@ -55,7 +55,7 @@ Clone the `sdk` repo.
git clone git@github.com: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
```
diff --git a/export/index.template.html b/export/index.template.html
index c53d1d6..dea20e9 100644
--- a/export/index.template.html
+++ b/export/index.template.html
@@ -160,6 +160,7 @@
Message log
>
+
@@ -172,6 +173,7 @@ Message log
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;
@@ -246,6 +248,25 @@ Message log
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
@@ -671,6 +692,111 @@ Message log
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}$",
+ 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,
@@ -693,6 +819,10 @@ Message log
additionalAssociatedData,
verifyEnclaveSignature,
getEd25519PublicKey,
+ applySettings,
+ validateStyles,
+ getSettings,
+ setSettings,
};
})();
@@ -709,6 +839,11 @@ Message log
document.addEventListener(
"DOMContentLoaded",
async () => {
+ // If styles are saved in local storage, sanitize and apply them.
+ const styleSettings = TKHQ.getSettings();
+ if (styleSettings) {
+ TKHQ.applySettings(styleSettings);
+ }
await TKHQ.initEmbeddedKey();
const embeddedKeyJwk = await TKHQ.getEmbeddedKey();
const targetPubBuf = await TKHQ.p256JWKPrivateToPublic(
@@ -764,6 +899,13 @@ Message log
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
);
@@ -820,7 +962,7 @@ Message log
*/
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";
}
});
@@ -835,13 +977,13 @@ Message log
};
// 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());
}
/**
@@ -1018,6 +1160,23 @@ Message log
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).
diff --git a/export/index.test.js b/export/index.test.js
index 41d3b1f..abe9230 100644
--- a/export/index.test.js
+++ b/export/index.test.js
@@ -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: "" };
+ 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);
+ });
});