end-to-end
encryption

Requirements

  • Cipher / Decipher large files
  • Server must not be able to read files
  • Multiple access point
  • Users can share files
  • Groups concepts

Javascript Encryption

Web Cryptography API

  • Encryption
    • RSA
    • AES
  • Sign
    • RSA
    • AES
    • ECDSA
    • HMAC

Generate key

window.crypto.subtle.generateKey(
    {
        name: "RSA-OAEP",
        modulusLength: 2048, //can be 1024, 2048, or 4096
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
    },
    true, //whether the key is extractable (i.e. can be used in exportKey)
    ["encrypt", "decrypt"] //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
)
.then(function(key){
    window.demoKey = key;
});
Object {
  publicKey: CryptoKey {
    algorithm: Object,
    extractable: true,
    type: "private",
    usages: ["decrypt"]
  },
  privateKey: CryptoKey {
    algorithm: Object,
    extractable: true,
    type: "public",
    usages: ["encrypt"]
  }
}

Export key

window.crypto.subtle.exportKey(
    "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
    window.demoKey.publicKey //can be a publicKey or privateKey, as long as extractable was true
)
.then(function(keyData){
    window.demoPublicJwk = keyData;
});
Object {
  alg: "RSA-OAEP-256"
  e: "AQAB"
  ext: true
  key_ops: ["encrypt"],
  kty: "RSA"
  n : "8khgF5fQz7OR209ASxTUyz7Oijayy-1kPRoy6MXF2L8cxVsqulk1jiJjyeXJcJoWhMfNVxHYblOQRFzuvhIuusrCJVI9RE5dun1EPydPTehEA8nmtbAkwBDQdU_Vwngyohk6FLrAMsD8fWbZpuacfxgdczLJimWbenZl5CIE2zHHsBS3-OcS7y7fDNdK1Am00QaUo2MeettlTMnlgO0LqzBSfymENMWG5lvsKvID8I7pjZaHZ38dwUXoCd5hued-ihn0cTOdujOecR6uzMu8ZPFumJu0HKqaM3_uJoUNfMw3c3RV2PaPVuPdHMNAib0S8clBXPbRe1ApqgytpCqSgQ"
}

Import key

window.crypto.subtle.importKey(
    "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
    window.demoPublicJwk,
    {   //these are the algorithm options
        name: "RSA-OAEP",
        hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
    },
    false, //whether the key is extractable (i.e. can be used in exportKey)
    ["encrypt"] //"verify" for public key import, "sign" for private key imports
)
.then(function(publicKey){
    console.log(publicKey);
});
CryptoKey {
  algorithm: {
    hash: {
      name: "SHA-256"
    },
    modulusLength: 2048,
    name: "RSA-OAEP",
    publicExponent: Uint8Array[1, 0, 1]
  }
  extractable: false,
  type: "public",
  usages: ["encrypt"]
}

Converter

var converter = {
    arrayBufferToBase64: function (buffer) {
        return window.btoa(this.arrayBufferToString(buffer));
    },
    base64ToArrayBuffer: function (base64) {
        return this.stringToArrayBuffer(window.atob(base64));
    },

    stringToArrayBuffer: function (str) {
        var buf = new ArrayBuffer(str.length);
        var bufView = new Uint8Array(buf);
        for (var i = 0, strLen = str.length; i < strLen; i++) {
            bufView[i] = str.charCodeAt(i);
        }
        return buf;
    },
    arrayBufferToString(buf) {
        var bufView = new Uint8Array(buf);
        var unis = "";
        for (var i = 0; i < bufView.length; i++) {
            unis = unis + String.fromCharCode(bufView[i]);
        }
        return unis;
    },
};

Cipher

window.crypto.subtle.encrypt(
    {
        name: "RSA-OAEP",
        //label: Uint8Array([...]) //optional
    },
    window.demoKey.publicKey, //from generateKey or importKey above
    converter.stringToArrayBuffer("demoData") //ArrayBuffer of data you want to encrypt
)
.then(function(ciphered){
    window.demoCiphered = converter.arrayBufferToString(ciphered);
});
"YFòð©³ê=´4©À?¡XúÅÉG=$?%7Šzn–E¨Ãáfi*“öež6r›úSê GIÿ³Ä‡ÊSózM§SA4\+°ˆE[C×ͨ·ѩF‹s›yç,ÊKV
÷3hþ÷Ìa°ª;-@YöaTèÑGÐyyÕYÿByʖ– ù(,+ …ˆžÈ›±õ#Ž£Q¾¾Vø-Ç&‹íز}\™7°+ŽëÙ<¢+.P±•/%F¸o:e…>Rƒì¤²ŽÓpàcg÷3ÝÀ4lvÅx4Wíg=~&=-±uò5é…ðUŸ°Wø/“Ð×UŽ?*î²7Óäû;"

Decipher

window.crypto.subtle.decrypt(
    {
        name: "RSA-OAEP",
        //label: Uint8Array([...]) //optional
    },
    window.demoKey.privateKey, //from generateKey or importKey above
    converter.stringToArrayBuffer(window.demoCiphered) //ArrayBuffer of the data
)
.then(function(deciphered){
    window.demoDeciphered = converter.arrayBufferToString(deciphered);
});

"demoData"

Upload

File API

function FileReader(file) {
  this.file = file;
  this.pointer = 0;
}
FileIO.prototype.eof = function () {
  return this.pointer >= this.file.size;
};
FileIO.prototype.read = function (length) {
  return new Promise(function (resolve, reject) {
    var slice = this.file.slice(this.pointer, this.pointer + length);
    this.pointer += length;

    var reader = new FileReader();
    reader.onload = function (e) {
      resolve(new Uint8Array(e.target.result));
    };
    reader.readAsArrayBuffer(slice);
  }.bind(this));
};
<input type="file" onchange="upload(this.files[0])"/>

<script>
function upload(file) {
  http.post('/api/files', {}).then(function (fileItem) {
    return uploadNextBlock(window.demoFileKey, fileItem, new FileReader(file))
  })
}
</script>

Upload block

function uploadNextBlock(fileKey, fileItem, fileReader) {
  return fileReader.read(
      1024 * 1024
  ).then(function (block) {
    var iv = window.crypto.getRandomValues(new Uint8Array(16));
    return window.crypto.subtle.encrypt({
        name: "AES-CBC",
        iv: iv,
      },
      fileKey,
      block
    ).then(function (ciphered) {
      var tmp = new Uint8Array(16 + ciphered.byteLength);
      tmp.set(iv, 0);
      tmp.set(new Uint8Array(ciphered), 16);
      return tmp;
    });
  }).then(function (ciphered) {
    return http.post('/api/files/' + fileItem.id + '/blocks', {
      'content': converter.arrayBufferToBase64(ciphered)
    });
  }).then(function () {
    if (!fileReader.eof()) {
      return uploadNextBlock(fileKey, fileItem, fileReader);
    } else {
      return fileItem;
    }
  });
}

Download

FileSystem API

function FileTempHtml5(fileName) {
  this.entryPromise = new Promise(function (resolve, reject) {
    window.requestFileSystem(
        window.TEMPORARY,
        1024 * 1024,
        function (fs) {
          fs.root.getFile(
            fileName,
            {create: true},
            function (fileEntry) {resolve(fileEntry);},
            function (err) {reject(err)}
          );
        }
    );
  });
  this.writerPromise = new Promise(function (resolve, reject) {
    this.entryPromise.then(function (fileEntry) {
      fileEntry.createWriter(function (fileWriter) {
        fileWriter.truncate(0);
        resolve(fileWriter);
      }, reject);
    }, function (err) {
      reject(err);
    });
  }.bind(this));
}

FileSystem API

FileTempHtml5.prototype.write = function (content) {
    return this.writerPromise.then(function (fileWriter) {
        return new Promise(function (resolve, reject) {
            fileWriter.onwriteend = resolve;
            fileWriter.onerror = reject;

            fileWriter.write(new Blob([content]));
        });
    });
};

FileTempHtml5.prototype.download = function () {
    this.entryPromise.then(function (fileEntry) {
        var elem = document.createElement('a');
        elem.download = fileEntry.name;
        elem.href = fileEntry.toURL();
        document.body.appendChild(elem);

        elem.click();

        document.body.removeChild(elem);

        setTimeout(function () {fileEntry.remove(function(){});}, 1000);
    });
};

Download block

function downloadNextBlock(fileKey, fileWriter, index, fileItem) {
  http.request(
    'get', '/api/files/' + fileItem.id + '/blocks/' + index
  ).then(function (ciphered) {
    return window.crypto.subtle.decrypt(
      {
        name: "AES-CFB-8",
        iv: ciphered.slice(0, 16)
      },
      fileKey
      ciphered.slice(16)
    )
  }).then(function (deciphered) {
    return fileWriter.write(new Uint8Array(deciphered));
  }).then(function () {
    if (index + 1 < fileItem.blocksLength) {
      downloadNextBlock(fileWriter, index + 1);
    } else {
      fileWriter.download();
    }
  });
}

multiplatform

Solution

  • Generate the personal key
  • Cipher the key with a master password
  • Store the key in the server

Generate MasterKey

crypto.subtle.digest(
  {name: "SHA-256"}, converter.stringToArrayBuffer(prompt("Master Password?"))
).then(function (result) {
  return window.crypto.subtle.importKey(
    "raw",
    result,
    {name: "AES-CBC"},
    true,
    ["encrypt", "decrypt"]
  );
}).then(function (masterKey) {
    window.demoMasterKey = masterKey;
});

Generate a Personal Key

window.crypto.subtle.generateKey({
    name: "RSA-OAEP",
    modulusLength: 2048,
    publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
    hash: {name: "SHA-256"},
  },
  true,
  ["encrypt", "decrypt"]
).then(function (key) {
  window.demoPersonalKey = key;
});

Cipher Personal Key

Promise.all([
  // export publicKey
  window.crypto.subtle.exportKey("spki", window.demoPersonalKey.publicKey),
  // export privateKey
  window.crypto.subtle.exportKey("pkcs8", window.demoPersonalKey.privateKey)
]).then(function (responses) {
  // Cipher private key
  var iv = window.crypto.getRandomValues(new Uint8Array(16));
  return window.crypto.subtle.encrypt({name: "AES-CBC", iv: iv},
    window.demoMasterKey,
    responses[1]
  ).then(function (encrypted) {
    var tmp = new Uint8Array(16 + encrypted.byteLength);
    tmp.set(iv, 0);
    tmp.set(new Uint8Array(encrypted), 16);
  
    return [responses[0], tmp];
  });
}).then(function (responses) {
  var payload = {
    algorithm: window.demoPersonalKey.publicKey.algorithm.name,
    publicKey: converter.arrayBufferToBase64(responses[0]),
    cipheredPrivateKey: converter.arrayBufferToBase64(responses[1])
  };

  return http.post(
    '/api/keys', payload
  });
});

Decipher Personal Key

http.get('/api/keys/' + id).then(function (keyItem) {
  window.demoPersonalKey = {};

  window.crypto.subtle.importKey(
    "spki",
    converter.base64ToArrayBuffer(keyItem.publicKey),
    {name: "RSA-OAEP", hash: {name: "SHA-256"}},
    true,
    ["encrypt"]
  ).then(function (key) {
    window.demoPersonalKey.publicKey = key;
  });

  const cipheredKey = converter.base64ToArrayBuffer(cryptoKey.cipheredPrivateKey)
  return window.crypto.subtle.decrypt(
    {name: "AES-CBC", iv: cipheredKey.slice(0, 16)},
    window.demoMasterKey,
    cipheredKey.slice(16)
  ).then(function (decipheredPrivateKey) {
    return window.crypto.subtle.importKey(
      "pkcs8",
      decipheredPrivateKey,
      {name: "RSA-OAEP", hash: {name: "SHA-256"}},
      true,
      ["decrypt"]
    );
  });
}).then(function (key) {
  window.demoPersonalKey.privateKey = key;
});

Sharing

Solution

  • Use one key per file
  • Cipher file's key with user's personal public key
  • store the ciphered key in the server

Sharing

function shareFile(fileKey, fileItem, targetKeyId) {
  http.get('/api/keys/' + targetKeyId).then(function (keyItem) {
    return Promise.all([
      window.crypto.subtle.importKey(
        "spki",
        converter.base64ToArrayBuffer(keyItem.publicKey),
        {
          name: "RSA-OAEP",
          hash: {name: "SHA-256"},
        },
        true,
        ["encrypt"]
      ),
      window.crypto.subtle.exportKey("jwk", fileKey)
    ]);
  }).then(function (results) {
    var fileKey = converter.stringToArrayBuffer(JSON.stringify(results[1]));
    var targetKey = results[0];
    return rsaCrypter.encrypt(targetKey, fileKey)
  }).then(function (cipĥeredKey) {
    return http.post('/api/files/' + fileItem.id + '/keys', {
      encoder: {
        id: targetKeyId
      },
      encryptedKey: converter.arrayBufferToBase64(cipĥeredKey)
    });
  });
}

Group sharing

Cipher chain

  • Generate the group's Key
  • Cipher the group's Key with the member's public key
  • Store the ciphered key in the server

Use the group's key

  • Retrieves the ciphered group's key
  • Retrieves file's Key
  • Decipher group's key with the personal Key
  • Decipher file's Key with the group's Key
  • Decipher document with the file's Key

Add a new member

  • Retrieves the ciphered group's key
  • Decipher group's key our personal Key
  • Retrieves member's personalKey
  • Cipher group's key with the member public key
  • Store the ciphered key in the server

Demo

Questions

End to end encryption

By Jérémy DERUSSÉ