Merge pull request #193 from mozilla/frontend_tests

Frontend tests
This commit is contained in:
Abhinav Adduri 2017-07-17 16:08:51 -07:00 committed by GitHub
commit 4122f4dc3e
12 changed files with 1058 additions and 2512 deletions

View File

@ -1,2 +1,4 @@
public/bundle.js public/bundle.js
public/webcrypto-shim.js public/webcrypto-shim.js
test/frontend/bundle.js
firefox

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ public/bundle.js
public/version.json public/version.json
static/* static/*
!static/info.txt !static/info.txt
test/frontend/bundle.js

View File

@ -4,6 +4,13 @@ machine:
services: services:
- docker - docker
- redis - redis
environment:
PATH: "/home/ubuntu/send/firefox:$PATH"
dependencies:
pre:
- npm i -g get-firefox geckodriver
- get-firefox --platform linux --extract --target /home/ubuntu/send
deployment: deployment:
latest: latest:

View File

@ -58,61 +58,41 @@ class FileReceiver extends EventEmitter {
true, true,
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
) )
]) ]).then(([fdata, key]) => {
.then(([fdata, key]) => { this.emit('decrypting', true);
this.emit('decrypting', true); return Promise.all([
return Promise.all([ window.crypto.subtle.decrypt(
window.crypto.subtle {
.decrypt( name: 'AES-GCM',
{ iv: hexToArray(fdata.iv),
name: 'AES-GCM', additionalData: hexToArray(fdata.aad)
iv: hexToArray(fdata.iv), },
additionalData: hexToArray(fdata.aad), key,
tagLength: 128 fdata.data
}, ).then(decrypted => {
key, this.emit('decrypting', false);
fdata.data return Promise.resolve(decrypted)
) }),
.then(decrypted => { fdata.filename,
this.emit('decrypting', false); hexToArray(fdata.aad)
return new Promise((resolve, reject) => { ]);
resolve(decrypted); }).then(([decrypted, fname, proposedHash]) => {
}); this.emit('hashing', true);
}), return window.crypto.subtle.digest('SHA-256', decrypted).then(calculatedHash => {
new Promise((resolve, reject) => { this.emit('hashing', false);
resolve(fdata.filename); const integrity = new Uint8Array(calculatedHash).toString() === proposedHash.toString();
}), if (!integrity) {
new Promise((resolve, reject) => { this.emit('unsafe', true)
resolve(hexToArray(fdata.aad)); return Promise.reject();
}) } else {
]); this.emit('safe', true);
return Promise.all([
decrypted,
fname
]);
}
}) })
.then(([decrypted, fname, proposedHash]) => { })
this.emit('hashing', true);
return window.crypto.subtle
.digest('SHA-256', decrypted)
.then(calculatedHash => {
this.emit('hashing', false);
const integrity =
new Uint8Array(calculatedHash).toString() ===
proposedHash.toString();
if (!integrity) {
return new Promise((resolve, reject) => {
console.log('This file has been tampered with.');
reject();
});
}
return Promise.all([
new Promise((resolve, reject) => {
resolve(decrypted);
}),
new Promise((resolve, reject) => {
resolve(fname);
})
]);
});
});
} }
} }

View File

@ -7,7 +7,6 @@ function arrayToHex(iv) {
hexStr += iv[i].toString(16); hexStr += iv[i].toString(16);
} }
} }
window.hexStr = hexStr;
return hexStr; return hexStr;
} }

3037
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
"raven": "^2.1.0", "raven": "^2.1.0",
"raven-js": "^3.16.0", "raven-js": "^3.16.0",
"redis": "^2.7.1", "redis": "^2.7.1",
"selenium-webdriver": "^3.4.0",
"supertest": "^3.0.0", "supertest": "^3.0.0",
"uglify-es": "3.0.19" "uglify-es": "3.0.19"
}, },
@ -52,7 +53,8 @@
"lint:css": "stylelint 'public/*.css'", "lint:css": "stylelint 'public/*.css'",
"lint:js": "eslint .", "lint:js": "eslint .",
"start": "node server/server", "start": "node server/server",
"test": "mocha test/unit && mocha test/server", "test": "mocha test/unit && mocha test/server && npm run test-browser && node test/frontend/driver.js",
"test-browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d",
"version": "node scripts/version" "version": "node scripts/version"
} }
} }

View File

@ -0,0 +1,2 @@
env:
browser: true

22
test/frontend/driver.js Normal file
View File

@ -0,0 +1,22 @@
const webdriver = require('selenium-webdriver');
const path = require('path');
const until = webdriver.until;
const driver = new webdriver.Builder()
.forBrowser('firefox')
.build();
driver.get(path.join('file:///', __dirname, '/frontend.test.html'));
driver.wait(until.titleIs('Mocha Tests'), 1000);
driver.wait(until.titleMatches(/^[0-1]$/), 10000);
driver.getTitle().then(title => {
driver.quit().then(() => {
if (title === '0') {
console.log('Frontend tests have passed.');
} else {
throw new Error('Frontend tests are failing. ' +
'Please open the frontend.test.html file in a browser.');
}
})
})

View File

@ -0,0 +1,22 @@
class FakeFile extends Blob {
constructor(name, data, opt) {
super(data, opt);
this.name = name;
}
}
window.Raven = {
captureException: function(err) {
console.error(err, err.stack);
}
}
window.FakeFile = FakeFile;
window.FileSender = require('../../frontend/src/fileSender');
window.FileReceiver = require('../../frontend/src/fileReceiver');
window.sinon = require('sinon');
window.server = window.sinon.fakeServer.create();
window.assert = require('assert');
const utils = require('../../frontend/src/utils');
window.hexToArray = utils.hexToArray;
window.arrayToHex = utils.arrayToHex;

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Mocha Tests</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css">
<script src="bundle.js"></script>
<meta charset="utf-8"/>
</head>
<body>
<div id="mocha"></div>
<script src="../../node_modules/mocha/mocha.js"></script>
<script>mocha.setup('bdd')</script>
<script src="frontend.test.js"></script>
<script>
mocha.checkLeaks();
mocha.globals(['jQuery']);
mocha.run(function(err) {
document.title = err;
});
</script>
</body>
</html>

View File

@ -0,0 +1,360 @@
const FileSender = window.FileSender;
const FileReceiver = window.FileReceiver;
const FakeFile = window.FakeFile;
const assert = window.assert;
const server = window.server;
const hexToArray = window.hexToArray;
const arrayToHex = window.arrayToHex;
const sinon = window.sinon;
let file;
let encryptedIV;
let fileHash;
let secretKey;
let originalBlob;
describe('File Sender', function() {
before(function() {
server.respondImmediately = true;
server.respondWith(
'POST',
'/upload',
function(request) {
const reader = new FileReader();
reader.readAsArrayBuffer(request.requestBody.get('data'));
reader.onload = function(event) {
file = this.result;
}
const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']);
request.respond(
200,
{'Content-Type': 'application/json'},
JSON.stringify({url: 'some url',
id: responseObj.id,
delete: responseObj.delete})
)
}
)
})
it('Should get a loading event emission', function() {
const file = new FakeFile('hello_world.txt', ['This is some data.'])
const fs = new FileSender(file);
let testLoading = true;
fs.on('loading', isStillLoading => {
assert(!(!testLoading && isStillLoading));
testLoading = isStillLoading;
})
return fs.upload()
.then(info => {
assert(info);
assert(!testLoading);
})
.catch(err => {
console.log(err, err.stack);
assert.fail();
});
})
it('Should get a hashing event emission', function() {
const file = new FakeFile('hello_world.txt', ['This is some data.'])
const fs = new FileSender(file);
let testHashing = true;
fs.on('hashing', isStillHashing => {
assert(!(!testHashing && isStillHashing));
testHashing = isStillHashing;
})
return fs.upload()
.then(info => {
assert(info);
assert(!testHashing);
})
.catch(err => {
console.log(err, err.stack);
assert.fail();
});
})
it('Should get a encrypting event emission', function() {
const file = new FakeFile('hello_world.txt', ['This is some data.'])
const fs = new FileSender(file);
let testEncrypting = true;
fs.on('encrypting', isStillEncrypting => {
assert(!(!testEncrypting && isStillEncrypting));
testEncrypting = isStillEncrypting;
})
return fs.upload()
.then(info => {
assert(info);
assert(!testEncrypting);
})
.catch(err => {
console.log(err, err.stack);
assert.fail();
});
})
it('Should encrypt a file properly', function(done) {
const newFile = new FakeFile('hello_world.txt', ['This is some data.'])
const fs = new FileSender(newFile);
fs.upload().then(info => {
const key = info.secretKey;
secretKey = info.secretKey;
const IV = info.fileId;
encryptedIV = info.fileId;
const readRaw = new FileReader;
readRaw.onload = function(event) {
const rawArray = new Uint8Array(this.result);
originalBlob = rawArray;
window.crypto.subtle.digest('SHA-256', rawArray).then(hash => {
fileHash = hash;
window.crypto.subtle.importKey(
'jwk',
{
kty: 'oct',
k: key,
alg: 'A128GCM',
ext: true,
},
{
name: 'AES-GCM'
},
true,
['encrypt', 'decrypt']
)
.then(cryptoKey => {
window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: hexToArray(IV),
additionalData: hash,
tagLength: 128
},
cryptoKey,
rawArray
)
.then(encrypted => {
assert(new Uint8Array(encrypted).toString() ===
new Uint8Array(file).toString());
done();
})
})
})
}
readRaw.readAsArrayBuffer(newFile);
})
})
});
describe('File Receiver', function() {
class FakeXHR {
constructor() {
this.response = file;
this.status = 200;
}
static setup() {
FakeXHR.prototype.open = sinon.spy();
FakeXHR.prototype.send = function () {
this.onload();
}
FakeXHR.prototype.originalXHR = window.XMLHttpRequest;
FakeXHR.prototype.getResponseHeader = function () {
return JSON.stringify({
aad: arrayToHex(new Uint8Array(fileHash)),
filename: 'hello_world.txt',
id: encryptedIV
})
}
window.XMLHttpRequest = FakeXHR;
}
static restore() {
// originalXHR is a sinon FakeXMLHttpRequest, since
// fakeServer.create() is called in frontend.bundle.js
window.XMLHttpRequest.prototype.originalXHR.restore();
}
}
const cb = function(done) {
if (file === undefined ||
encryptedIV === undefined ||
fileHash === undefined ||
secretKey === undefined) {
assert.fail('Please run file sending tests before trying to receive the files.');
done();
}
FakeXHR.setup();
done();
}
before(cb)
after(function() {
FakeXHR.restore();
})
it('Should decrypt properly', function() {
const fr = new FileReceiver();
location.hash = secretKey;
return fr.download().then(([decrypted, name]) => {
assert(name);
assert(new Uint8Array(decrypted).toString() ===
new Uint8Array(originalBlob).toString())
}).catch(err => {
console.log(err, err.stack);
assert.fail();
})
})
it('Should emit decrypting events', function() {
const fr = new FileReceiver();
location.hash = secretKey;
let testDecrypting = true;
fr.on('decrypting', isStillDecrypting => {
assert(!(!testDecrypting && isStillDecrypting));
testDecrypting = isStillDecrypting;
});
fr.on('safe', isSafe => {
assert(isSafe);
})
return fr.download().then(([decrypted, name]) => {
assert(decrypted);
assert(name);
assert(!testDecrypting);
}).catch(err => {
console.log(err, err.stack);
assert.fail();
})
})
it('Should emit hashing events', function() {
const fr = new FileReceiver();
location.hash = secretKey;
let testHashing = true;
fr.on('hashing', isStillHashing => {
assert(!(!testHashing && isStillHashing));
testHashing = isStillHashing;
});
fr.on('safe', isSafe => {
assert(isSafe);
})
return fr.download().then(([decrypted, name]) => {
assert(decrypted);
assert(name);
assert(!testHashing);
}).catch(err => {
assert.fail();
})
})
it('Should catch fraudulent checksums', function(done) {
// Use the secret key and file hash of the previous file to encrypt,
// which has a different hash than this one (different strings).
const newFile = new FakeFile('hello_world.txt',
['This is some data, with a changed hash.'])
const readRaw = new FileReader();
readRaw.onload = function(event) {
const plaintext = new Uint8Array(this.result);
window.crypto.subtle.importKey(
'jwk',
{
kty: 'oct',
k: secretKey,
alg: 'A128GCM',
ext: true
},
{
name: 'AES-GCM'
},
true,
['encrypt', 'decrypt']
)
.then(key => {
// The file hash used here is the hash of the fake
// file from the previous test; it's a phony checksum.
return window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: hexToArray(encryptedIV),
additionalData: fileHash,
tagLength: 128
},
key,
plaintext
)
})
.then(encrypted => {
file = encrypted;
const fr = new FileReceiver();
location.hash = secretKey;
fr.on('unsafe', isUnsafe => {
assert(isUnsafe)
})
fr.on('safe', () => {
// This event should not be emitted.
assert.fail();
})
fr.download().then(() => {
assert.fail();
done();
}).catch(err => {
assert(1);
done();
})
})
}
readRaw.readAsArrayBuffer(newFile);
})
it('Should not decrypt with an incorrect checksum', function() {
FakeXHR.prototype.getResponseHeader = function () {
return JSON.stringify({
aad: 'some_bad_hashz',
filename: 'hello_world.txt',
id: encryptedIV
})
}
const fr = new FileReceiver();
location.hash = secretKey;
return fr.download().then(([decrypted, name]) => {
assert(decrypted);
assert(name);
assert.fail();
}).catch(err => {
assert(1);
})
})
})