add encryption
This commit is contained in:
+12
-1
@@ -2,12 +2,23 @@ import * as React from "bloatless-react";
|
||||
|
||||
import { MessageComposer } from "../Views/messageComposer";
|
||||
import { ThreadView } from "../Views/threadView";
|
||||
import { clearMessageHistory } from "../model";
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function MessageTab() {
|
||||
return (
|
||||
<article id="message-tab">
|
||||
<header>{translation.messages}</header>
|
||||
<header>
|
||||
{translation.messages}
|
||||
<span>
|
||||
<button
|
||||
aria-label={translation.clearHistory}
|
||||
on:click={clearMessageHistory}
|
||||
>
|
||||
<span class="icon">delete_sweep</span>
|
||||
</button>
|
||||
</span>
|
||||
</header>
|
||||
{ThreadView()}
|
||||
<footer>{MessageComposer()}</footer>
|
||||
</article>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { encryptionKey } from "../model";
|
||||
import { encryptionKey, isEncryptionUnavailable } from "../model";
|
||||
|
||||
import { translation } from "../translations";
|
||||
|
||||
const shouldShowKey = new React.State(false);
|
||||
@@ -12,11 +13,23 @@ export function EncryptionSection() {
|
||||
return (
|
||||
<div>
|
||||
<h2>{translation.encryption}</h2>
|
||||
<div
|
||||
class="error tile margin-bottom"
|
||||
toggle:hidden={!isEncryptionUnavailable}
|
||||
>
|
||||
<span class="icon">warning</span>
|
||||
<div>
|
||||
<b>{translation.encryptionUnavailableTitle}</b>
|
||||
<p>{translation.encryptionUnavailableMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="tile">
|
||||
<span class="icon">key</span>
|
||||
<div>
|
||||
<span>{translation.encryptionKey}</span>
|
||||
<input
|
||||
toggle:disabled={isEncryptionUnavailable}
|
||||
bind:value={encryptionKey}
|
||||
set:type={inputType}
|
||||
placeholder={translation.encryptionKeyPlaceholder}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
const IV_SIZE = 12;
|
||||
const ENCRYPTION_ALG = "AES-GCM";
|
||||
|
||||
// process
|
||||
export function encode(string: string): Uint8Array {
|
||||
return new TextEncoder().encode(string);
|
||||
}
|
||||
|
||||
export function decode(array: Uint8Array): string {
|
||||
return new TextDecoder("utf-8").decode(array);
|
||||
}
|
||||
|
||||
export async function encrypt(
|
||||
iv: Uint8Array,
|
||||
key: CryptoKey,
|
||||
message: string
|
||||
): Promise<Uint8Array> {
|
||||
const arrayBuffer = await window.crypto.subtle.encrypt(
|
||||
{ name: ENCRYPTION_ALG, iv },
|
||||
key,
|
||||
encode(message)
|
||||
);
|
||||
return new Uint8Array(arrayBuffer);
|
||||
}
|
||||
|
||||
export async function decrypt(
|
||||
iv: Uint8Array,
|
||||
key: CryptoKey,
|
||||
cyphertext: Uint8Array
|
||||
): Promise<string> {
|
||||
const arrayBuffer = await crypto.subtle.decrypt(
|
||||
{ name: ENCRYPTION_ALG, iv },
|
||||
key,
|
||||
cyphertext
|
||||
);
|
||||
return arrayBufferToString(arrayBuffer);
|
||||
}
|
||||
|
||||
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
|
||||
return await crypto.subtle.digest("SHA-256", encoded);
|
||||
}
|
||||
|
||||
// iv
|
||||
export function generateIV(): Uint8Array {
|
||||
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
|
||||
}
|
||||
|
||||
export function splitMergedString(mergedString: string): [Uint8Array, string] {
|
||||
const iv = mergedString.slice(0, IV_SIZE);
|
||||
const encryptedString = mergedString.slice(IV_SIZE);
|
||||
|
||||
return [JSON.parse(iv), encryptedString];
|
||||
}
|
||||
|
||||
// key
|
||||
export async function importKey(
|
||||
passphrase: string,
|
||||
purpose: "encrypt" | "decrypt"
|
||||
): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey(
|
||||
"raw",
|
||||
await hash(encode(passphrase)),
|
||||
{ name: ENCRYPTION_ALG },
|
||||
false,
|
||||
[purpose]
|
||||
);
|
||||
}
|
||||
|
||||
// conversion
|
||||
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
|
||||
const uInt8Array = new Uint8Array(arrayBuffer);
|
||||
return decode(uInt8Array);
|
||||
}
|
||||
|
||||
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
|
||||
return Array.from(uInt8Array);
|
||||
}
|
||||
|
||||
export function arrayToUint8(array: number[]): Uint8Array {
|
||||
return new Uint8Array(array);
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import "./styles.css";
|
||||
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { MessageTab } from "./Tabs/messageTab";
|
||||
import { SettingsTab } from "./Tabs/settingsTab";
|
||||
import { translation } from "./translations";
|
||||
|
||||
document.querySelector("main")!.append(SettingsTab(), MessageTab());
|
||||
document.querySelector("main")!.classList.add("split");
|
||||
|
||||
+61
-7
@@ -1,6 +1,18 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import {
|
||||
arrayToUint8,
|
||||
decode,
|
||||
decrypt,
|
||||
encrypt,
|
||||
generateIV,
|
||||
importKey,
|
||||
splitMergedString,
|
||||
uInt8ToArray,
|
||||
} from "./cryptUtility";
|
||||
|
||||
import UDNFrontend from "udn-frontend";
|
||||
import { translation } from "./translations";
|
||||
|
||||
const UDN = new UDNFrontend();
|
||||
|
||||
@@ -35,19 +47,22 @@ export const serverAddress = React.restoreState("socket-address", "");
|
||||
export const isConnected = new React.State(false);
|
||||
|
||||
export const cannotConnect = React.createProxyState(
|
||||
[serverAddress, isConnected],
|
||||
[serverAddress, currentAddress, isConnected],
|
||||
() =>
|
||||
serverAddress.value == "" ||
|
||||
(currentAddress.value == serverAddress.value && isConnected.value == true)
|
||||
);
|
||||
|
||||
// encryption
|
||||
export const encryptionKey = React.restoreState("encryption-key", "");
|
||||
export const isEncryptionUnavailable = window.crypto.subtle == undefined;
|
||||
|
||||
// sending
|
||||
export const currentPrimaryChannel = new React.State("");
|
||||
export const primaryChannel = React.restoreState("primary-channel", "");
|
||||
export const newSecondaryChannelName = new React.State("");
|
||||
export const secondaryChannels =
|
||||
React.restoreListState<Channel>("secondary-channels");
|
||||
export const encryptionKey = React.restoreState("encryption-key", "");
|
||||
export const senderName = React.restoreState("sender-name", "");
|
||||
export const messageBody = React.restoreState("message", "");
|
||||
|
||||
@@ -79,10 +94,11 @@ export const messages = React.restoreListState<Message>("messages");
|
||||
export function connect() {
|
||||
if (cannotConnect.value == true) return;
|
||||
currentAddress.value = serverAddress.value;
|
||||
isConnected.value = false;
|
||||
UDN.connect(serverAddress.value);
|
||||
}
|
||||
|
||||
export function sendMessage() {
|
||||
export async function sendMessage(): Promise<void> {
|
||||
if (cannotSendMessage.value == true) return;
|
||||
|
||||
// get channels
|
||||
@@ -94,12 +110,16 @@ export function sendMessage() {
|
||||
...secondaryChannelNames,
|
||||
];
|
||||
|
||||
// encrypt
|
||||
const encrypted = await encryptMessage();
|
||||
if (encrypted == undefined) return;
|
||||
|
||||
// create object
|
||||
const joinedChannelName = allChannelNames.join("/");
|
||||
const messageObject = new Message(
|
||||
joinedChannelName,
|
||||
senderName.value,
|
||||
messageBody.value,
|
||||
encrypted,
|
||||
new Date().toISOString()
|
||||
);
|
||||
const messageString = JSON.stringify(messageObject);
|
||||
@@ -111,6 +131,10 @@ export function sendMessage() {
|
||||
messageBody.value = "";
|
||||
}
|
||||
|
||||
export function clearMessageHistory() {
|
||||
messages.clear();
|
||||
}
|
||||
|
||||
export function setChannel(): void {
|
||||
if (cannotSetChannel.value == true) return;
|
||||
|
||||
@@ -135,6 +159,7 @@ export function removeSecondaryChannel(channel: Channel): void {
|
||||
// LISTENERS
|
||||
UDN.onconnect = () => {
|
||||
isConnected.value = true;
|
||||
setChannel();
|
||||
};
|
||||
|
||||
UDN.onmessage = (data) => {
|
||||
@@ -154,13 +179,12 @@ function handleSubscriptionConfirmation(
|
||||
channel: string,
|
||||
isSubscribed: boolean
|
||||
): void {
|
||||
console.log(channel, isSubscribed);
|
||||
if (isSubscribed == true) {
|
||||
currentPrimaryChannel.value = channel;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessage(body: string): void {
|
||||
async function handleMessage(body: string): Promise<void> {
|
||||
try {
|
||||
const object: Message = JSON.parse(body);
|
||||
if (!object.channel || !object.sender || !object.body || !object.isoDate)
|
||||
@@ -169,9 +193,39 @@ function handleMessage(body: string): void {
|
||||
const messageObject = new Message(
|
||||
object.channel,
|
||||
object.sender,
|
||||
object.body,
|
||||
await decryptMessage(object.body),
|
||||
object.isoDate
|
||||
);
|
||||
messages.add(messageObject);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function encryptMessage(): Promise<string> {
|
||||
if (encryptionKey.value == "") return messageBody.value;
|
||||
|
||||
if (!window.crypto.subtle) return messageBody.value;
|
||||
|
||||
const iv = generateIV();
|
||||
const key = await importKey(encryptionKey.value, "encrypt");
|
||||
const encryptedArray = await encrypt(iv, key, messageBody.value);
|
||||
|
||||
const encryptionData = {
|
||||
iv: uInt8ToArray(iv),
|
||||
encryptedArray: uInt8ToArray(encryptedArray),
|
||||
};
|
||||
return btoa(JSON.stringify(encryptionData));
|
||||
}
|
||||
|
||||
async function decryptMessage(encryptionData: string): Promise<string> {
|
||||
try {
|
||||
const encrypionData = JSON.parse(atob(encryptionData));
|
||||
const iv = arrayToUint8(encrypionData.iv);
|
||||
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
|
||||
|
||||
const key = await importKey(encryptionKey.value, "decrypt");
|
||||
return await decrypt(iv, key, encryptedArray);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return encryptionData;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,13 +1,13 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { currentAddress, primaryChannel } from "./model";
|
||||
import { currentAddress } from "./model";
|
||||
|
||||
const englishTranslations = {
|
||||
// general
|
||||
setInput: "Set",
|
||||
connectionStatus: "Connection status",
|
||||
connectedTo: React.createProxyState(
|
||||
[currentAddress],
|
||||
[],
|
||||
() => `Connected to ${currentAddress}`
|
||||
),
|
||||
disconnected: "Disconnected",
|
||||
@@ -34,7 +34,11 @@ const englishTranslations = {
|
||||
// messages
|
||||
messages: "Messages",
|
||||
composerPlaceholder: "Type a message...",
|
||||
sendMessage: "Send"
|
||||
sendMessage: "Send",
|
||||
clearHistory: "Clear history",
|
||||
encryptionUnavailableTitle: "Encryption is not available",
|
||||
encryptionUnavailableMessage:
|
||||
"Encryption is not available on insecure contexts. Obtain this app via HTTPS or continue without encryption",
|
||||
};
|
||||
|
||||
const allTranslations = {
|
||||
|
||||
Reference in New Issue
Block a user