Mozilla Test Pilot Team has released Firefox Send. This post describes how safe Firefox Send is by reading code. Note that The team blog post announces the safety of their service:

Your files are encrypted during transmission. Plus, Send encrypts files on the client side, so that not even Mozilla can read them.

TL;DR

As of v1.1.0,

  • File data is encrypted on the client side during uploading.
  • File data is decrypted on the client side during downloading.
  • File name is not encrypted and stored on the server.
  • Secret Key is generated by the client side and shared with the sender/receiver persons by hash (#) of the download link URL. It is never sent to the server.

Client side: FileSender#upload()

This clause describes FileSender#upload() method defined in frontend/src/fileSender.js (to upload with client-side encryption).

first Promise : Generate Key and Load File

return Promise.all([
      window.crypto.subtle.generateKey(
        {
          name: 'AES-GCM',
          length: 128
        },
        true,
        ['encrypt', 'decrypt']
      ),
      new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsArrayBuffer(this.file);
        reader.onload = function(event) {
          const plaintext = new Uint8Array(this.result);
          resolve(plaintext);
        };
        reader.onerror = function(err) {
          reject(err);
        };
      })
])

The first promise is resolved when both of following two procedures are completed:

  • Generate Key:
    • using generateKey() of Web Crypto API;
    • with a symmetric algorithm "AES-GCM" (AES in Galois/Counter Mode);
    • with 128-bit key length.
  • Read file

second Promise : Encrypt and Export key

.then(([secretKey, plaintext]) => {
        self.emit('encrypting');
        return Promise.all([
          window.crypto.subtle.encrypt(
            {
              name: 'AES-GCM',
              iv: this.iv,
              tagLength: 128
            },
            secretKey,
            plaintext
          ),
          window.crypto.subtle.exportKey('jwk', secretKey)
]);

The second promise is resolved when both of following two procedures are completed:

  • Encrypt file data plaintext
    • using encrypt() of Web Crypto API;
    • with 128-bit authentication tag length.
  • Export key

third Promise : Upload

.then(([encrypted, keydata]) => {
       return new Promise((resolve, reject) => {
          const file = this.file;
          const fileId = arrayToHex(this.iv);
          const dataView = new DataView(encrypted);
          const blob = new Blob([dataView], { type: file.type });
          const fd = new FormData();
          fd.append('data', blob, file.name);

          // ****** snip (describe the next section...) ******

          xhr.open('post', '/upload', true);
          xhr.setRequestHeader(
            'X-File-Metadata',
            JSON.stringify({
              id: fileId,
              filename: encodeURIComponent(file.name)
            })
          );
          xhr.send(fd);
        });
});

The third promise executes the following procedure:

  • Upload the file data encrypted
    • using XHR with Blob;
    • with X-File-Metadata consisting of
      • id: the file ID equivalent to iv (initial vector);
      • filename.

return value: File Parameters

xhr.onreadystatechange = () => {
            if (xhr.readyState === XMLHttpRequest.DONE) {
              if (xhr.status === 200) {
                const responseObj = JSON.parse(xhr.responseText);
                return resolve({
                  url: responseObj.url,
                  fileId: responseObj.id,
                  secretKey: keydata.k,
                  deleteToken: responseObj.delete
                });
              }
              reject(xhr.status);
            }
};

The final promise returns following parameters:

  • url: URL generated by server;
  • fileId: File ID generated by server (different than iv (initial vector) generated by client side);
  • secretKey: Secret key "K" exported by the second promise on client side;
  • deleteToken: Key token to delete the file generated by server.

Note: Download URL is `${url}#${secretKey}`. See frontend/src/upload.js for detail.

Server side: POST /upload/

This clause describes "POST /upload/" handler defined in server/server.js (to store).

app.post('/upload', (req, res, next) => {
  const newId = crypto.randomBytes(5).toString('hex');

  // ***** snip *****

  meta.delete = crypto.randomBytes(10).toString('hex');
  req.pipe(req.busboy);

  req.busboy.on('file', async (fieldname, file, filename) => {
    try {
      await storage.set(newId, file, filename, meta);

      const protocol = conf.env === 'production' ? 'https' : req.protocol;
      const url = `${protocol}://${req.get('host')}/download/${newId}/`;
      res.json({
        url,
        delete: meta.delete,
        id: newId
      });
    } catch (e) {
      if (e.message === 'limit') {
        return res.sendStatus(413);
      }
      res.sendStatus(500);
    }
  });

  // ***** snip *****
});

Server stores:

  • newId: File ID generated by server (different than iv (initial vector) generated by client side).
  • file: File data encrypted (by client side)
  • meta: X-File-Metadata consisting of
    • id: the file ID equivalent to iv (initial vector) generated by client side;
    • filename;
    • delete: Key token to delete the file generated by server.

Server returns a response:

  • with JSON response body consisting of
    • url: URL generated by server;
    • delete: Key token to delete the file generated by server.
    • id: File ID generated by server (different than iv (initial vector) generated by client side).

Client side: FileReceiver#download()

This clause describes FileReceiver#download() method defined in frontend/src/fileReceiver.js (to download with client side decryption).

first Promise : Import Key

window.crypto.subtle
      .importKey(
        'jwk',
        {
          kty: 'oct',
          k: location.hash.slice(1),
          alg: 'A128GCM',
          ext: true
        },
        {
          name: 'AES-GCM'
        },
        true,
        ['encrypt', 'decrypt']
)

The first promise is resolved when following procedure is completed:

  • Import Key:
    • using importKey() of Web Crypto API;
    • from the URL hash (#) parameter;
    • with a symmetric algorithm "AES-GCM" (AES in Galois/Counter Mode);
    • with 128-bit key length.

second Promise : Download

.then(key => {
        return new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();

          // ***** snip *****

          xhr.onload = function(event) {
            // ***** snip *****

            const blob = new Blob([this.response]);
            const type = xhr.getResponseHeader('Content-Type');
            const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
            const fileReader = new FileReader();
            fileReader.onload = function() {
              resolve([
                {
                  data: this.result,
                  filename: meta.filename,
                  type,
                  iv: meta.id
                },
                key
              ]);
            };

            fileReader.readAsArrayBuffer(blob);
          };

          xhr.open('get', '/assets' + location.pathname.slice(0, -1), true);
          xhr.responseType = 'blob';
          xhr.send();
        });
})

The second promise is resolved when following procedure is completed:

  • Download file:
    • using XHR with Blob;
    • returns parameter consisting of
      • data: File data encrypted
      • filename;
      • type: the Content-Type of the HTTP response header;
      • iv: the file ID equivalent to iv (initial vector) generated by sender client side.

third Promise : Decrypt

.then(([fdata, key]) => {
        this.emit('decrypting');
        return Promise.all([
          window.crypto.subtle
            .decrypt(
              {
                name: 'AES-GCM',
                iv: hexToArray(fdata.iv),
                tagLength: 128
              },
              key,
              fdata.data
            )
            .then(decrypted => {
              return Promise.resolve(decrypted);
            }),
          {
            name: decodeURIComponent(fdata.filename),
            type: fdata.type
          }
        ]);
});

The third promise is resolved when following procedure is completed:

  • Encrypt file data fdata.data
    • using encrypt() of Web Crypto API;
    • with a symmetric algorithm "AES-GCM" (AES in Galois/Counter Mode);
    • with following parameters:
      • iv: the file ID equivalent to iv (initial vector) generated by sender client side;
      • key: Secret key "K" import by the first promise on client side.

Conclusion

Parameter Analysis

As of v1.1.0,

  • File data is encrypted on the client side during uploading.
  • File data is decrypted on the client side during downloading.
  • File name is not encrypted and stored on the server.
  • Secret Key is generated by the client side and shared with the sender/receiver persons by hash (#) of the download link URL. It is never sent to the server.

References