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 @@
+
+