From 5508945a85d9405072a778eec6a90660e06e4a80 Mon Sep 17 00:00:00 2001
From: Greg Fodor <gfodor@gmail.com>
Date: Mon, 7 May 2018 18:15:57 -0700
Subject: [PATCH] WIP crypto

---
 src/utils/crypto.js       | 79 +++++++++++++++++++++++++++++++++++++
 src/utils/xfer-channel.js | 82 ++++++++++++++++++++++++---------------
 2 files changed, 129 insertions(+), 32 deletions(-)
 create mode 100644 src/utils/crypto.js

diff --git a/src/utils/crypto.js b/src/utils/crypto.js
new file mode 100644
index 000000000..53280bfc6
--- /dev/null
+++ b/src/utils/crypto.js
@@ -0,0 +1,79 @@
+// NOTE: We do not use an IV since we generate a new keypair each time we use these routines.
+
+async function deriveKey(privateKey, publicKey) {
+  return crypto.subtle.deriveKey(
+    { name: "ECDH", public: publicKey },
+    privateKey,
+    { name: "AES-CBC", length: 256 },
+    true,
+    ["encrypt", "decrypt"]
+  );
+}
+
+async function publicKeyToString(key) {
+  return JSON.stringify(await crypto.subtle.exportKey("jwk", key));
+}
+
+async function stringToPublicKey(s) {
+  return await crypto.subtle.importKey("jwk", JSON.parse(s), { name: "ECDH", namedCurve: "P-256" }, true, [
+    "deriveKey"
+  ]);
+}
+
+function stringToArrayBuffer(s) {
+  const buf = new Uint8Array(s.length);
+
+  for (let i = 0; i < s.length; i++) {
+    buf[i] = s.charCodeAt(i);
+  }
+
+  return buf;
+}
+
+function arrayBufferToString(buf) {
+  let s = "";
+
+  for (let i = 0; i < buf.byteLength; i++) {
+    s += String.fromCharCode(buf[i]);
+  }
+
+  return s;
+}
+
+// This allows a single object to be passed encrypted from a receiver in a req -> response flow
+
+// Requestor generates a public key and private key, and should send the public key to receiver.
+export async function generateKeys() {
+  const keyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"]);
+  const publicKeyString = await publicKeyToString(keyPair.publicKey);
+  return { publicKeyString, privateKey: keyPair.privateKey };
+}
+
+// Receiver takes the public key from requestor and passes obj to get a response public key and the encrypted data to return.
+export async function generatePublicKeyAndEncryptedObject(incomingPublicKeyString, obj) {
+  const iv = new Uint8Array(16);
+  const incomingPublicKey = await stringToPublicKey(incomingPublicKeyString);
+  const keyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"]);
+  const publicKeyString = await publicKeyToString(keyPair.publicKey);
+  const secret = await deriveKey(keyPair.privateKey, incomingPublicKey);
+  const encryptedData = btoa(
+    await crypto.subtle.encrypt({ name: "AES-CBC", iv }, secret, stringToArrayBuffer(JSON.stringify(obj)))
+  );
+
+  return { publicKeyString, encryptedData };
+}
+
+// Requestor then takes the receiver's public key, the private key (returned from generateKeys()), and the data from the receiver.
+export async function decryptObject(publicKeyString, privateKey, base64value) {
+  const iv = new Uint8Array(16);
+  const publicKey = await publicKeyToString(publicKeyString);
+  const secret = await deriveKey(privateKey, publicKey);
+
+  return JSON.parse(
+    arrayBufferToString(
+      new Uint8Array(
+        await crypto.subtle.decrypt({ name: "AES-CBC", iv }, secret, stringToArrayBuffer(atob(base64value)))
+      )
+    )
+  );
+}
diff --git a/src/utils/xfer-channel.js b/src/utils/xfer-channel.js
index 15c4c0fc2..ac77f1617 100644
--- a/src/utils/xfer-channel.js
+++ b/src/utils/xfer-channel.js
@@ -1,3 +1,5 @@
+import { generatePublicKeyAndEncryptedObject, generateKeys, decryptObject } from "./crypto";
+
 export default class XferChannel {
   constructor(store) {
     this.store = store;
@@ -46,17 +48,27 @@ export default class XferChannel {
 
           channel.on("xfer_request", incoming => {
             if (readyToSend) {
-              const payload = { path: location.pathname, target_session_id: incoming.reply_to_session_id };
+              const data = { path: location.pathname };
 
               // Copy profile data to xfer'ed device if it's been set.
               if (this.store.state.activity.hasChangedName) {
-                payload.profile = { ...this.store.state.profile };
+                data.profile = { ...this.store.state.profile };
               }
 
-              channel.push("xfer_response", payload);
-              channel.leave();
-              finished("used");
-              readyToSend = false;
+              this.generatePublicKeyAndEncryptedObject(incoming.public_key).then(
+                ({ publicKeyString, encryptedData }) => {
+                  const payload = {
+                    target_session_id: incoming.reply_to_session_id,
+                    public_key: publicKeyString,
+                    data: encryptedData
+                  };
+
+                  channel.push("xfer_response", payload);
+                  channel.leave();
+                  finished("used");
+                  readyToSend = false;
+                }
+              );
             }
           });
 
@@ -77,34 +89,40 @@ export default class XferChannel {
       const channel = this.socket.channel(`xfer:${code}`, { timeout: 10000 });
       let finished = false;
 
-      channel.on("presence_state", state => {
-        const numOccupants = Object.keys(state).length;
-
-        if (numOccupants === 1) {
-          // Great, only sender is in topic, request xfer
-          channel.push("xfer_request", { reply_to_session_id: this.socket.params.session_id });
-
-          setTimeout(() => {
-            if (finished) return;
-            channel.leave();
-            reject("no_response");
-          }, 10000);
-        } else if (numOccupants === 0) {
-          // Nobody in this channel, probably a bad code.
-          reject("failed");
-        } else {
-          console.warn("xfer code channel already has 2 or more occupants, something fishy is going on.");
-          reject("in_use");
-        }
-      });
+      generateKeys().then(({ publicKeyString, privateKey }) => {
+        channel.on("presence_state", state => {
+          const numOccupants = Object.keys(state).length;
 
-      channel.on("xfer_response", payload => {
-        finished = true;
-        channel.leave();
-        resolve(payload);
-      });
+          if (numOccupants === 1) {
+            // Great, only sender is in topic, request xfer
+            channel.push("xfer_request", {
+              reply_to_session_id: this.socket.params.session_id,
+              public_key: publicKeyString
+            });
 
-      channel.join().receive("error", r => console.error(r));
+            setTimeout(() => {
+              if (finished) return;
+              channel.leave();
+              reject("no_response");
+            }, 10000);
+          } else if (numOccupants === 0) {
+            // Nobody in this channel, probably a bad code.
+            reject("failed");
+          } else {
+            console.warn("xfer code channel already has 2 or more occupants, something fishy is going on.");
+            reject("in_use");
+          }
+        });
+
+        channel.on("xfer_response", payload => {
+          finished = true;
+          channel.leave();
+
+          this.decryptObject(payload.public_key, privateKey, payload.data).then(resolve);
+        });
+
+        channel.join().receive("error", r => console.error(r));
+      });
     });
   };
 }
-- 
GitLab