|
@@ -80,8 +80,10 @@ export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSour
|
|
|
_brand: "socketUpdateData";
|
|
|
};
|
|
|
|
|
|
+const IV_LENGTH_BYTES = 12; // 96 bits
|
|
|
+
|
|
|
export const createIV = () => {
|
|
|
- const arr = new Uint8Array(12);
|
|
|
+ const arr = new Uint8Array(IV_LENGTH_BYTES);
|
|
|
return window.crypto.getRandomValues(arr);
|
|
|
};
|
|
|
|
|
@@ -175,6 +177,22 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
|
|
|
[usage],
|
|
|
);
|
|
|
|
|
|
+const decryptImported = async (
|
|
|
+ iv: ArrayBuffer,
|
|
|
+ encrypted: ArrayBuffer,
|
|
|
+ privateKey: string,
|
|
|
+): Promise<ArrayBuffer> => {
|
|
|
+ const key = await getImportedKey(privateKey, "decrypt");
|
|
|
+ return window.crypto.subtle.decrypt(
|
|
|
+ {
|
|
|
+ name: "AES-GCM",
|
|
|
+ iv,
|
|
|
+ },
|
|
|
+ key,
|
|
|
+ encrypted,
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
const importFromBackend = async (
|
|
|
id: string | null,
|
|
|
privateKey?: string | null,
|
|
@@ -183,6 +201,7 @@ const importFromBackend = async (
|
|
|
const response = await fetch(
|
|
|
privateKey ? `${BACKEND_V2_GET}${id}` : `${BACKEND_GET}${id}.json`,
|
|
|
);
|
|
|
+
|
|
|
if (!response.ok) {
|
|
|
window.alert(t("alerts.importBackendFailed"));
|
|
|
return {};
|
|
@@ -190,16 +209,19 @@ const importFromBackend = async (
|
|
|
let data: ImportedDataState;
|
|
|
if (privateKey) {
|
|
|
const buffer = await response.arrayBuffer();
|
|
|
- const key = await getImportedKey(privateKey, "decrypt");
|
|
|
- const iv = new Uint8Array(12);
|
|
|
- const decrypted = await window.crypto.subtle.decrypt(
|
|
|
- {
|
|
|
- name: "AES-GCM",
|
|
|
- iv,
|
|
|
- },
|
|
|
- key,
|
|
|
- buffer,
|
|
|
- );
|
|
|
+
|
|
|
+ let decrypted: ArrayBuffer;
|
|
|
+ try {
|
|
|
+ // Buffer should contain both the IV (fixed length) and encrypted data
|
|
|
+ const iv = buffer.slice(0, IV_LENGTH_BYTES);
|
|
|
+ const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
|
|
|
+ decrypted = await decryptImported(iv, encrypted, privateKey);
|
|
|
+ } catch (error) {
|
|
|
+ // Fixed IV (old format, backward compatibility)
|
|
|
+ const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
|
|
|
+ decrypted = await decryptImported(fixedIv, buffer, privateKey);
|
|
|
+ }
|
|
|
+
|
|
|
// We need to convert the decrypted array buffer to a string
|
|
|
const string = new window.TextDecoder("utf-8").decode(
|
|
|
new Uint8Array(decrypted) as any,
|
|
@@ -263,9 +285,8 @@ export const exportToBackend = async (
|
|
|
true, // extractable
|
|
|
["encrypt", "decrypt"],
|
|
|
);
|
|
|
- // The iv is set to 0. We are never going to reuse the same key so we don't
|
|
|
- // need to have an iv. (I hope that's correct...)
|
|
|
- const iv = new Uint8Array(12);
|
|
|
+
|
|
|
+ const iv = createIV();
|
|
|
// We use symmetric encryption. AES-GCM is the recommended algorithm and
|
|
|
// includes checks that the ciphertext has not been modified by an attacker.
|
|
|
const encrypted = await window.crypto.subtle.encrypt(
|
|
@@ -276,6 +297,11 @@ export const exportToBackend = async (
|
|
|
key,
|
|
|
encoded,
|
|
|
);
|
|
|
+
|
|
|
+ // Concatenate IV with encrypted data (IV does not have to be secret).
|
|
|
+ const payloadBlob = new Blob([iv.buffer, encrypted]);
|
|
|
+ const payload = await new Response(payloadBlob).arrayBuffer();
|
|
|
+
|
|
|
// We use jwk encoding to be able to extract just the base64 encoded key.
|
|
|
// We will hardcode the rest of the attributes when importing back the key.
|
|
|
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
|
@@ -283,7 +309,7 @@ export const exportToBackend = async (
|
|
|
try {
|
|
|
const response = await fetch(BACKEND_V2_POST, {
|
|
|
method: "POST",
|
|
|
- body: encrypted,
|
|
|
+ body: payload,
|
|
|
});
|
|
|
const json = await response.json();
|
|
|
if (json.id) {
|