add encryption
This commit is contained in:
Vendored
+123
-15
@@ -106,6 +106,9 @@
|
|||||||
});
|
});
|
||||||
this.callSubscriptions();
|
this.callSubscriptions();
|
||||||
}
|
}
|
||||||
|
clear() {
|
||||||
|
this.remove(...this.value.values());
|
||||||
|
}
|
||||||
// handlers
|
// handlers
|
||||||
handleAddition(handler) {
|
handleAddition(handler) {
|
||||||
this.additionHandlers.add(handler);
|
this.additionHandlers.add(handler);
|
||||||
@@ -211,10 +214,14 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "toggle": {
|
case "toggle": {
|
||||||
const state = value;
|
if (value.subscribe) {
|
||||||
state.subscribe(
|
const state = value;
|
||||||
(newValue) => element.toggleAttribute(directiveValue, newValue)
|
state.subscribe(
|
||||||
);
|
(newValue) => element.toggleAttribute(directiveValue, newValue)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
element.toggleAttribute(directiveValue, value);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "set": {
|
case "set": {
|
||||||
@@ -232,6 +239,57 @@
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/cryptUtility.ts
|
||||||
|
var IV_SIZE = 12;
|
||||||
|
var ENCRYPTION_ALG = "AES-GCM";
|
||||||
|
function encode(string) {
|
||||||
|
return new TextEncoder().encode(string);
|
||||||
|
}
|
||||||
|
function decode(array) {
|
||||||
|
return new TextDecoder("utf-8").decode(array);
|
||||||
|
}
|
||||||
|
async function encrypt(iv, key, message) {
|
||||||
|
const arrayBuffer = await window.crypto.subtle.encrypt(
|
||||||
|
{ name: ENCRYPTION_ALG, iv },
|
||||||
|
key,
|
||||||
|
encode(message)
|
||||||
|
);
|
||||||
|
return new Uint8Array(arrayBuffer);
|
||||||
|
}
|
||||||
|
async function decrypt(iv, key, cyphertext) {
|
||||||
|
const arrayBuffer = await crypto.subtle.decrypt(
|
||||||
|
{ name: ENCRYPTION_ALG, iv },
|
||||||
|
key,
|
||||||
|
cyphertext
|
||||||
|
);
|
||||||
|
return arrayBufferToString(arrayBuffer);
|
||||||
|
}
|
||||||
|
async function hash(encoded) {
|
||||||
|
return await crypto.subtle.digest("SHA-256", encoded);
|
||||||
|
}
|
||||||
|
function generateIV() {
|
||||||
|
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
|
||||||
|
}
|
||||||
|
async function importKey(passphrase, purpose) {
|
||||||
|
return await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
await hash(encode(passphrase)),
|
||||||
|
{ name: ENCRYPTION_ALG },
|
||||||
|
false,
|
||||||
|
[purpose]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function arrayBufferToString(arrayBuffer) {
|
||||||
|
const uInt8Array = new Uint8Array(arrayBuffer);
|
||||||
|
return decode(uInt8Array);
|
||||||
|
}
|
||||||
|
function uInt8ToArray(uInt8Array) {
|
||||||
|
return Array.from(uInt8Array);
|
||||||
|
}
|
||||||
|
function arrayToUint8(array) {
|
||||||
|
return new Uint8Array(array);
|
||||||
|
}
|
||||||
|
|
||||||
// node_modules/udn-frontend/index.ts
|
// node_modules/udn-frontend/index.ts
|
||||||
var UDNFrontend = class {
|
var UDNFrontend = class {
|
||||||
ws;
|
ws;
|
||||||
@@ -312,14 +370,15 @@
|
|||||||
var serverAddress = restoreState("socket-address", "");
|
var serverAddress = restoreState("socket-address", "");
|
||||||
var isConnected = new State(false);
|
var isConnected = new State(false);
|
||||||
var cannotConnect = createProxyState(
|
var cannotConnect = createProxyState(
|
||||||
[serverAddress, isConnected],
|
[serverAddress, currentAddress, isConnected],
|
||||||
() => serverAddress.value == "" || currentAddress.value == serverAddress.value && isConnected.value == true
|
() => serverAddress.value == "" || currentAddress.value == serverAddress.value && isConnected.value == true
|
||||||
);
|
);
|
||||||
|
var encryptionKey = restoreState("encryption-key", "");
|
||||||
|
var isEncryptionUnavailable = window.crypto.subtle == void 0;
|
||||||
var currentPrimaryChannel = new State("");
|
var currentPrimaryChannel = new State("");
|
||||||
var primaryChannel = restoreState("primary-channel", "");
|
var primaryChannel = restoreState("primary-channel", "");
|
||||||
var newSecondaryChannelName = new State("");
|
var newSecondaryChannelName = new State("");
|
||||||
var secondaryChannels = restoreListState("secondary-channels");
|
var secondaryChannels = restoreListState("secondary-channels");
|
||||||
var encryptionKey = restoreState("encryption-key", "");
|
|
||||||
var senderName = restoreState("sender-name", "");
|
var senderName = restoreState("sender-name", "");
|
||||||
var messageBody = restoreState("message", "");
|
var messageBody = restoreState("message", "");
|
||||||
var cannotSetChannel = createProxyState(
|
var cannotSetChannel = createProxyState(
|
||||||
@@ -338,9 +397,10 @@
|
|||||||
function connect() {
|
function connect() {
|
||||||
if (cannotConnect.value == true) return;
|
if (cannotConnect.value == true) return;
|
||||||
currentAddress.value = serverAddress.value;
|
currentAddress.value = serverAddress.value;
|
||||||
|
isConnected.value = false;
|
||||||
UDN.connect(serverAddress.value);
|
UDN.connect(serverAddress.value);
|
||||||
}
|
}
|
||||||
function sendMessage() {
|
async function sendMessage() {
|
||||||
if (cannotSendMessage.value == true) return;
|
if (cannotSendMessage.value == true) return;
|
||||||
const secondaryChannelNames = [
|
const secondaryChannelNames = [
|
||||||
...secondaryChannels.value.values()
|
...secondaryChannels.value.values()
|
||||||
@@ -349,17 +409,22 @@
|
|||||||
primaryChannel.value,
|
primaryChannel.value,
|
||||||
...secondaryChannelNames
|
...secondaryChannelNames
|
||||||
];
|
];
|
||||||
|
const encrypted = await encryptMessage();
|
||||||
|
if (encrypted == void 0) return;
|
||||||
const joinedChannelName = allChannelNames.join("/");
|
const joinedChannelName = allChannelNames.join("/");
|
||||||
const messageObject = new Message(
|
const messageObject = new Message(
|
||||||
joinedChannelName,
|
joinedChannelName,
|
||||||
senderName.value,
|
senderName.value,
|
||||||
messageBody.value,
|
encrypted,
|
||||||
(/* @__PURE__ */ new Date()).toISOString()
|
(/* @__PURE__ */ new Date()).toISOString()
|
||||||
);
|
);
|
||||||
const messageString = JSON.stringify(messageObject);
|
const messageString = JSON.stringify(messageObject);
|
||||||
UDN.sendMessage(joinedChannelName, messageString);
|
UDN.sendMessage(joinedChannelName, messageString);
|
||||||
messageBody.value = "";
|
messageBody.value = "";
|
||||||
}
|
}
|
||||||
|
function clearMessageHistory() {
|
||||||
|
messages.clear();
|
||||||
|
}
|
||||||
function setChannel() {
|
function setChannel() {
|
||||||
if (cannotSetChannel.value == true) return;
|
if (cannotSetChannel.value == true) return;
|
||||||
if (currentPrimaryChannel != void 0) {
|
if (currentPrimaryChannel != void 0) {
|
||||||
@@ -378,6 +443,7 @@
|
|||||||
}
|
}
|
||||||
UDN.onconnect = () => {
|
UDN.onconnect = () => {
|
||||||
isConnected.value = true;
|
isConnected.value = true;
|
||||||
|
setChannel();
|
||||||
};
|
};
|
||||||
UDN.onmessage = (data) => {
|
UDN.onmessage = (data) => {
|
||||||
if (data.subscribed != void 0 && data.messageChannel != void 0) {
|
if (data.subscribed != void 0 && data.messageChannel != void 0) {
|
||||||
@@ -390,12 +456,11 @@
|
|||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
};
|
};
|
||||||
function handleSubscriptionConfirmation(channel, isSubscribed) {
|
function handleSubscriptionConfirmation(channel, isSubscribed) {
|
||||||
console.log(channel, isSubscribed);
|
|
||||||
if (isSubscribed == true) {
|
if (isSubscribed == true) {
|
||||||
currentPrimaryChannel.value = channel;
|
currentPrimaryChannel.value = channel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleMessage(body) {
|
async function handleMessage(body) {
|
||||||
try {
|
try {
|
||||||
const object = JSON.parse(body);
|
const object = JSON.parse(body);
|
||||||
if (!object.channel || !object.sender || !object.body || !object.isoDate)
|
if (!object.channel || !object.sender || !object.body || !object.isoDate)
|
||||||
@@ -403,13 +468,37 @@
|
|||||||
const messageObject = new Message(
|
const messageObject = new Message(
|
||||||
object.channel,
|
object.channel,
|
||||||
object.sender,
|
object.sender,
|
||||||
object.body,
|
await decryptMessage(object.body),
|
||||||
object.isoDate
|
object.isoDate
|
||||||
);
|
);
|
||||||
messages.add(messageObject);
|
messages.add(messageObject);
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function encryptMessage() {
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// src/translations.ts
|
// src/translations.ts
|
||||||
var englishTranslations = {
|
var englishTranslations = {
|
||||||
@@ -417,7 +506,7 @@
|
|||||||
setInput: "Set",
|
setInput: "Set",
|
||||||
connectionStatus: "Connection status",
|
connectionStatus: "Connection status",
|
||||||
connectedTo: createProxyState(
|
connectedTo: createProxyState(
|
||||||
[currentAddress],
|
[],
|
||||||
() => `Connected to ${currentAddress}`
|
() => `Connected to ${currentAddress}`
|
||||||
),
|
),
|
||||||
disconnected: "Disconnected",
|
disconnected: "Disconnected",
|
||||||
@@ -442,7 +531,10 @@
|
|||||||
// messages
|
// messages
|
||||||
messages: "Messages",
|
messages: "Messages",
|
||||||
composerPlaceholder: "Type a message...",
|
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"
|
||||||
};
|
};
|
||||||
var allTranslations = {
|
var allTranslations = {
|
||||||
en: englishTranslations
|
en: englishTranslations
|
||||||
@@ -482,7 +574,14 @@
|
|||||||
|
|
||||||
// src/Tabs/messageTab.tsx
|
// src/Tabs/messageTab.tsx
|
||||||
function MessageTab() {
|
function MessageTab() {
|
||||||
return /* @__PURE__ */ createElement("article", { id: "message-tab" }, /* @__PURE__ */ createElement("header", null, translation.messages), ThreadView(), /* @__PURE__ */ createElement("footer", null, MessageComposer()));
|
return /* @__PURE__ */ createElement("article", { id: "message-tab" }, /* @__PURE__ */ createElement("header", null, translation.messages, /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
"aria-label": translation.clearHistory,
|
||||||
|
"on:click": clearMessageHistory
|
||||||
|
},
|
||||||
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "delete_sweep")
|
||||||
|
))), ThreadView(), /* @__PURE__ */ createElement("footer", null, MessageComposer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/Views/communicationSection.tsx
|
// src/Views/communicationSection.tsx
|
||||||
@@ -593,9 +692,18 @@
|
|||||||
() => shouldShowKey.value ? "text" : "password"
|
() => shouldShowKey.value ? "text" : "password"
|
||||||
);
|
);
|
||||||
function EncryptionSection() {
|
function EncryptionSection() {
|
||||||
return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("h2", null, translation.encryption), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.encryptionKey), /* @__PURE__ */ createElement(
|
return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("h2", null, translation.encryption), /* @__PURE__ */ createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "error tile margin-bottom",
|
||||||
|
"toggle:hidden": !isEncryptionUnavailable
|
||||||
|
},
|
||||||
|
/* @__PURE__ */ createElement("span", { class: "icon" }, "warning"),
|
||||||
|
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translation.encryptionUnavailableTitle), /* @__PURE__ */ createElement("p", null, translation.encryptionUnavailableMessage))
|
||||||
|
), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.encryptionKey), /* @__PURE__ */ createElement(
|
||||||
"input",
|
"input",
|
||||||
{
|
{
|
||||||
|
"toggle:disabled": isEncryptionUnavailable,
|
||||||
"bind:value": encryptionKey,
|
"bind:value": encryptionKey,
|
||||||
"set:type": inputType,
|
"set:type": inputType,
|
||||||
placeholder: translation.encryptionKeyPlaceholder
|
placeholder: translation.encryptionKeyPlaceholder
|
||||||
|
|||||||
Vendored
+4
@@ -932,6 +932,10 @@ main {
|
|||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
/* scrolling */
|
/* scrolling */
|
||||||
main {
|
main {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bloatless-react": "^1.2.1",
|
"bloatless-react": "^1.2.3",
|
||||||
"esbuild": "latest",
|
"esbuild": "latest",
|
||||||
"udn-frontend": "^1.0.2"
|
"udn-frontend": "^1.0.2"
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-1
@@ -2,12 +2,23 @@ import * as React from "bloatless-react";
|
|||||||
|
|
||||||
import { MessageComposer } from "../Views/messageComposer";
|
import { MessageComposer } from "../Views/messageComposer";
|
||||||
import { ThreadView } from "../Views/threadView";
|
import { ThreadView } from "../Views/threadView";
|
||||||
|
import { clearMessageHistory } from "../model";
|
||||||
import { translation } from "../translations";
|
import { translation } from "../translations";
|
||||||
|
|
||||||
export function MessageTab() {
|
export function MessageTab() {
|
||||||
return (
|
return (
|
||||||
<article id="message-tab">
|
<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()}
|
{ThreadView()}
|
||||||
<footer>{MessageComposer()}</footer>
|
<footer>{MessageComposer()}</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
import { encryptionKey } from "../model";
|
import { encryptionKey, isEncryptionUnavailable } from "../model";
|
||||||
|
|
||||||
import { translation } from "../translations";
|
import { translation } from "../translations";
|
||||||
|
|
||||||
const shouldShowKey = new React.State(false);
|
const shouldShowKey = new React.State(false);
|
||||||
@@ -12,11 +13,23 @@ export function EncryptionSection() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>{translation.encryption}</h2>
|
<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">
|
<label class="tile">
|
||||||
<span class="icon">key</span>
|
<span class="icon">key</span>
|
||||||
<div>
|
<div>
|
||||||
<span>{translation.encryptionKey}</span>
|
<span>{translation.encryptionKey}</span>
|
||||||
<input
|
<input
|
||||||
|
toggle:disabled={isEncryptionUnavailable}
|
||||||
bind:value={encryptionKey}
|
bind:value={encryptionKey}
|
||||||
set:type={inputType}
|
set:type={inputType}
|
||||||
placeholder={translation.encryptionKeyPlaceholder}
|
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 "./styles.css";
|
||||||
|
|
||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { MessageTab } from "./Tabs/messageTab";
|
import { MessageTab } from "./Tabs/messageTab";
|
||||||
import { SettingsTab } from "./Tabs/settingsTab";
|
import { SettingsTab } from "./Tabs/settingsTab";
|
||||||
import { translation } from "./translations";
|
|
||||||
|
|
||||||
document.querySelector("main")!.append(SettingsTab(), MessageTab());
|
document.querySelector("main")!.append(SettingsTab(), MessageTab());
|
||||||
document.querySelector("main")!.classList.add("split");
|
document.querySelector("main")!.classList.add("split");
|
||||||
|
|||||||
+61
-7
@@ -1,6 +1,18 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
arrayToUint8,
|
||||||
|
decode,
|
||||||
|
decrypt,
|
||||||
|
encrypt,
|
||||||
|
generateIV,
|
||||||
|
importKey,
|
||||||
|
splitMergedString,
|
||||||
|
uInt8ToArray,
|
||||||
|
} from "./cryptUtility";
|
||||||
|
|
||||||
import UDNFrontend from "udn-frontend";
|
import UDNFrontend from "udn-frontend";
|
||||||
|
import { translation } from "./translations";
|
||||||
|
|
||||||
const UDN = new UDNFrontend();
|
const UDN = new UDNFrontend();
|
||||||
|
|
||||||
@@ -35,19 +47,22 @@ export const serverAddress = React.restoreState("socket-address", "");
|
|||||||
export const isConnected = new React.State(false);
|
export const isConnected = new React.State(false);
|
||||||
|
|
||||||
export const cannotConnect = React.createProxyState(
|
export const cannotConnect = React.createProxyState(
|
||||||
[serverAddress, isConnected],
|
[serverAddress, currentAddress, isConnected],
|
||||||
() =>
|
() =>
|
||||||
serverAddress.value == "" ||
|
serverAddress.value == "" ||
|
||||||
(currentAddress.value == serverAddress.value && isConnected.value == true)
|
(currentAddress.value == serverAddress.value && isConnected.value == true)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// encryption
|
||||||
|
export const encryptionKey = React.restoreState("encryption-key", "");
|
||||||
|
export const isEncryptionUnavailable = window.crypto.subtle == undefined;
|
||||||
|
|
||||||
// sending
|
// sending
|
||||||
export const currentPrimaryChannel = new React.State("");
|
export const currentPrimaryChannel = new React.State("");
|
||||||
export const primaryChannel = React.restoreState("primary-channel", "");
|
export const primaryChannel = React.restoreState("primary-channel", "");
|
||||||
export const newSecondaryChannelName = new React.State("");
|
export const newSecondaryChannelName = new React.State("");
|
||||||
export const secondaryChannels =
|
export const secondaryChannels =
|
||||||
React.restoreListState<Channel>("secondary-channels");
|
React.restoreListState<Channel>("secondary-channels");
|
||||||
export const encryptionKey = React.restoreState("encryption-key", "");
|
|
||||||
export const senderName = React.restoreState("sender-name", "");
|
export const senderName = React.restoreState("sender-name", "");
|
||||||
export const messageBody = React.restoreState("message", "");
|
export const messageBody = React.restoreState("message", "");
|
||||||
|
|
||||||
@@ -79,10 +94,11 @@ export const messages = React.restoreListState<Message>("messages");
|
|||||||
export function connect() {
|
export function connect() {
|
||||||
if (cannotConnect.value == true) return;
|
if (cannotConnect.value == true) return;
|
||||||
currentAddress.value = serverAddress.value;
|
currentAddress.value = serverAddress.value;
|
||||||
|
isConnected.value = false;
|
||||||
UDN.connect(serverAddress.value);
|
UDN.connect(serverAddress.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMessage() {
|
export async function sendMessage(): Promise<void> {
|
||||||
if (cannotSendMessage.value == true) return;
|
if (cannotSendMessage.value == true) return;
|
||||||
|
|
||||||
// get channels
|
// get channels
|
||||||
@@ -94,12 +110,16 @@ export function sendMessage() {
|
|||||||
...secondaryChannelNames,
|
...secondaryChannelNames,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// encrypt
|
||||||
|
const encrypted = await encryptMessage();
|
||||||
|
if (encrypted == undefined) return;
|
||||||
|
|
||||||
// create object
|
// create object
|
||||||
const joinedChannelName = allChannelNames.join("/");
|
const joinedChannelName = allChannelNames.join("/");
|
||||||
const messageObject = new Message(
|
const messageObject = new Message(
|
||||||
joinedChannelName,
|
joinedChannelName,
|
||||||
senderName.value,
|
senderName.value,
|
||||||
messageBody.value,
|
encrypted,
|
||||||
new Date().toISOString()
|
new Date().toISOString()
|
||||||
);
|
);
|
||||||
const messageString = JSON.stringify(messageObject);
|
const messageString = JSON.stringify(messageObject);
|
||||||
@@ -111,6 +131,10 @@ export function sendMessage() {
|
|||||||
messageBody.value = "";
|
messageBody.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearMessageHistory() {
|
||||||
|
messages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
export function setChannel(): void {
|
export function setChannel(): void {
|
||||||
if (cannotSetChannel.value == true) return;
|
if (cannotSetChannel.value == true) return;
|
||||||
|
|
||||||
@@ -135,6 +159,7 @@ export function removeSecondaryChannel(channel: Channel): void {
|
|||||||
// LISTENERS
|
// LISTENERS
|
||||||
UDN.onconnect = () => {
|
UDN.onconnect = () => {
|
||||||
isConnected.value = true;
|
isConnected.value = true;
|
||||||
|
setChannel();
|
||||||
};
|
};
|
||||||
|
|
||||||
UDN.onmessage = (data) => {
|
UDN.onmessage = (data) => {
|
||||||
@@ -154,13 +179,12 @@ function handleSubscriptionConfirmation(
|
|||||||
channel: string,
|
channel: string,
|
||||||
isSubscribed: boolean
|
isSubscribed: boolean
|
||||||
): void {
|
): void {
|
||||||
console.log(channel, isSubscribed);
|
|
||||||
if (isSubscribed == true) {
|
if (isSubscribed == true) {
|
||||||
currentPrimaryChannel.value = channel;
|
currentPrimaryChannel.value = channel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMessage(body: string): void {
|
async function handleMessage(body: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const object: Message = JSON.parse(body);
|
const object: Message = JSON.parse(body);
|
||||||
if (!object.channel || !object.sender || !object.body || !object.isoDate)
|
if (!object.channel || !object.sender || !object.body || !object.isoDate)
|
||||||
@@ -169,9 +193,39 @@ function handleMessage(body: string): void {
|
|||||||
const messageObject = new Message(
|
const messageObject = new Message(
|
||||||
object.channel,
|
object.channel,
|
||||||
object.sender,
|
object.sender,
|
||||||
object.body,
|
await decryptMessage(object.body),
|
||||||
object.isoDate
|
object.isoDate
|
||||||
);
|
);
|
||||||
messages.add(messageObject);
|
messages.add(messageObject);
|
||||||
} catch {}
|
} 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 * as React from "bloatless-react";
|
||||||
|
|
||||||
import { currentAddress, primaryChannel } from "./model";
|
import { currentAddress } from "./model";
|
||||||
|
|
||||||
const englishTranslations = {
|
const englishTranslations = {
|
||||||
// general
|
// general
|
||||||
setInput: "Set",
|
setInput: "Set",
|
||||||
connectionStatus: "Connection status",
|
connectionStatus: "Connection status",
|
||||||
connectedTo: React.createProxyState(
|
connectedTo: React.createProxyState(
|
||||||
[currentAddress],
|
[],
|
||||||
() => `Connected to ${currentAddress}`
|
() => `Connected to ${currentAddress}`
|
||||||
),
|
),
|
||||||
disconnected: "Disconnected",
|
disconnected: "Disconnected",
|
||||||
@@ -34,7 +34,11 @@ const englishTranslations = {
|
|||||||
// messages
|
// messages
|
||||||
messages: "Messages",
|
messages: "Messages",
|
||||||
composerPlaceholder: "Type a message...",
|
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 = {
|
const allTranslations = {
|
||||||
|
|||||||
Reference in New Issue
Block a user