diff --git a/Gruntfile.js b/Gruntfile.js index 3233983..e328d94 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -75,7 +75,8 @@ module.exports = function(grunt) { 'src/SignalProtocolAddress.js', 'src/SessionBuilder.js', 'src/SessionCipher.js', - 'src/SessionLock.js' + 'src/SessionLock.js', + 'src/NumericFingerprint.js' ], dest: 'dist/libsignal-protocol.js', options: { diff --git a/dist/libsignal-protocol.js b/dist/libsignal-protocol.js index 4671f7c..2541f17 100644 --- a/dist/libsignal-protocol.js +++ b/dist/libsignal-protocol.js @@ -35245,6 +35245,10 @@ var Internal = Internal || {}; }); }, + hash: function(data) { + return crypto.subtle.digest({name: 'SHA-512'}, data); + }, + HKDF: function(input, salt, info) { // Specific implementation of RFC 5869 that only returns the first 3 32-byte chunks // TODO: We dont always need the third chunk, we might skip it @@ -36431,4 +36435,76 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ })(); +(function() { + var VERSION = 0; + + function iterateHash(data, key, count) { + data = dcodeIO.ByteBuffer.concat([data, key]).toArrayBuffer(); + return Internal.crypto.hash(data).then(function(result) { + if (--count === 0) { + return result; + } else { + return iterateHash(result, key, count); + } + }); + } + + function shortToArrayBuffer(number) { + return new Uint16Array([number]).buffer; + } + + function getEncodedChunk(hash, offset) { + var chunk = ( hash[offset] * Math.pow(2,32) + + hash[offset+1] * Math.pow(2,24) + + hash[offset+2] * Math.pow(2,16) + + hash[offset+3] * Math.pow(2,8) + + hash[offset+4] ) % 100000; + var s = chunk.toString(); + while (s.length < 5) { + s = '0' + s; + } + return s; + } + + function getDisplayStringFor(identifier, key, iterations) { + var bytes = dcodeIO.ByteBuffer.concat([ + shortToArrayBuffer(VERSION), key, identifier + ]).toArrayBuffer(); + return iterateHash(bytes, key, iterations).then(function(output) { + output = new Uint8Array(output); + return getEncodedChunk(output, 0) + + getEncodedChunk(output, 5) + + getEncodedChunk(output, 10) + + getEncodedChunk(output, 15) + + getEncodedChunk(output, 20) + + getEncodedChunk(output, 25); + }); + } + + libsignal.FingerprintGenerator = function(iterations) { + this.iterations = iterations; + }; + libsignal.FingerprintGenerator.prototype = { + createFor: function(localIdentifier, localIdentityKey, + remoteIdentifier, remoteIdentityKey) { + if (typeof localIdentifier !== 'string' || + typeof remoteIdentifier !== 'string' || + !(localIdentityKey instanceof ArrayBuffer) || + !(remoteIdentityKey instanceof ArrayBuffer)) { + + throw new Error('Invalid arguments'); + } + + return Promise.all([ + getDisplayStringFor(localIdentifier, localIdentityKey, this.iterations), + getDisplayStringFor(remoteIdentifier, remoteIdentityKey, this.iterations) + ]).then(function(fingerprints) { + return fingerprints.sort().join(''); + }); + } + }; + +})(); + + })(); \ No newline at end of file diff --git a/src/NumericFingerprint.js b/src/NumericFingerprint.js new file mode 100644 index 0000000..b4a3fd9 --- /dev/null +++ b/src/NumericFingerprint.js @@ -0,0 +1,71 @@ +(function() { + var VERSION = 0; + + function iterateHash(data, key, count) { + data = dcodeIO.ByteBuffer.concat([data, key]).toArrayBuffer(); + return Internal.crypto.hash(data).then(function(result) { + if (--count === 0) { + return result; + } else { + return iterateHash(result, key, count); + } + }); + } + + function shortToArrayBuffer(number) { + return new Uint16Array([number]).buffer; + } + + function getEncodedChunk(hash, offset) { + var chunk = ( hash[offset] * Math.pow(2,32) + + hash[offset+1] * Math.pow(2,24) + + hash[offset+2] * Math.pow(2,16) + + hash[offset+3] * Math.pow(2,8) + + hash[offset+4] ) % 100000; + var s = chunk.toString(); + while (s.length < 5) { + s = '0' + s; + } + return s; + } + + function getDisplayStringFor(identifier, key, iterations) { + var bytes = dcodeIO.ByteBuffer.concat([ + shortToArrayBuffer(VERSION), key, identifier + ]).toArrayBuffer(); + return iterateHash(bytes, key, iterations).then(function(output) { + output = new Uint8Array(output); + return getEncodedChunk(output, 0) + + getEncodedChunk(output, 5) + + getEncodedChunk(output, 10) + + getEncodedChunk(output, 15) + + getEncodedChunk(output, 20) + + getEncodedChunk(output, 25); + }); + } + + libsignal.FingerprintGenerator = function(iterations) { + this.iterations = iterations; + }; + libsignal.FingerprintGenerator.prototype = { + createFor: function(localIdentifier, localIdentityKey, + remoteIdentifier, remoteIdentityKey) { + if (typeof localIdentifier !== 'string' || + typeof remoteIdentifier !== 'string' || + !(localIdentityKey instanceof ArrayBuffer) || + !(remoteIdentityKey instanceof ArrayBuffer)) { + + throw new Error('Invalid arguments'); + } + + return Promise.all([ + getDisplayStringFor(localIdentifier, localIdentityKey, this.iterations), + getDisplayStringFor(remoteIdentifier, remoteIdentityKey, this.iterations) + ]).then(function(fingerprints) { + return fingerprints.sort().join(''); + }); + } + }; + +})(); + diff --git a/src/crypto.js b/src/crypto.js index 02dbfa5..26df2f8 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -35,6 +35,10 @@ var Internal = Internal || {}; }); }, + hash: function(data) { + return crypto.subtle.digest({name: 'SHA-512'}, data); + }, + HKDF: function(input, salt, info) { // Specific implementation of RFC 5869 that only returns the first 3 32-byte chunks // TODO: We dont always need the third chunk, we might skip it diff --git a/test/NumericFingerprintTest.js b/test/NumericFingerprintTest.js new file mode 100644 index 0000000..b5b5a66 --- /dev/null +++ b/test/NumericFingerprintTest.js @@ -0,0 +1,80 @@ +/* + * vim: ts=4:sw=4 + */ + +'use strict'; +describe('NumericFingerprint', function() { + this.timeout(5000); + var ALICE_IDENTITY = [ + 0x05, 0x06, 0x86, 0x3b, 0xc6, 0x6d, 0x02, 0xb4, 0x0d, 0x27, 0xb8, 0xd4, + 0x9c, 0xa7, 0xc0, 0x9e, 0x92, 0x39, 0x23, 0x6f, 0x9d, 0x7d, 0x25, 0xd6, + 0xfc, 0xca, 0x5c, 0xe1, 0x3c, 0x70, 0x64, 0xd8, 0x68 + ]; + var BOB_IDENTITY = [ + 0x05, 0xf7, 0x81, 0xb6, 0xfb, 0x32, 0xfe, 0xd9, 0xba, 0x1c, 0xf2, 0xde, + 0x97, 0x8d, 0x4d, 0x5d, 0xa2, 0x8d, 0xc3, 0x40, 0x46, 0xae, 0x81, 0x44, + 0x02, 0xb5, 0xc0, 0xdb, 0xd9, 0x6f, 0xda, 0x90, 0x7b + ]; + var FINGERPRINT = "300354477692869396892869876765458257569162576843440918079131"; + + var alice = { + identifier: '+14152222222', + key: new Uint8Array(ALICE_IDENTITY).buffer + }; + var bob = { + identifier: '+14153333333', + key: new Uint8Array(BOB_IDENTITY).buffer + }; + + it('returns the correct fingerprint', function(done) { + var generator = new libsignal.FingerprintGenerator(5200); + generator.createFor( + alice.identifier, alice.key, bob.identifier, bob.key + ).then(function(fingerprint) { + assert.strictEqual(fingerprint, FINGERPRINT); + }).then(done,done); + }); + + it ('alice and bob results match', function(done) { + var generator = new libsignal.FingerprintGenerator(1024); + Promise.all([ + generator.createFor( + alice.identifier, alice.key, bob.identifier, bob.key + ), + generator.createFor( + bob.identifier, bob.key, alice.identifier, alice.key + ) + ]).then(function(fingerprints) { + assert.strictEqual(fingerprints[0], fingerprints[1]); + }).then(done,done); + }); + + it ('alice and !bob results mismatch', function(done) { + var generator = new libsignal.FingerprintGenerator(1024); + Promise.all([ + generator.createFor( + alice.identifier, alice.key, '+15558675309', bob.key + ), + generator.createFor( + bob.identifier, bob.key, alice.identifier, alice.key + ) + ]).then(function(fingerprints) { + assert.notStrictEqual(fingerprints[0], fingerprints[1]); + }).then(done,done); + }); + + it ('alice and mitm results mismatch', function(done) { + var mitm = libsignal.crypto.getRandomBytes(33); + var generator = new libsignal.FingerprintGenerator(1024); + Promise.all([ + generator.createFor( + alice.identifier, alice.key, bob.identifier, mitm + ), + generator.createFor( + bob.identifier, bob.key, alice.identifier, alice.key + ) + ]).then(function(fingerprints) { + assert.notStrictEqual(fingerprints[0], fingerprints[1]); + }).then(done,done); + }); +}); diff --git a/test/index.html b/test/index.html index c0f66e6..71a43d5 100644 --- a/test/index.html +++ b/test/index.html @@ -28,12 +28,14 @@ + +