diff --git a/apps/android/app/src/main/assets/www/call.js b/apps/android/app/src/main/assets/www/call.js index b5381b3cd9..dd71ea87a2 100644 --- a/apps/android/app/src/main/assets/www/call.js +++ b/apps/android/app/src/main/assets/www/call.js @@ -431,8 +431,9 @@ function callCryptoFunction() { }; } function decodeAesKey(aesKey) { - const keyData = callCrypto.decodeBase64(callCrypto.encodeAscii(aesKey)); - return crypto.subtle.importKey("raw", keyData, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); + const keyData = callCrypto.decodeBase64url(callCrypto.encodeAscii(aesKey)); + console.log("keyData", keyData); + return crypto.subtle.importKey("raw", keyData, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); } function concatN(...bs) { const a = new Uint8Array(bs.reduce((size, b) => size + b.byteLength, 0)); @@ -445,9 +446,9 @@ function callCryptoFunction() { function randomIV() { return crypto.getRandomValues(new Uint8Array(IV_LENGTH)); } - const base64chars = new Uint8Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("").map((c) => c.charCodeAt(0))); - const base64lookup = new Array(256); - base64chars.forEach((c, i) => (base64lookup[c] = i)); + const base64urlChars = new Uint8Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split("").map((c) => c.charCodeAt(0))); + const base64urlLookup = new Array(256); + base64urlChars.forEach((c, i) => (base64urlLookup[c] = i)); const char_equal = "=".charCodeAt(0); function encodeAscii(s) { const a = new Uint8Array(s.length); @@ -462,16 +463,16 @@ function callCryptoFunction() { s += String.fromCharCode(a[i]); return s; } - function encodeBase64(a) { + function encodeBase64url(a) { const len = a.length; const b64len = Math.ceil(len / 3) * 4; const b64 = new Uint8Array(b64len); let j = 0; for (let i = 0; i < len; i += 3) { - b64[j++] = base64chars[a[i] >> 2]; - b64[j++] = base64chars[((a[i] & 3) << 4) | (a[i + 1] >> 4)]; - b64[j++] = base64chars[((a[i + 1] & 15) << 2) | (a[i + 2] >> 6)]; - b64[j++] = base64chars[a[i + 2] & 63]; + b64[j++] = base64urlChars[a[i] >> 2]; + b64[j++] = base64urlChars[((a[i] & 3) << 4) | (a[i + 1] >> 4)]; + b64[j++] = base64urlChars[((a[i + 1] & 15) << 2) | (a[i + 2] >> 6)]; + b64[j++] = base64urlChars[a[i + 2] & 63]; } if (len % 3) b64[b64len - 1] = char_equal; @@ -479,7 +480,7 @@ function callCryptoFunction() { b64[b64len - 2] = char_equal; return b64; } - function decodeBase64(b64) { + function decodeBase64url(b64) { let len = b64.length; if (len % 4) return; @@ -496,10 +497,10 @@ function callCryptoFunction() { let i = 0; let pos = 0; while (i < len) { - const enc1 = base64lookup[b64[i++]]; - const enc2 = i < len ? base64lookup[b64[i++]] : 0; - const enc3 = i < len ? base64lookup[b64[i++]] : 0; - const enc4 = i < len ? base64lookup[b64[i++]] : 0; + const enc1 = base64urlLookup[b64[i++]]; + const enc2 = i < len ? base64urlLookup[b64[i++]] : 0; + const enc3 = i < len ? base64urlLookup[b64[i++]] : 0; + const enc4 = i < len ? base64urlLookup[b64[i++]] : 0; if (enc1 === undefined || enc2 === undefined || enc3 === undefined || enc4 === undefined) return; bytes[pos++] = (enc1 << 2) | (enc2 >> 4); @@ -513,8 +514,8 @@ function callCryptoFunction() { decodeAesKey, encodeAscii, decodeAscii, - encodeBase64, - decodeBase64, + encodeBase64url, + decodeBase64url, }; } // If the worker is used for decryption, this function code (as string) is used to load the worker via Blob diff --git a/package.yaml b/package.yaml index 784ba532af..fe136ddad9 100644 --- a/package.yaml +++ b/package.yaml @@ -26,6 +26,7 @@ dependencies: - email-validate == 2.3.* - exceptions == 0.10.* - filepath == 1.4.* + - http-types == 0.12.* - mtl == 2.2.* - optparse-applicative >= 0.15 && < 0.17 - process == 1.6.* diff --git a/packages/simplex-chat-webrtc/package.json b/packages/simplex-chat-webrtc/package.json index 86fe6c0fb0..6b33d7292d 100644 --- a/packages/simplex-chat-webrtc/package.json +++ b/packages/simplex-chat-webrtc/package.json @@ -1,17 +1,30 @@ { - "name": "simplex-chat-webrtc", - "version": "0.0.1", - "description": "WebRTC call in browser and webview", + "name": "@simplex-chat/webrtc", + "version": "0.0.2", + "description": "WebRTC call in browser and webview for SimpleX Chat clients", "main": "dist/call.js", + "types": "dist/call.d.ts", + "files": [ + "dist" + ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "prettier --write --ignore-unknown . && tsc && ./copy" }, + "repository": { + "type": "git", + "url": "git+https://github.com/simplex-chat/simplex-chat.git" + }, "keywords": [ "SimpleX", "WebRTC" ], - "author": "", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-webrtc#readme", + "author": "SimpleX Chat", "license": "AGPL-3.0-or-later", "devDependencies": { "@types/lz-string": "^1.3.34", diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index b5fa306870..f21210d2d6 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -569,8 +569,8 @@ interface CallCrypto { decodeAesKey: (aesKey: string) => Promise encodeAscii: (s: string) => Uint8Array decodeAscii: (a: Uint8Array) => string - encodeBase64: (a: Uint8Array) => Uint8Array - decodeBase64: (b64: Uint8Array) => Uint8Array | undefined + encodeBase64url: (a: Uint8Array) => Uint8Array + decodeBase64url: (b64: Uint8Array) => Uint8Array | undefined } interface RTCEncodedVideoFrame { @@ -624,8 +624,8 @@ function callCryptoFunction(): CallCrypto { } function decodeAesKey(aesKey: string): Promise { - const keyData = callCrypto.decodeBase64(callCrypto.encodeAscii(aesKey)) - return crypto.subtle.importKey("raw", keyData!, {name: "AES-GCM", length: 256}, false, ["encrypt", "decrypt"]) + const keyData = callCrypto.decodeBase64url(callCrypto.encodeAscii(aesKey)) + return crypto.subtle.importKey("raw", keyData!, {name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"]) } function concatN(...bs: Uint8Array[]): Uint8Array { @@ -641,12 +641,12 @@ function callCryptoFunction(): CallCrypto { return crypto.getRandomValues(new Uint8Array(IV_LENGTH)) } - const base64chars = new Uint8Array( - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("").map((c) => c.charCodeAt(0)) + const base64urlChars = new Uint8Array( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split("").map((c) => c.charCodeAt(0)) ) - const base64lookup = new Array(256) as (number | undefined)[] - base64chars.forEach((c, i) => (base64lookup[c] = i)) + const base64urlLookup = new Array(256) as (number | undefined)[] + base64urlChars.forEach((c, i) => (base64urlLookup[c] = i)) const char_equal = "=".charCodeAt(0) @@ -663,17 +663,17 @@ function callCryptoFunction(): CallCrypto { return s } - function encodeBase64(a: Uint8Array): Uint8Array { + function encodeBase64url(a: Uint8Array): Uint8Array { const len = a.length const b64len = Math.ceil(len / 3) * 4 const b64 = new Uint8Array(b64len) let j = 0 for (let i = 0; i < len; i += 3) { - b64[j++] = base64chars[a[i] >> 2] - b64[j++] = base64chars[((a[i] & 3) << 4) | (a[i + 1] >> 4)] - b64[j++] = base64chars[((a[i + 1] & 15) << 2) | (a[i + 2] >> 6)] - b64[j++] = base64chars[a[i + 2] & 63] + b64[j++] = base64urlChars[a[i] >> 2] + b64[j++] = base64urlChars[((a[i] & 3) << 4) | (a[i + 1] >> 4)] + b64[j++] = base64urlChars[((a[i + 1] & 15) << 2) | (a[i + 2] >> 6)] + b64[j++] = base64urlChars[a[i + 2] & 63] } if (len % 3) b64[b64len - 1] = char_equal @@ -682,7 +682,7 @@ function callCryptoFunction(): CallCrypto { return b64 } - function decodeBase64(b64: Uint8Array): Uint8Array | undefined { + function decodeBase64url(b64: Uint8Array): Uint8Array | undefined { let len = b64.length if (len % 4) return let bLen = (len * 3) / 4 @@ -701,10 +701,10 @@ function callCryptoFunction(): CallCrypto { let i = 0 let pos = 0 while (i < len) { - const enc1 = base64lookup[b64[i++]] - const enc2 = i < len ? base64lookup[b64[i++]] : 0 - const enc3 = i < len ? base64lookup[b64[i++]] : 0 - const enc4 = i < len ? base64lookup[b64[i++]] : 0 + const enc1 = base64urlLookup[b64[i++]] + const enc2 = i < len ? base64urlLookup[b64[i++]] : 0 + const enc3 = i < len ? base64urlLookup[b64[i++]] : 0 + const enc4 = i < len ? base64urlLookup[b64[i++]] : 0 if (enc1 === undefined || enc2 === undefined || enc3 === undefined || enc4 === undefined) return bytes[pos++] = (enc1 << 2) | (enc2 >> 4) bytes[pos++] = ((enc2 & 15) << 4) | (enc3 >> 2) @@ -719,8 +719,8 @@ function callCryptoFunction(): CallCrypto { decodeAesKey, encodeAscii, decodeAscii, - encodeBase64, - decodeBase64, + encodeBase64url, + decodeBase64url, } } diff --git a/packages/simplex-chat-webrtc/src/ui.js b/packages/simplex-chat-webrtc/src/ui.js index 09b413763c..3ef8d32f2d 100644 --- a/packages/simplex-chat-webrtc/src/ui.js +++ b/packages/simplex-chat-webrtc/src/ui.js @@ -1,103 +1,113 @@ ;(async function run() { - const START_E2EE_CALL_BTN = "start-e2ee-call" - const START_CALL_BTN = "start-call" + // const START_E2EE_CALL_BTN = "start-e2ee-call" + // const START_CALL_BTN = "start-call" const URL_FOR_PEER = "url-for-peer" - const COPY_URL_FOR_PEER_BTN = "copy-url-for-peer" + // const COPY_URL_FOR_PEER_BTN = "copy-url-for-peer" const DATA_FOR_PEER = "data-for-peer" - const COPY_DATA_FOR_PEER_BTN = "copy-data-for-peer" + // const COPY_DATA_FOR_PEER_BTN = "copy-data-for-peer" const PASS_DATA_TO_PEER_TEXT = "pass-data-to-peer" - const CHAT_COMMAND_FOR_PEER = "chat-command-for-peer" + const SIMPLEX_CHAT_COMMAND = "simplex-chat-command" + const COPY_SIMPLEX_CHAT_COMMAND_BTN = "copy-simplex-chat-command" const COMMAND_TO_PROCESS = "command-to-process" const PROCESS_COMMAND_BTN = "process-command" const urlForPeer = document.getElementById(URL_FOR_PEER) const dataForPeer = document.getElementById(DATA_FOR_PEER) const passDataToPeerText = document.getElementById(PASS_DATA_TO_PEER_TEXT) - const chatCommandForPeer = document.getElementById(CHAT_COMMAND_FOR_PEER) + const simplexChatCommand = document.getElementById(SIMPLEX_CHAT_COMMAND) const commandToProcess = document.getElementById(COMMAND_TO_PROCESS) const processCommandButton = document.getElementById(PROCESS_COMMAND_BTN) - const startE2EECallButton = document.getElementById(START_E2EE_CALL_BTN) - const {resp} = await processCommand({command: {type: "capabilities", useWorker: true}}) - if (resp?.capabilities?.encryption) { - startE2EECallButton.onclick = startCall(true) - } else { - startE2EECallButton.style.display = "none" - } - const startCallButton = document.getElementById(START_CALL_BTN) - startCallButton.onclick = startCall() - const copyUrlButton = document.getElementById(COPY_URL_FOR_PEER_BTN) - copyUrlButton.onclick = () => { - navigator.clipboard.writeText(urlForPeer.innerText) - commandToProcess.style.display = "" - processCommandButton.style.display = "" - } - const copyDataButton = document.getElementById(COPY_DATA_FOR_PEER_BTN) - copyDataButton.onclick = () => { - navigator.clipboard.writeText(dataForPeer.innerText) - commandToProcess.style.display = "" - processCommandButton.style.display = "" + // const startE2EECallButton = document.getElementById(START_E2EE_CALL_BTN) + // const {resp} = await processCommand({command: {type: "capabilities", useWorker: true}}) + // if (resp?.capabilities?.encryption) { + // startE2EECallButton.onclick = startCall(true) + // } else { + // startE2EECallButton.style.display = "none" + // } + // const startCallButton = document.getElementById(START_CALL_BTN) + // startCallButton.onclick = startCall() + // const copyUrlButton = document.getElementById(COPY_URL_FOR_PEER_BTN) + // copyUrlButton.onclick = () => { + // navigator.clipboard.writeText(urlForPeer.innerText) + // commandToProcess.style.display = "" + // processCommandButton.style.display = "" + // } + // const copyDataButton = document.getElementById(COPY_DATA_FOR_PEER_BTN) + // copyDataButton.onclick = () => { + // navigator.clipboard.writeText(dataForPeer.innerText) + // commandToProcess.style.display = "" + // processCommandButton.style.display = "" + // } + const copySimplexChatCommandButton = document.getElementById(COPY_SIMPLEX_CHAT_COMMAND_BTN) + copySimplexChatCommandButton.onclick = () => { + navigator.clipboard.writeText(simplexChatCommand.innerText) + if (simplexChatCommand.innerText.startsWith("/_call offer")) { + commandToProcess.style.display = "" + processCommandButton.style.display = "" + } } processCommandButton.onclick = () => { sendCommand(JSON.parse(commandToProcess.value)) commandToProcess.value = "" } const parsed = new URLSearchParams(document.location.hash.substring(1)) - let apiCallStr = parsed.get("command") - if (apiCallStr) { - startE2EECallButton.style.display = "none" - startCallButton.style.display = "none" - await sendCommand(JSON.parse(decodeURIComponent(apiCallStr))) + let commandStr = parsed.get("command") + if (commandStr) { + // startE2EECallButton.style.display = "none" + // startCallButton.style.display = "none" + await sendCommand(JSON.parse(decodeURIComponent(commandStr))) } - function startCall(encryption) { - return async () => { - let aesKey - if (encryption) { - const key = await crypto.subtle.generateKey({name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"]) - const keyBytes = await crypto.subtle.exportKey("raw", key) - aesKey = callCrypto.decodeAscii(callCrypto.encodeBase64(new Uint8Array(keyBytes))) - } - sendCommand({command: {type: "start", media: "video", aesKey, useWorker: true}}) - startE2EECallButton.style.display = "none" - startCallButton.style.display = "none" - } - } + // function startCall(encryption) { + // return async () => { + // let aesKey + // if (encryption) { + // const key = await crypto.subtle.generateKey({name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"]) + // const keyBytes = await crypto.subtle.exportKey("raw", key) + // aesKey = callCrypto.decodeAscii(callCrypto.encodeBase64url(new Uint8Array(keyBytes))) + // } + // startE2EECallButton.style.display = "none" + // startCallButton.style.display = "none" + // await sendCommand({type: "start", media: "video", aesKey, useWorker: true}) + // } + // } - async function sendCommand(apiCall) { + async function sendCommand(command) { try { - console.log(apiCall) - const {command} = apiCall - const {resp} = await processCommand(apiCall) + console.log(command) + const {resp} = await processCommand({command}) console.log(resp) switch (resp.type) { case "offer": { - const {media, aesKey} = command + const {media} = command const {offer, iceCandidates, capabilities} = resp - const peerWCommand = { - command: {type: "offer", offer, iceCandidates, media, aesKey: capabilities.encryption ? aesKey : undefined, useWorker: true}, - } + const aesKey = capabilities.encryption ? command.aesKey : undefined + const peerWCommand = {type: "offer", offer, iceCandidates, media, aesKey, useWorker: true} const url = new URL(document.location) parsed.set("command", encodeURIComponent(JSON.stringify(peerWCommand))) url.hash = parsed.toString() urlForPeer.innerText = url.toString() dataForPeer.innerText = JSON.stringify(peerWCommand) - copyUrlButton.style.display = "" - copyDataButton.style.display = "" - // const webRTCCallOffer = {callType: {media, capabilities}, rtcSession: {rtcSession: offer, rtcIceCandidates: iceCandidates}} - // const peerChatCommand = `/_call @${parsed.contact} offer ${JSON.stringify(webRTCCallOffer)}` - // chatCommandForPeer.innerText = peerChatCommand + const webRTCCallOffer = {callType: {media, capabilities}, rtcSession: {rtcSession: offer, rtcIceCandidates: iceCandidates}} + const peerChatCommand = `/_call offer @${parsed.get("contact_id")} ${JSON.stringify(webRTCCallOffer)}` + simplexChatCommand.innerText = peerChatCommand + + // copyUrlButton.style.display = "" + // copyDataButton.style.display = "" + copySimplexChatCommandButton.style.display = "" return } case "answer": { const {answer, iceCandidates} = resp - const peerWCommand = {command: {type: "answer", answer, iceCandidates}} + const peerWCommand = {type: "answer", answer, iceCandidates} dataForPeer.innerText = JSON.stringify(peerWCommand) - copyUrlButton.style.display = "none" - copyDataButton.style.display = "" - // const webRTCSession = {rtcSession: answer, rtcIceCandidates: iceCandidates} - // const peerChatCommand = `/_call @${parsed.contact} answer ${JSON.stringify(webRTCSession)}` - // chatCommandForPeer.innerText = peerChatCommand + const webRTCSession = {rtcSession: answer, rtcIceCandidates: iceCandidates} + const peerChatCommand = `/_call answer @${parsed.get("contact_id")} ${JSON.stringify(webRTCSession)}` + // copyUrlButton.style.display = "none" + // copyDataButton.style.display = "" + copySimplexChatCommandButton.style.display = "" + simplexChatCommand.innerText = peerChatCommand return } case "ok": diff --git a/packages/simplex-chat-webrtc/src/webcall.html b/packages/simplex-chat-webrtc/src/webcall.html index e648df8537..1e1fad9e12 100644 --- a/packages/simplex-chat-webrtc/src/webcall.html +++ b/packages/simplex-chat-webrtc/src/webcall.html @@ -21,10 +21,11 @@
- + +
@@ -41,7 +42,7 @@
- +