mirror of
https://github.com/logos-messaging/examples.waku.org.git
synced 2026-01-04 05:43:07 +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() {
|
export default function Create() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { note, onNoteChange } = useEditNote();
|
const { note, onNoteChange } = useEditNote();
|
||||||
const { toEncrypt, onEncryptChange } = useEncryptedState();
|
|
||||||
|
|
||||||
const onSave = async () => {
|
const onSave = async () => {
|
||||||
if (!note) {
|
if (!note) {
|
||||||
@ -15,8 +14,8 @@ export default function Create() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { id, password } = await notes.createNote(note, toEncrypt);
|
const { id, key } = await notes.createNote(note);
|
||||||
const passwordParam = password ? `?password=${password}` : "";
|
const passwordParam = `?key=${key}`;
|
||||||
|
|
||||||
router.push(`/view/${id}${passwordParam}`);
|
router.push(`/view/${id}${passwordParam}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -30,17 +29,7 @@ export default function Create() {
|
|||||||
Your record will be stored for couple of days. Markdown is supported.
|
Your record will be stored for couple of days. Markdown is supported.
|
||||||
</p>
|
</p>
|
||||||
<div className="create-header">
|
<div className="create-header">
|
||||||
<div>
|
<div></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>
|
|
||||||
<button onClick={onSave} className="save-note">
|
<button onClick={onSave} className="save-note">
|
||||||
Save note
|
Save note
|
||||||
</button>
|
</button>
|
||||||
@ -66,16 +55,3 @@ const useEditNote = () => {
|
|||||||
onNoteChange,
|
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 View = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id, password } = useNoteURL();
|
const { id, key } = useNoteURL();
|
||||||
const [note, setNote] = React.useState<string>("");
|
const [note, setNote] = React.useState<string>("");
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -18,8 +18,8 @@ const View = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
notes.readNote(id, password).then((note) => setNote(note || ""));
|
notes.readNote(id, key).then((note) => setNote(note || ""));
|
||||||
}, [id, password, setNote]);
|
}, [id, key, setNote]);
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { usePathname, useSearchParams } from "next/navigation";
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
export const useNoteURL = (): undefined | string => {
|
export const useNoteURL = (): { id: string; key: string } => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
|
|
||||||
const segments = pathname.split("/");
|
const segments = pathname.split("/");
|
||||||
const viewIndex = segments.indexOf("view");
|
const viewIndex = segments.indexOf("view");
|
||||||
const password = params.get("password");
|
const key = params.get("key");
|
||||||
|
|
||||||
return {
|
const id = segments[viewIndex + 1];
|
||||||
password,
|
|
||||||
id: segments[viewIndex + 1] || undefined,
|
return { key, id };
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,73 +3,64 @@
|
|||||||
import { waku } from "@/services/waku";
|
import { waku } from "@/services/waku";
|
||||||
import { CONTENT_TOPIC } from "@/const";
|
import { CONTENT_TOPIC } from "@/const";
|
||||||
import {
|
import {
|
||||||
symmetric,
|
|
||||||
generateSymmetricKey,
|
|
||||||
} from "@waku/message-encryption/crypto";
|
|
||||||
import {
|
|
||||||
createDecoder,
|
|
||||||
createEncoder,
|
createEncoder,
|
||||||
Decoder,
|
createDecoder,
|
||||||
Encoder,
|
DecodedMessage,
|
||||||
IDecodedMessage,
|
generateSymmetricKey,
|
||||||
Unsubscribe,
|
} from "@waku/message-encryption/symmetric";
|
||||||
utf8ToBytes,
|
import { Unsubscribe, utf8ToBytes, bytesToUtf8 } from "@waku/sdk";
|
||||||
bytesToUtf8,
|
|
||||||
} from "@waku/sdk";
|
|
||||||
import { generateRandomString } from "@/utils";
|
|
||||||
import { bytesToHex, hexToBytes } from "@waku/utils/bytes";
|
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 = {
|
type Note = {
|
||||||
id: string;
|
id: string;
|
||||||
content: string;
|
content: string;
|
||||||
iv: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type NoteResult = {
|
type NoteResult = {
|
||||||
id: string;
|
id: string;
|
||||||
password?: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Notes {
|
export class Notes {
|
||||||
private decoder: Decoder;
|
private messages: DecodedMessage[] = [];
|
||||||
private encoder: Encoder;
|
|
||||||
private messages: IDecodedMessage[] = [];
|
|
||||||
private subscription: undefined | Unsubscribe;
|
private subscription: undefined | Unsubscribe;
|
||||||
|
|
||||||
constructor() {
|
constructor() {}
|
||||||
this.decoder = createDecoder(CONTENT_TOPIC);
|
|
||||||
this.encoder = createEncoder({ contentTopic: CONTENT_TOPIC });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createNote(
|
public async createNote(content: string): Promise<NoteResult> {
|
||||||
content: string,
|
const symKey = generateSymmetricKey();
|
||||||
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, {
|
const encoder = createEncoder({ contentTopic: CONTENT_TOPIC, symKey });
|
||||||
payload: utf8ToBytes(JSON.stringify(note)),
|
const id = self.crypto.randomUUID();
|
||||||
|
|
||||||
|
if (id.length !== UUID_V4_STR_LEN) {
|
||||||
|
throw "Unexpected uuid length";
|
||||||
|
}
|
||||||
|
|
||||||
|
await waku.send(encoder, {
|
||||||
|
payload: utf8ToBytes(id + content),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: note.id,
|
id,
|
||||||
password: symmetricKey ? bytesToHex(symmetricKey) : undefined,
|
key: bytesToHex(symKey),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readNote(
|
public async readNote(id: string, key: string): Promise<string | undefined> {
|
||||||
id: string,
|
await this.initMessages(hexToBytes(key));
|
||||||
password?: string
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
await this.initMessages();
|
|
||||||
|
|
||||||
const message = this.messages
|
const message = this.messages
|
||||||
.map((m) => {
|
.map((m) => {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.log("Failed to read message:", error);
|
console.log("Failed to read message:", error);
|
||||||
}
|
}
|
||||||
@ -80,65 +71,21 @@ export class Notes {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!message?.iv) {
|
return message?.content;
|
||||||
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) {
|
if (this.subscription) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messages = await waku.getHistory(this.decoder);
|
const decoder = createDecoder(CONTENT_TOPIC, key);
|
||||||
this.subscription = await waku.subscribe(this.decoder, (message) => {
|
|
||||||
|
this.messages = await waku.getHistory(decoder);
|
||||||
|
this.subscription = await waku.subscribe(decoder, (message) => {
|
||||||
this.messages.push(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();
|
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