started to add symmetric key encryption

This commit is contained in:
PhilReact 2025-03-22 03:11:15 +02:00
parent 770080b942
commit 418c2c79a9
10 changed files with 3050 additions and 43 deletions

158
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.7", "version": "1.0.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.7", "version": "1.0.9",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
@ -15,8 +15,11 @@
"@mui/material": "^6.4.7", "@mui/material": "^6.4.7",
"@tanstack/react-virtual": "^3.13.2", "@tanstack/react-virtual": "^3.13.2",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"bloom-filters": "^3.0.4",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"compressorjs": "^1.2.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dropzone": "^14.3.8",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"short-unique-id": "^5.2.0", "short-unique-id": "^5.2.0",
"zustand": "^4.3.2" "zustand": "^4.3.2"
@ -1295,6 +1298,11 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/seedrandom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
"integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ=="
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@ -1325,6 +1333,14 @@
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true "dev": true
}, },
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/babel-plugin-macros": { "node_modules/babel-plugin-macros": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@ -1345,6 +1361,14 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -1364,6 +1388,29 @@
} }
] ]
}, },
"node_modules/bloom-filters": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz",
"integrity": "sha512-BdnPWo2OpYhlvuP2fRzJBdioMCkm7Zp0HCf8NJgF5Mbyqy7VQ/CnTiVWMMyq4EZCBHwj0Kq6098gW2/3RsZsrA==",
"dependencies": {
"@types/seedrandom": "^3.0.8",
"base64-arraybuffer": "^1.0.2",
"is-buffer": "^2.0.5",
"lodash": "^4.17.21",
"long": "^5.2.0",
"reflect-metadata": "^0.1.13",
"seedrandom": "^3.0.5",
"xxhashjs": "^0.2.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/blueimp-canvas-to-blob": {
"version": "3.29.0",
"resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -1478,6 +1525,15 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/compressorjs": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
"integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
"dependencies": {
"blueimp-canvas-to-blob": "^3.29.0",
"is-blob": "^2.1.0"
}
},
"node_modules/consola": { "node_modules/consola": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.1.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.1.tgz",
@ -1534,6 +1590,11 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"node_modules/cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
"integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw=="
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -1644,6 +1705,17 @@
} }
} }
}, },
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"dependencies": {
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/find-root": { "node_modules/find-root": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@ -1786,6 +1858,39 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
}, },
"node_modules/is-blob": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"engines": {
"node": ">=4"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@ -1886,12 +1991,22 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.sortby": { "node_modules/lodash.sortby": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"dev": true "dev": true
}, },
"node_modules/long": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
"integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -2141,6 +2256,22 @@
"react": "^19.0.0" "react": "^19.0.0"
} }
}, },
"node_modules/react-dropzone": {
"version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-intersection-observer": { "node_modules/react-intersection-observer": {
"version": "9.16.0", "version": "9.16.0",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
@ -2188,6 +2319,11 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/reflect-metadata": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
"integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A=="
},
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@ -2265,6 +2401,11 @@
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"peer": true "peer": true
}, },
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2520,6 +2661,11 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true "dev": true
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/tsup": { "node_modules/tsup": {
"version": "8.4.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/tsup/-/tsup-8.4.0.tgz", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.4.0.tgz",
@ -2715,6 +2861,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/xxhashjs": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
"integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
"dependencies": {
"cuint": "^0.2.2"
}
},
"node_modules/zustand": { "node_modules/zustand": {
"version": "4.5.6", "version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",

View File

@ -26,8 +26,11 @@
"@mui/material": "^6.4.7", "@mui/material": "^6.4.7",
"@tanstack/react-virtual": "^3.13.2", "@tanstack/react-virtual": "^3.13.2",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"bloom-filters": "^3.0.4",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"compressorjs": "^1.2.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dropzone": "^14.3.8",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"short-unique-id": "^5.2.0", "short-unique-id": "^5.2.0",
"zustand": "^4.3.2" "zustand": "^4.3.2"

109
src/common/ImagePicker.tsx Normal file
View File

@ -0,0 +1,109 @@
import React, { useCallback } from "react";
import { Box } from "@mui/material";
import {
useDropzone,
DropzoneRootProps,
DropzoneInputProps,
} from "react-dropzone";
import Compressor from "compressorjs";
import { fileToBase64 } from "../utils/base64";
type Mode = "single" | "multi";
interface CommonProps {
children: React.ReactNode;
mode?: Mode;
}
interface SingleModeProps extends CommonProps {
mode?: "single";
onPick: (base64: string) => void;
}
interface MultiModeProps extends CommonProps {
mode: "multi";
onPick: (base64s: string[]) => void;
}
type ImageUploaderProps = SingleModeProps | MultiModeProps;
export const ImagePicker: React.FC<ImageUploaderProps> = ({
children,
onPick,
mode = "single",
}) => {
const onDrop = useCallback(
async (acceptedFiles: File[]) => {
const images =
mode === "single" ? acceptedFiles.slice(0, 1) : acceptedFiles;
const base64s: string[] = [];
for (const image of images) {
try {
let fileToConvert: File;
if (image.type === "image/gif") {
if (image.size > 500 * 1024) {
console.error("GIF file size exceeds 500KB limit.");
continue;
}
fileToConvert = image;
} else {
fileToConvert = await new Promise<File>((resolve, reject) => {
new Compressor(image, {
quality: 0.6,
maxWidth: 1200,
mimeType: "image/webp",
success(result) {
resolve(
new File([result], image.name, { type: "image/webp" })
);
},
error(err) {
console.error("Compression error:", err);
reject(err);
},
});
}).catch(() => image); // fallback to original if compression fails
}
const base64 = await fileToBase64(fileToConvert);
base64s.push(base64);
} catch (error) {
console.error("File processing error:", error);
}
}
if (mode === "single") {
if (base64s[0]) {
(onPick as (base64: string) => void)(base64s[0]);
}
} else {
(onPick as (base64s: string[]) => void)(base64s);
}
},
[onPick, mode]
);
const {
getRootProps,
getInputProps,
}: {
getRootProps: () => DropzoneRootProps;
getInputProps: () => DropzoneInputProps;
isDragActive: boolean;
} = useDropzone({
onDrop,
accept: {
"image/*": [],
},
});
return (
<Box {...getRootProps()} sx={{ display: "flex" }}>
<input {...getInputProps()} />
{children}
</Box>
);
};

View File

@ -2,7 +2,7 @@ import React, { createContext, useContext, useMemo } from "react";
import { useAuth, UseAuthProps } from "../hooks/useAuth"; import { useAuth, UseAuthProps } from "../hooks/useAuth";
import { useResources } from "../hooks/useResources"; import { useResources } from "../hooks/useResources";
import { useAppInfo } from "../hooks/useAppInfo"; import { useAppInfo } from "../hooks/useAppInfo";
import { IdentifierBuilder } from "../utils/encryption"; import { addAndEncryptSymmetricKeys, IdentifierBuilder } from "../utils/encryption";
import { useIdentifiers } from "../hooks/useIdentifiers"; import { useIdentifiers } from "../hooks/useIdentifiers";
import { objectToBase64 } from "../utils/base64"; import { objectToBase64 } from "../utils/base64";
import { base64ToObject } from "../utils/publish"; import { base64ToObject } from "../utils/publish";
@ -10,7 +10,8 @@ import { base64ToObject } from "../utils/publish";
const utils = { const utils = {
objectToBase64, objectToBase64,
base64ToObject base64ToObject,
addAndEncryptSymmetricKeys
} }

2424
src/deps/nacl-fast.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ export const usePublish = (
const hasFetched = useRef(false); const hasFetched = useRef(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<null | string>(null); const [error, setError] = useState<null | string>(null);
const publish = usePublishStore().getPublish(metadata || null); const publish = usePublishStore().getPublish(metadata || null, true);
const setPublish = usePublishStore().setPublish; const setPublish = usePublishStore().setPublish;
const getPublish = usePublishStore().getPublish; const getPublish = usePublishStore().getPublish;

View File

@ -3,3 +3,4 @@ export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
export {usePublish} from "./hooks/usePublish" export {usePublish} from "./hooks/usePublish"
export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay" export {ResourceListDisplay} from "./components/ResourceList/ResourceListDisplay"
export {QortalSearchParams} from './types/interfaces/resources' export {QortalSearchParams} from './types/interfaces/resources'
export {ImagePicker} from './common/ImagePicker'

View File

@ -31,7 +31,7 @@ class Semaphore {
const semaphore = new Semaphore(1) const semaphore = new Semaphore(1)
export const fileToBase64 = (file : File): Promise<string> => new Promise((resolve, reject) => { export const fileToBase64 = (file : File | Blob): Promise<string> => new Promise((resolve, reject) => {
const reader = new FileReader(); // Create a new instance const reader = new FileReader(); // Create a new instance
semaphore.acquire(); semaphore.acquire();
reader.readAsDataURL(file); reader.readAsDataURL(file);

48
src/utils/bloomFilter.ts Normal file
View File

@ -0,0 +1,48 @@
import { BloomFilter } from 'bloom-filters';
import { base64ToObject } from './base64';
export async function hashPublicKey(publicKeyString: string) {
const encoder = new TextEncoder();
const data = encoder.encode(publicKeyString);
const digest = await crypto.subtle.digest("SHA-256", data);
const hashBytes = new Uint8Array(digest);
return Array.from(hashBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
export async function generateBloomFilterBase64(publicKeys: string[]) {
if (publicKeys.length > 100) throw new Error("Max 100 users allowed");
const bloom = BloomFilter.create(100, 0.0004); // ~0.04% FPR
for (const pk of publicKeys) {
const hash = await hashPublicKey(pk);
bloom.add(hash);
}
// Serialize to compact form
const byteArray = new Uint8Array(bloom.saveAsJSON()._data);
const base64 = btoa(String.fromCharCode(...byteArray));
if (byteArray.length > 230) {
throw new Error(`Bloom filter exceeds 230 bytes: ${byteArray.length}`);
}
return base64;
}
export async function userCanProbablyDecrypt(base64Bloom: string, userPublicKey: string) {
const base64ToJson = base64ToObject(base64Bloom)
const bloom = BloomFilter.fromJSON(base64ToJson)
const hash = await hashPublicKey(userPublicKey);
return bloom.has(hash);
}

View File

@ -1,16 +1,25 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { objectToBase64, uint8ArrayToBase64 } from "./base64";
import { RequestQueueWithPromise } from "./queue";
import { base64ToUint8Array } from "./publish";
import nacl from "../deps/nacl-fast";
export const requestQueueGetPublicKeys = new RequestQueueWithPromise(10);
export enum EnumCollisionStrength { export enum EnumCollisionStrength {
LOW = 8, LOW = 8,
MEDIUM = 11, MEDIUM = 11,
HIGH = 14, HIGH = 14,
PARENT_REF = 14, PARENT_REF = 14,
ENTITY_LABEL= 6 ENTITY_LABEL = 6,
} }
export async function hashWord(
export async function hashWord(word: string, collisionStrength: number, publicSalt: string) { word: string,
collisionStrength: number,
publicSalt: string
) {
const saltedWord = publicSalt + word; // Use public salt directly const saltedWord = publicSalt + word; // Use public salt directly
const encoded = new TextEncoder().encode(saltedWord); const encoded = new TextEncoder().encode(saltedWord);
const hashBuffer = await crypto.subtle.digest("SHA-256", encoded); const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
@ -24,8 +33,6 @@ export async function hashWord(word: string, collisionStrength: number, publicSa
.slice(0, collisionStrength); .slice(0, collisionStrength);
} }
const uid = new ShortUniqueId({ length: 10, dictionary: "alphanum" }); const uid = new ShortUniqueId({ length: 10, dictionary: "alphanum" });
interface EntityConfig { interface EntityConfig {
@ -39,10 +46,12 @@ export type IdentifierBuilder = {
}; };
// Recursive function to traverse identifierBuilder // Recursive function to traverse identifierBuilder
function findEntityConfig(identifierBuilder: IdentifierBuilder, path: string[]): EntityConfig { function findEntityConfig(
identifierBuilder: IdentifierBuilder,
path: string[]
): EntityConfig {
let current: EntityConfig | undefined = { children: identifierBuilder }; // ✅ Wrap it inside `{ children }` so it behaves like other levels let current: EntityConfig | undefined = { children: identifierBuilder }; // ✅ Wrap it inside `{ children }` so it behaves like other levels
for (const key of path) { for (const key of path) {
if (!current.children || !current.children[key]) { if (!current.children || !current.children[key]) {
throw new Error(`Entity '${key}' is not defined in identifierBuilder`); throw new Error(`Entity '${key}' is not defined in identifierBuilder`);
@ -53,7 +62,6 @@ function findEntityConfig(identifierBuilder: IdentifierBuilder, path: string[]):
return current; return current;
} }
// Function to generate a prefix for searching // Function to generate a prefix for searching
export async function buildSearchPrefix( export async function buildSearchPrefix(
appName: string, appName: string,
@ -63,10 +71,18 @@ export async function buildSearchPrefix(
identifierBuilder: IdentifierBuilder identifierBuilder: IdentifierBuilder
): Promise<string> { ): Promise<string> {
// Hash app name (11 chars) // Hash app name (11 chars)
const appHash: string = await hashWord(appName, EnumCollisionStrength.HIGH, publicSalt); const appHash: string = await hashWord(
appName,
EnumCollisionStrength.HIGH,
publicSalt
);
// Hash entity type (4 chars) // Hash entity type (4 chars)
const entityPrefix: string = await hashWord(entityType, EnumCollisionStrength.ENTITY_LABEL, publicSalt); const entityPrefix: string = await hashWord(
entityType,
EnumCollisionStrength.ENTITY_LABEL,
publicSalt
);
// ✅ Detect if this entity is actually a root entity // ✅ Detect if this entity is actually a root entity
const isRootEntity = !!identifierBuilder[entityType]; const isRootEntity = !!identifierBuilder[entityType];
@ -76,7 +92,11 @@ export async function buildSearchPrefix(
if (isRootEntity && parentId === null) { if (isRootEntity && parentId === null) {
parentRef = "00000000000000"; // ✅ Only for true root entities parentRef = "00000000000000"; // ✅ Only for true root entities
} else if (parentId) { } else if (parentId) {
parentRef = await hashWord(parentId, EnumCollisionStrength.PARENT_REF, publicSalt); parentRef = await hashWord(
parentId,
EnumCollisionStrength.PARENT_REF,
publicSalt
);
} }
// ✅ If there's no parentRef, return without it // ✅ If there's no parentRef, return without it
@ -85,8 +105,6 @@ export async function buildSearchPrefix(
: `${appHash}-${entityPrefix}-`; // ✅ Global search for entity type : `${appHash}-${entityPrefix}-`; // ✅ Global search for entity type
} }
// Function to generate IDs dynamically with `publicSalt` // Function to generate IDs dynamically with `publicSalt`
export async function buildIdentifier( export async function buildIdentifier(
appName: string, appName: string,
@ -95,13 +113,19 @@ export async function buildIdentifier(
parentId: string | null, parentId: string | null,
identifierBuilder: IdentifierBuilder identifierBuilder: IdentifierBuilder
): Promise<string> { ): Promise<string> {
// Hash app name (11 chars) // Hash app name (11 chars)
const appHash: string = await hashWord(appName, EnumCollisionStrength.HIGH, publicSalt); const appHash: string = await hashWord(
appName,
EnumCollisionStrength.HIGH,
publicSalt
);
// Hash entity type (4 chars) // Hash entity type (4 chars)
const entityPrefix: string = await hashWord(entityType, EnumCollisionStrength.ENTITY_LABEL, publicSalt); const entityPrefix: string = await hashWord(
entityType,
EnumCollisionStrength.ENTITY_LABEL,
publicSalt
);
// Generate a unique identifier for this entity // Generate a unique identifier for this entity
const entityUid = uid.rnd(); const entityUid = uid.rnd();
@ -109,8 +133,251 @@ export async function buildIdentifier(
// Determine parent reference // Determine parent reference
let parentRef = "00000000000000"; // Default for feeds let parentRef = "00000000000000"; // Default for feeds
if (parentId) { if (parentId) {
parentRef = await hashWord(parentId, EnumCollisionStrength.PARENT_REF, publicSalt); parentRef = await hashWord(
parentId,
EnumCollisionStrength.PARENT_REF,
publicSalt
);
} }
return `${appHash}-${entityPrefix}-${parentRef}-${entityUid}`; return `${appHash}-${entityPrefix}-${parentRef}-${entityUid}`;
} }
export const createSymmetricKeyAndNonce = () => {
const messageKey = new Uint8Array(32); // 32 bytes for the symmetric key
crypto.getRandomValues(messageKey);
return { messageKey: uint8ArrayToBase64(messageKey) };
};
const getPublicKeysByNames = async (names: string[]) => {
// Use the request queue for fetching public keys
const memberPromises = names.map((name) =>
requestQueueGetPublicKeys.enqueue(async () => {
try {
const response = await fetch(`/names/${name}`);
const nameInfo = await response.json();
const resAddress = await fetch(`/addresses/${nameInfo}`);
const resData = await resAddress.json();
return resData.publicKey;
} catch (error) {
return null;
}
})
);
const members = await Promise.all(memberPromises);
return members?.filter((item: string | null) => !!item);
};
export const addAndEncryptSymmetricKeys = async ({
previousData,
names,
}: {
previousData: Object;
names: string[];
}) => {
try {
let highestKey = 0;
if (previousData) {
highestKey = Math.max(
...Object.keys(previousData || {})
.filter((item) => !isNaN(+item))
.map(Number)
);
}
const groupmemberPublicKeys = await getPublicKeysByNames(names);
const symmetricKey = createSymmetricKeyAndNonce();
const nextNumber = highestKey + 1;
const objectToSave = {
...previousData,
[nextNumber]: symmetricKey,
};
const symmetricKeyAndNonceBase64 = await objectToBase64(objectToSave);
const encryptedData = await qortalRequest({
action: "ENCRYPT_DATA",
base64: symmetricKeyAndNonceBase64,
publicKeys: groupmemberPublicKeys,
});
if (encryptedData) {
return encryptedData;
} else {
throw new Error("Cannot encrypt content");
}
} catch (error: any) {
throw new Error(error.message);
}
};
export const encryptWithSymmetricKeys = async ({
data64,
secretKeyObject,
typeNumber = 2,
}: any) => {
// Find the highest key in the secretKeyObject
const highestKey = Math.max(
...Object.keys(secretKeyObject)
.filter((item) => !isNaN(+item))
.map(Number)
);
const highestKeyObject = secretKeyObject[highestKey];
// Convert data and keys from base64
const Uint8ArrayData = base64ToUint8Array(data64);
const messageKey = base64ToUint8Array(highestKeyObject.messageKey);
if (!(Uint8ArrayData instanceof Uint8Array)) {
throw new Error("The Uint8ArrayData you've submitted is invalid");
}
let nonce, encryptedData, encryptedDataBase64, finalEncryptedData;
// Convert type number to a fixed length of 3 digits
const typeNumberStr = typeNumber.toString().padStart(3, "0");
if (highestKeyObject.nonce) {
// Old format: Use the nonce from secretKeyObject
nonce = base64ToUint8Array(highestKeyObject.nonce);
// Encrypt the data with the existing nonce and message key
encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey);
encryptedDataBase64 = uint8ArrayToBase64(encryptedData);
// Concatenate the highest key, type number, and encrypted data (old format)
const highestKeyStr = highestKey.toString().padStart(10, "0"); // Fixed length of 10 digits
finalEncryptedData = btoa(highestKeyStr + encryptedDataBase64);
} else {
// New format: Generate a random nonce and embed it in the message
nonce = new Uint8Array(24); // 24 bytes for the nonce
crypto.getRandomValues(nonce);
// Encrypt the data with the new nonce and message key
encryptedData = nacl.secretbox(Uint8ArrayData, nonce, messageKey);
encryptedDataBase64 = uint8ArrayToBase64(encryptedData);
// Convert the nonce to base64
const nonceBase64 = uint8ArrayToBase64(nonce);
// Concatenate the highest key, type number, nonce, and encrypted data (new format)
const highestKeyStr = highestKey.toString().padStart(10, "0"); // Fixed length of 10 digits
const highestKeyBytes = new TextEncoder().encode(
highestKeyStr.padStart(10, "0")
);
const typeNumberBytes = new TextEncoder().encode(
typeNumberStr.padStart(3, "0")
);
// Step 3: Concatenate all binary
const combinedBinary = new Uint8Array(
highestKeyBytes.length +
typeNumberBytes.length +
nonce.length +
encryptedData.length
);
// finalEncryptedData = btoa(highestKeyStr) + btoa(typeNumberStr) + nonceBase64 + encryptedDataBase64;
combinedBinary.set(highestKeyBytes, 0);
combinedBinary.set(typeNumberBytes, highestKeyBytes.length);
combinedBinary.set(nonce, highestKeyBytes.length + typeNumberBytes.length);
combinedBinary.set(
encryptedData,
highestKeyBytes.length + typeNumberBytes.length + nonce.length
);
// Step 4: Base64 encode once
finalEncryptedData = uint8ArrayToBase64(combinedBinary);
}
return finalEncryptedData;
};
interface SecretKeyValue {
messageKey: string;
}
export const decryptSingle = async ({
base64,
secretKeyObject,
}: {
base64: string;
secretKeyObject: Record<number, SecretKeyValue>;
}) => {
// First, decode the base64-encoded input (if skipDecodeBase64 is not set)
const decodedData = base64;
// Then, decode it again for the specific format (if double encoding is used)
const decodeForNumber = atob(decodedData);
// Extract the key (assuming it's always the first 10 characters)
const keyStr = decodeForNumber.slice(0, 10);
// Convert the key string back to a number
const highestKey = parseInt(keyStr, 10);
// Check if we have a valid secret key for the extracted highestKey
if (!secretKeyObject[highestKey]) {
throw new Error("Cannot find correct secretKey");
}
const secretKeyEntry = secretKeyObject[highestKey];
let typeNumberStr, nonceBase64, encryptedDataBase64;
// Determine if typeNumber exists by checking if the next 3 characters after keyStr are digits
const possibleTypeNumberStr = decodeForNumber.slice(10, 13);
// const typeNumberStr = new TextDecoder().decode(typeNumberBytes);
if (decodeForNumber.slice(10, 13) !== "001") {
const decodedBinary = base64ToUint8Array(decodedData);
const highestKeyBytes = decodedBinary.slice(0, 10); // if ASCII digits only
const highestKeyStr = new TextDecoder().decode(highestKeyBytes);
const nonce = decodedBinary.slice(13, 13 + 24);
const encryptedData = decodedBinary.slice(13 + 24);
const highestKey = parseInt(highestKeyStr, 10);
const messageKey = base64ToUint8Array(
secretKeyObject[+highestKey].messageKey
);
const decryptedBytes = nacl.secretbox.open(
encryptedData,
nonce,
messageKey
);
// Check if decryption was successful
if (!decryptedBytes) {
throw new Error("Decryption failed");
}
// Convert the decrypted Uint8Array back to a Base64 string
return uint8ArrayToBase64(decryptedBytes);
}
// New format: Extract type number and nonce
typeNumberStr = possibleTypeNumberStr; // Extract type number
nonceBase64 = decodeForNumber.slice(13, 45); // Extract nonce (next 32 characters after type number)
encryptedDataBase64 = decodeForNumber.slice(45); // The remaining part is the encrypted data
// Convert Base64 strings to Uint8Array
const Uint8ArrayData = base64ToUint8Array(encryptedDataBase64);
const nonce = base64ToUint8Array(nonceBase64);
const messageKey = base64ToUint8Array(secretKeyEntry.messageKey);
if (!(Uint8ArrayData instanceof Uint8Array)) {
throw new Error("The Uint8ArrayData you've submitted is invalid");
}
// Decrypt the data using the nonce and messageKey
const decryptedData = nacl.secretbox.open(Uint8ArrayData, nonce, messageKey);
// Check if decryption was successful
if (!decryptedData) {
throw new Error("Decryption failed");
}
// Convert the decrypted Uint8Array back to a Base64 string
return uint8ArrayToBase64(decryptedData);
};