Encrypt device name on account create, on first launch if needed

pull/272/head
Scott Nonnenberg 6 years ago
parent 775e31c854
commit 47f834cf5c

@ -681,6 +681,7 @@
textsecure.storage.user.getDeviceId() != '1' textsecure.storage.user.getDeviceId() != '1'
) { ) {
window.getSyncRequest(); window.getSyncRequest();
window.getAccountManager().maybeUpdateDeviceName();
} }
const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery'; const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';

@ -1,5 +1,5 @@
/* eslint-env browser */ /* eslint-env browser */
/* global dcodeIO */ /* global dcodeIO, libsignal */
/* eslint-disable camelcase, no-bitwise */ /* eslint-disable camelcase, no-bitwise */
@ -10,9 +10,11 @@ module.exports = {
concatenateBytes, concatenateBytes,
constantTimeEqual, constantTimeEqual,
decryptAesCtr, decryptAesCtr,
decryptDeviceName,
decryptSymmetric, decryptSymmetric,
deriveAccessKey, deriveAccessKey,
encryptAesCtr, encryptAesCtr,
encryptDeviceName,
encryptSymmetric, encryptSymmetric,
fromEncodedBinaryToArrayBuffer, fromEncodedBinaryToArrayBuffer,
getAccessKeyVerifier, getAccessKeyVerifier,
@ -30,6 +32,55 @@ module.exports = {
// High-level Operations // High-level Operations
async function encryptDeviceName(deviceName, identityPublic) {
const plaintext = bytesFromString(deviceName);
const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
const masterSecret = await libsignal.Curve.async.calculateAgreement(
identityPublic,
ephemeralKeyPair.privKey
);
const key1 = await hmacSha256(masterSecret, bytesFromString('auth'));
const syntheticIv = _getFirstBytes(await hmacSha256(key1, plaintext), 16);
const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
const cipherKey = await hmacSha256(key2, syntheticIv);
const counter = getZeroes(16);
const ciphertext = await encryptAesCtr(cipherKey, plaintext, counter);
return {
ephemeralPublic: ephemeralKeyPair.pubKey,
syntheticIv,
ciphertext,
};
}
async function decryptDeviceName(
{ ephemeralPublic, syntheticIv, ciphertext } = {},
identityPrivate
) {
const masterSecret = await libsignal.Curve.async.calculateAgreement(
ephemeralPublic,
identityPrivate
);
const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
const cipherKey = await hmacSha256(key2, syntheticIv);
const counter = getZeroes(16);
const plaintext = await decryptAesCtr(cipherKey, ciphertext, counter);
const key1 = await hmacSha256(masterSecret, bytesFromString('auth'));
const ourSyntheticIv = _getFirstBytes(await hmacSha256(key1, plaintext), 16);
if (!constantTimeEqual(ourSyntheticIv, syntheticIv)) {
throw new Error('decryptDeviceName: synthetic IV did not match');
}
return stringFromBytes(plaintext);
}
async function deriveAccessKey(profileKey) { async function deriveAccessKey(profileKey) {
const iv = getZeroes(12); const iv = getZeroes(12);
const plaintext = getZeroes(16); const plaintext = getZeroes(16);

@ -325,6 +325,7 @@ function HTTPError(message, providedCode, response, stack) {
const URL_CALLS = { const URL_CALLS = {
accounts: 'v1/accounts', accounts: 'v1/accounts',
updateDeviceName: 'v1/accounts/name',
attachment: 'v1/attachments', attachment: 'v1/attachments',
deliveryCert: 'v1/certificate/delivery', deliveryCert: 'v1/certificate/delivery',
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery', supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
@ -386,6 +387,7 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
sendMessages, sendMessages,
sendMessagesUnauth, sendMessagesUnauth,
setSignedPreKey, setSignedPreKey,
updateDeviceName,
}; };
function _ajax(param) { function _ajax(param) {
@ -568,6 +570,16 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
return response; return response;
} }
function updateDeviceName(deviceName) {
return _ajax({
call: 'updateDeviceName',
httpType: 'PUT',
jsonData: {
deviceName,
},
});
}
function getDevices() { function getDevices() {
return _ajax({ return _ajax({
call: 'devices', call: 'devices',

@ -4,6 +4,7 @@
libsignal, libsignal,
WebSocketResource, WebSocketResource,
btoa, btoa,
Signal,
getString, getString,
libphonenumber, libphonenumber,
Event, Event,
@ -45,6 +46,59 @@
requestSMSVerification(number) { requestSMSVerification(number) {
return this.server.requestVerificationSMS(number); return this.server.requestVerificationSMS(number);
}, },
async encryptDeviceName(name, providedIdentityKey) {
const identityKey =
providedIdentityKey ||
(await textsecure.storage.protocol.getIdentityKeyPair());
if (!identityKey) {
throw new Error(
'Identity key was not provided and is not in database!'
);
}
const encrypted = await Signal.Crypto.encryptDeviceName(
name,
identityKey.pubKey
);
const proto = new textsecure.protobuf.DeviceName();
proto.ephemeralPublic = encrypted.ephemeralPublic;
proto.syntheticIv = encrypted.syntheticIv;
proto.ciphertext = encrypted.ciphertext;
const arrayBuffer = proto.encode().toArrayBuffer();
return Signal.Crypto.arrayBufferToBase64(arrayBuffer);
},
async decryptDeviceName(base64) {
const identityKey = await textsecure.storage.protocol.getIdentityKeyPair();
const arrayBuffer = Signal.Crypto.base64ToArrayBuffer(base64);
const proto = textsecure.protobuf.DeviceName.decode(arrayBuffer);
const encrypted = {
ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(),
syntheticIv: proto.syntheticIv.toArrayBuffer(),
ciphertext: proto.ciphertext.toArrayBuffer(),
};
const name = await Signal.Crypto.decryptDeviceName(
encrypted,
identityKey.privKey
);
return name;
},
async maybeUpdateDeviceName() {
const isNameEncrypted = textsecure.storage.user.getDeviceNameEncrypted();
if (isNameEncrypted) {
return;
}
const deviceName = await textsecure.storage.user.getDeviceName();
const base64 = await this.encryptDeviceName(deviceName);
await this.server.updateDeviceName(base64);
},
async deviceNameIsEncrypted() {
await textsecure.storage.user.setDeviceNameEncrypted();
},
registerSingleDevice(number, verificationCode) { registerSingleDevice(number, verificationCode) {
const registerKeys = this.server.registerKeys.bind(this.server); const registerKeys = this.server.registerKeys.bind(this.server);
const createAccount = this.createAccount.bind(this); const createAccount = this.createAccount.bind(this);
@ -335,7 +389,7 @@
}); });
}); });
}, },
createAccount( async createAccount(
number, number,
verificationCode, verificationCode,
identityKeyPair, identityKeyPair,
@ -353,41 +407,38 @@
const previousNumber = getNumber(textsecure.storage.get('number_id')); const previousNumber = getNumber(textsecure.storage.get('number_id'));
return this.server const encryptedDeviceName = await this.encryptDeviceName(
.confirmCode( deviceName,
identityKeyPair
);
await this.deviceNameIsEncrypted();
const response = await this.server.confirmCode(
number, number,
verificationCode, verificationCode,
password, password,
signalingKey, signalingKey,
registrationId, registrationId,
deviceName, encryptedDeviceName,
{ accessKey } { accessKey }
) );
.then(response => {
if (previousNumber && previousNumber !== number) { if (previousNumber && previousNumber !== number) {
window.log.warn( window.log.warn(
'New number is different from old number; deleting all previous data' 'New number is different from old number; deleting all previous data'
); );
return textsecure.storage.protocol.removeAllData().then( try {
() => { await textsecure.storage.protocol.removeAllData();
window.log.info('Successfully deleted previous data'); window.log.info('Successfully deleted previous data');
return response; } catch (error) {
},
error => {
window.log.error( window.log.error(
'Something went wrong deleting data from previous number', 'Something went wrong deleting data from previous number',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
return response;
} }
);
} }
return response;
})
.then(async response => {
await Promise.all([ await Promise.all([
textsecure.storage.remove('identityKey'), textsecure.storage.remove('identityKey'),
textsecure.storage.remove('signaling_key'), textsecure.storage.remove('signaling_key'),
@ -437,26 +488,25 @@
'regionCode', 'regionCode',
libphonenumber.util.getRegionCodeForNumber(number) libphonenumber.util.getRegionCodeForNumber(number)
); );
});
}, },
clearSessionsAndPreKeys() { async clearSessionsAndPreKeys() {
const store = textsecure.storage.protocol; const store = textsecure.storage.protocol;
window.log.info('clearing all sessions, prekeys, and signed prekeys'); window.log.info('clearing all sessions, prekeys, and signed prekeys');
return Promise.all([ await Promise.all([
store.clearPreKeyStore(), store.clearPreKeyStore(),
store.clearSignedPreKeysStore(), store.clearSignedPreKeysStore(),
store.clearSessionStore(), store.clearSessionStore(),
]); ]);
}, },
// Takes the same object returned by generateKeys // Takes the same object returned by generateKeys
confirmKeys(keys) { async confirmKeys(keys) {
const store = textsecure.storage.protocol; const store = textsecure.storage.protocol;
const key = keys.signedPreKey; const key = keys.signedPreKey;
const confirmed = true; const confirmed = true;
window.log.info('confirmKeys: confirming key', key.keyId); window.log.info('confirmKeys: confirming key', key.keyId);
return store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed);
}, },
generateKeys(count, providedProgressCallback) { generateKeys(count, providedProgressCallback) {
const progressCallback = const progressCallback =

@ -36,6 +36,9 @@
loadProtoBufs('SubProtocol.proto'); loadProtoBufs('SubProtocol.proto');
loadProtoBufs('DeviceMessages.proto'); loadProtoBufs('DeviceMessages.proto');
// Just for encrypting device names
loadProtoBufs('DeviceName.proto');
// Metadata-specific protos // Metadata-specific protos
loadProtoBufs('UnidentifiedDelivery.proto'); loadProtoBufs('UnidentifiedDelivery.proto');
})(); })();

@ -31,5 +31,13 @@
getDeviceName() { getDeviceName() {
return textsecure.storage.get('device_name'); return textsecure.storage.get('device_name');
}, },
setDeviceNameEncrypted() {
return textsecure.storage.put('deviceNameEncrypted', true);
},
getDeviceNameEncrypted() {
return textsecure.storage.get('deviceNameEncrypted');
},
}; };
})(); })();

@ -1,3 +1,5 @@
/* global libsignal */
describe('AccountManager', () => { describe('AccountManager', () => {
let accountManager; let accountManager;
@ -10,9 +12,14 @@ describe('AccountManager', () => {
let signedPreKeys; let signedPreKeys;
const DAY = 1000 * 60 * 60 * 24; const DAY = 1000 * 60 * 60 * 24;
beforeEach(() => { beforeEach(async () => {
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
originalProtocolStorage = window.textsecure.storage.protocol; originalProtocolStorage = window.textsecure.storage.protocol;
window.textsecure.storage.protocol = { window.textsecure.storage.protocol = {
getIdentityKeyPair() {
return identityKey;
},
loadSignedPreKeys() { loadSignedPreKeys() {
return Promise.resolve(signedPreKeys); return Promise.resolve(signedPreKeys);
}, },
@ -22,6 +29,17 @@ describe('AccountManager', () => {
window.textsecure.storage.protocol = originalProtocolStorage; window.textsecure.storage.protocol = originalProtocolStorage;
}); });
describe('encrypted device name', () => {
it('roundtrips', async () => {
const deviceName = 'v2.5.0 on Ubunto 20.04';
const encrypted = await accountManager.encryptDeviceName(deviceName);
assert.strictEqual(typeof encrypted, 'string');
const decrypted = await accountManager.decryptDeviceName(encrypted);
assert.strictEqual(decrypted, deviceName);
});
});
it('keeps three confirmed keys even if over a week old', () => { it('keeps three confirmed keys even if over a week old', () => {
const now = Date.now(); const now = Date.now();
signedPreKeys = [ signedPreKeys = [

@ -0,0 +1,7 @@
package signalservice;
message DeviceName {
optional bytes ephemeralPublic = 1;
optional bytes syntheticIv = 2;
optional bytes ciphertext = 3;
}

@ -1,4 +1,4 @@
/* global Signal, textsecure */ /* global Signal, textsecure, libsignal */
'use strict'; 'use strict';
@ -109,4 +109,41 @@ describe('Crypto', () => {
throw new Error('Expected error to be thrown'); throw new Error('Expected error to be thrown');
}); });
}); });
describe('encrypted device name', () => {
it('roundtrips', async () => {
const deviceName = 'v1.19.0 on Windows 10';
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
const encrypted = await Signal.Crypto.encryptDeviceName(
deviceName,
identityKey.pubKey
);
const decrypted = await Signal.Crypto.decryptDeviceName(
encrypted,
identityKey.privKey
);
assert.strictEqual(decrypted, deviceName);
});
it('fails if iv is changed', async () => {
const deviceName = 'v1.19.0 on Windows 10';
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
const encrypted = await Signal.Crypto.encryptDeviceName(
deviceName,
identityKey.pubKey
);
encrypted.syntheticIv = Signal.Crypto.getRandomBytes(16);
try {
await Signal.Crypto.decryptDeviceName(encrypted, identityKey.privKey);
} catch (error) {
assert.strictEqual(
error.message,
'decryptDeviceName: synthetic IV did not match'
);
}
});
});
}); });

@ -244,7 +244,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/background.js", "path": "js/background.js",
"line": " wrap(", "line": " wrap(",
"lineNumber": 727, "lineNumber": 728,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-10-18T22:23:00.485Z" "updated": "2018-10-18T22:23:00.485Z"
}, },
@ -252,7 +252,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/background.js", "path": "js/background.js",
"line": " await wrap(", "line": " await wrap(",
"lineNumber": 1257, "lineNumber": 1258,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-10-26T22:43:23.229Z" "updated": "2018-10-26T22:43:23.229Z"
}, },
@ -319,7 +319,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/modules/crypto.js", "path": "js/modules/crypto.js",
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');", "line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
"lineNumber": 271, "lineNumber": 322,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z" "updated": "2018-10-05T23:12:28.961Z"
}, },
@ -327,7 +327,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/modules/crypto.js", "path": "js/modules/crypto.js",
"line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();", "line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();",
"lineNumber": 274, "lineNumber": 325,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z" "updated": "2018-10-05T23:12:28.961Z"
}, },
@ -335,7 +335,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/modules/crypto.js", "path": "js/modules/crypto.js",
"line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();", "line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();",
"lineNumber": 278, "lineNumber": 329,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z" "updated": "2018-10-05T23:12:28.961Z"
}, },
@ -343,7 +343,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/modules/crypto.js", "path": "js/modules/crypto.js",
"line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();", "line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();",
"lineNumber": 282, "lineNumber": 333,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z" "updated": "2018-10-05T23:12:28.961Z"
}, },
@ -351,7 +351,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/modules/crypto.js", "path": "js/modules/crypto.js",
"line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');", "line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');",
"lineNumber": 285, "lineNumber": 336,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z" "updated": "2018-10-05T23:12:28.961Z"
}, },

Loading…
Cancel
Save