mirror of
https://github.com/status-im/js-waku-examples.git
synced 2025-02-08 13:13:32 +00:00
Improved security (#291)
- Notes are fully encrypted using Waku Message version 1 (no metadata leak) - Notes are always encrypted, key is part of the URL - Using standard uuid v4 from browser API to generate ID - upgraded deps to fix a bug
This commit is contained in:
parent
ce693cfdf1
commit
3c7795e121
5791
examples/flush-notes/package-lock.json
generated
5791
examples/flush-notes/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@ import { notes } from "@/services/notes";
|
||||
export default function Create() {
|
||||
const router = useRouter();
|
||||
const { note, onNoteChange } = useEditNote();
|
||||
const { toEncrypt, onEncryptChange } = useEncryptedState();
|
||||
|
||||
const onSave = async () => {
|
||||
if (!note) {
|
||||
@ -15,8 +14,8 @@ export default function Create() {
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, password } = await notes.createNote(note, toEncrypt);
|
||||
const passwordParam = password ? `?password=${password}` : "";
|
||||
const { id, key } = await notes.createNote(note);
|
||||
const passwordParam = `?key=${key}`;
|
||||
|
||||
router.push(`/view/${id}${passwordParam}`);
|
||||
} catch (error) {
|
||||
@ -30,17 +29,7 @@ export default function Create() {
|
||||
Your record will be stored for couple of days. Markdown is supported.
|
||||
</p>
|
||||
<div className="create-header">
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isEncrypted"
|
||||
name="isEncrypted"
|
||||
onChange={onEncryptChange}
|
||||
/>
|
||||
<label htmlFor="isEncrypted" className="to-encrypt">
|
||||
Private (only those that have link will read the note)
|
||||
</label>
|
||||
</div>
|
||||
<div></div>
|
||||
<button onClick={onSave} className="save-note">
|
||||
Save note
|
||||
</button>
|
||||
@ -66,16 +55,3 @@ const useEditNote = () => {
|
||||
onNoteChange,
|
||||
};
|
||||
};
|
||||
|
||||
const useEncryptedState = () => {
|
||||
const [toEncrypt, setToEncrypt] = React.useState<string>();
|
||||
|
||||
const onEncryptChange = (event: React.FormEvent<HTMLSelectElement>) => {
|
||||
setToEncrypt(event?.currentTarget?.value);
|
||||
};
|
||||
|
||||
return {
|
||||
toEncrypt,
|
||||
onEncryptChange,
|
||||
};
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import { Loading } from "@/components/Loading";
|
||||
|
||||
const View = () => {
|
||||
const router = useRouter();
|
||||
const { id, password } = useNoteURL();
|
||||
const { id, key } = useNoteURL();
|
||||
const [note, setNote] = React.useState<string>("");
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -18,8 +18,8 @@ const View = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
notes.readNote(id, password).then((note) => setNote(note || ""));
|
||||
}, [id, password, setNote]);
|
||||
notes.readNote(id, key).then((note) => setNote(note || ""));
|
||||
}, [id, key, setNote]);
|
||||
|
||||
if (!note) {
|
||||
return <Loading />;
|
||||
|
@ -1,16 +1,15 @@
|
||||
"use client";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export const useNoteURL = (): undefined | string => {
|
||||
export const useNoteURL = (): { id: string; key: string } => {
|
||||
const pathname = usePathname();
|
||||
const params = useSearchParams();
|
||||
|
||||
const segments = pathname.split("/");
|
||||
const viewIndex = segments.indexOf("view");
|
||||
const password = params.get("password");
|
||||
const key = params.get("key");
|
||||
|
||||
return {
|
||||
password,
|
||||
id: segments[viewIndex + 1] || undefined,
|
||||
};
|
||||
const id = segments[viewIndex + 1];
|
||||
|
||||
return { key, id };
|
||||
};
|
||||
|
@ -3,73 +3,64 @@
|
||||
import { waku } from "@/services/waku";
|
||||
import { CONTENT_TOPIC } from "@/const";
|
||||
import {
|
||||
symmetric,
|
||||
generateSymmetricKey,
|
||||
} from "@waku/message-encryption/crypto";
|
||||
import {
|
||||
createDecoder,
|
||||
createEncoder,
|
||||
Decoder,
|
||||
Encoder,
|
||||
IDecodedMessage,
|
||||
Unsubscribe,
|
||||
utf8ToBytes,
|
||||
bytesToUtf8,
|
||||
} from "@waku/sdk";
|
||||
import { generateRandomString } from "@/utils";
|
||||
createDecoder,
|
||||
DecodedMessage,
|
||||
generateSymmetricKey,
|
||||
} from "@waku/message-encryption/symmetric";
|
||||
import { Unsubscribe, utf8ToBytes, bytesToUtf8 } from "@waku/sdk";
|
||||
import { bytesToHex, hexToBytes } from "@waku/utils/bytes";
|
||||
|
||||
const UUID_V4_STR_LEN = 8 + 1 + 4 + 1 + 4 + 1 + 4 + 1 + 12; // 8-4-4-4-12 format
|
||||
|
||||
type Note = {
|
||||
id: string;
|
||||
content: string;
|
||||
iv: string;
|
||||
};
|
||||
|
||||
type NoteResult = {
|
||||
id: string;
|
||||
password?: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
export class Notes {
|
||||
private decoder: Decoder;
|
||||
private encoder: Encoder;
|
||||
private messages: IDecodedMessage[] = [];
|
||||
private messages: DecodedMessage[] = [];
|
||||
private subscription: undefined | Unsubscribe;
|
||||
|
||||
constructor() {
|
||||
this.decoder = createDecoder(CONTENT_TOPIC);
|
||||
this.encoder = createEncoder({ contentTopic: CONTENT_TOPIC });
|
||||
constructor() {}
|
||||
|
||||
public async createNote(content: string): Promise<NoteResult> {
|
||||
const symKey = generateSymmetricKey();
|
||||
|
||||
const encoder = createEncoder({ contentTopic: CONTENT_TOPIC, symKey });
|
||||
const id = self.crypto.randomUUID();
|
||||
|
||||
if (id.length !== UUID_V4_STR_LEN) {
|
||||
throw "Unexpected uuid length";
|
||||
}
|
||||
|
||||
public async createNote(
|
||||
content: string,
|
||||
toEncrypt?: boolean
|
||||
): Promise<NoteResult> {
|
||||
const symmetricKey = toEncrypt ? generateSymmetricKey() : undefined;
|
||||
const note = toEncrypt
|
||||
? await this.encryptNote(content, symmetricKey)
|
||||
: { id: generateRandomString(), content, iv: undefined };
|
||||
|
||||
await waku.send(this.encoder, {
|
||||
payload: utf8ToBytes(JSON.stringify(note)),
|
||||
await waku.send(encoder, {
|
||||
payload: utf8ToBytes(id + content),
|
||||
});
|
||||
|
||||
return {
|
||||
id: note.id,
|
||||
password: symmetricKey ? bytesToHex(symmetricKey) : undefined,
|
||||
id,
|
||||
key: bytesToHex(symKey),
|
||||
};
|
||||
}
|
||||
|
||||
public async readNote(
|
||||
id: string,
|
||||
password?: string
|
||||
): Promise<string | undefined> {
|
||||
await this.initMessages();
|
||||
public async readNote(id: string, key: string): Promise<string | undefined> {
|
||||
await this.initMessages(hexToBytes(key));
|
||||
|
||||
const message = this.messages
|
||||
.map((m) => {
|
||||
try {
|
||||
return JSON.parse(bytesToUtf8(m.payload)) as Note;
|
||||
const str = bytesToUtf8(m.payload);
|
||||
|
||||
const id = str.substring(0, UUID_V4_STR_LEN);
|
||||
const content = str.substring(UUID_V4_STR_LEN);
|
||||
|
||||
return { id, content } as Note;
|
||||
} catch (error) {
|
||||
console.log("Failed to read message:", error);
|
||||
}
|
||||
@ -80,65 +71,21 @@ export class Notes {
|
||||
}
|
||||
});
|
||||
|
||||
if (!message?.iv) {
|
||||
return message?.content;
|
||||
}
|
||||
|
||||
const passwordReceived =
|
||||
password || window.prompt("This note is encrypted, need password:");
|
||||
|
||||
if (!passwordReceived) {
|
||||
console.log("No password was provided, stopping reading a note.");
|
||||
return;
|
||||
}
|
||||
|
||||
return this.decryptNote(message, passwordReceived);
|
||||
}
|
||||
|
||||
private async initMessages() {
|
||||
private async initMessages(key: Uint8Array) {
|
||||
if (this.subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messages = await waku.getHistory(this.decoder);
|
||||
this.subscription = await waku.subscribe(this.decoder, (message) => {
|
||||
const decoder = createDecoder(CONTENT_TOPIC, key);
|
||||
|
||||
this.messages = await waku.getHistory(decoder);
|
||||
this.subscription = await waku.subscribe(decoder, (message) => {
|
||||
this.messages.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
private async encryptNote(
|
||||
content: string,
|
||||
symmetricKey: Uint8Array
|
||||
): Promise<Note> {
|
||||
const iv = symmetric.generateIv();
|
||||
const encryptedContent = await symmetric.encrypt(
|
||||
iv,
|
||||
symmetricKey,
|
||||
utf8ToBytes(content)
|
||||
);
|
||||
|
||||
return {
|
||||
id: generateRandomString(),
|
||||
content: bytesToHex(encryptedContent),
|
||||
iv: bytesToHex(iv),
|
||||
};
|
||||
}
|
||||
|
||||
private async decryptNote(note: Note, password: string): Promise<string> {
|
||||
if (!note?.iv) {
|
||||
throw Error("Failed to decrypt a note, no IV params found.");
|
||||
}
|
||||
|
||||
const iv = hexToBytes(note.iv);
|
||||
const symmetricKey = hexToBytes(password);
|
||||
const decryptedContent = await symmetric.decrypt(
|
||||
iv,
|
||||
symmetricKey,
|
||||
hexToBytes(note.content)
|
||||
);
|
||||
|
||||
return bytesToUtf8(decryptedContent);
|
||||
}
|
||||
}
|
||||
|
||||
export const notes = new Notes();
|
||||
|
@ -1,10 +0,0 @@
|
||||
export function generateRandomString(): string {
|
||||
let result = "";
|
||||
let characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let charactersLength = characters.length;
|
||||
for (let i = 0; i < 16; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user