Encrypt device name on account create, on first launch if needed
This commit is contained in:
parent
775e31c854
commit
47f834cf5c
10 changed files with 282 additions and 95 deletions
|
@ -681,6 +681,7 @@
|
|||
textsecure.storage.user.getDeviceId() != '1'
|
||||
) {
|
||||
window.getSyncRequest();
|
||||
window.getAccountManager().maybeUpdateDeviceName();
|
||||
}
|
||||
|
||||
const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-env browser */
|
||||
/* global dcodeIO */
|
||||
/* global dcodeIO, libsignal */
|
||||
|
||||
/* eslint-disable camelcase, no-bitwise */
|
||||
|
||||
|
@ -10,9 +10,11 @@ module.exports = {
|
|||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesCtr,
|
||||
decryptDeviceName,
|
||||
decryptSymmetric,
|
||||
deriveAccessKey,
|
||||
encryptAesCtr,
|
||||
encryptDeviceName,
|
||||
encryptSymmetric,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
getAccessKeyVerifier,
|
||||
|
@ -30,6 +32,55 @@ module.exports = {
|
|||
|
||||
// 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) {
|
||||
const iv = getZeroes(12);
|
||||
const plaintext = getZeroes(16);
|
||||
|
|
|
@ -325,6 +325,7 @@ function HTTPError(message, providedCode, response, stack) {
|
|||
|
||||
const URL_CALLS = {
|
||||
accounts: 'v1/accounts',
|
||||
updateDeviceName: 'v1/accounts/name',
|
||||
attachment: 'v1/attachments',
|
||||
deliveryCert: 'v1/certificate/delivery',
|
||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||
|
@ -386,6 +387,7 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
sendMessages,
|
||||
sendMessagesUnauth,
|
||||
setSignedPreKey,
|
||||
updateDeviceName,
|
||||
};
|
||||
|
||||
function _ajax(param) {
|
||||
|
@ -568,6 +570,16 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
return response;
|
||||
}
|
||||
|
||||
function updateDeviceName(deviceName) {
|
||||
return _ajax({
|
||||
call: 'updateDeviceName',
|
||||
httpType: 'PUT',
|
||||
jsonData: {
|
||||
deviceName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getDevices() {
|
||||
return _ajax({
|
||||
call: 'devices',
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
libsignal,
|
||||
WebSocketResource,
|
||||
btoa,
|
||||
Signal,
|
||||
getString,
|
||||
libphonenumber,
|
||||
Event,
|
||||
|
@ -45,6 +46,59 @@
|
|||
requestSMSVerification(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) {
|
||||
const registerKeys = this.server.registerKeys.bind(this.server);
|
||||
const createAccount = this.createAccount.bind(this);
|
||||
|
@ -335,7 +389,7 @@
|
|||
});
|
||||
});
|
||||
},
|
||||
createAccount(
|
||||
async createAccount(
|
||||
number,
|
||||
verificationCode,
|
||||
identityKeyPair,
|
||||
|
@ -353,110 +407,106 @@
|
|||
|
||||
const previousNumber = getNumber(textsecure.storage.get('number_id'));
|
||||
|
||||
return this.server
|
||||
.confirmCode(
|
||||
number,
|
||||
verificationCode,
|
||||
password,
|
||||
signalingKey,
|
||||
registrationId,
|
||||
deviceName,
|
||||
{ accessKey }
|
||||
)
|
||||
.then(response => {
|
||||
if (previousNumber && previousNumber !== number) {
|
||||
window.log.warn(
|
||||
'New number is different from old number; deleting all previous data'
|
||||
);
|
||||
const encryptedDeviceName = await this.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKeyPair
|
||||
);
|
||||
await this.deviceNameIsEncrypted();
|
||||
|
||||
return textsecure.storage.protocol.removeAllData().then(
|
||||
() => {
|
||||
window.log.info('Successfully deleted previous data');
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
window.log.error(
|
||||
'Something went wrong deleting data from previous number',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
const response = await this.server.confirmCode(
|
||||
number,
|
||||
verificationCode,
|
||||
password,
|
||||
signalingKey,
|
||||
registrationId,
|
||||
encryptedDeviceName,
|
||||
{ accessKey }
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
);
|
||||
}
|
||||
if (previousNumber && previousNumber !== number) {
|
||||
window.log.warn(
|
||||
'New number is different from old number; deleting all previous data'
|
||||
);
|
||||
|
||||
return response;
|
||||
})
|
||||
.then(async response => {
|
||||
await Promise.all([
|
||||
textsecure.storage.remove('identityKey'),
|
||||
textsecure.storage.remove('signaling_key'),
|
||||
textsecure.storage.remove('password'),
|
||||
textsecure.storage.remove('registrationId'),
|
||||
textsecure.storage.remove('number_id'),
|
||||
textsecure.storage.remove('device_name'),
|
||||
textsecure.storage.remove('regionCode'),
|
||||
textsecure.storage.remove('userAgent'),
|
||||
textsecure.storage.remove('profileKey'),
|
||||
textsecure.storage.remove('read-receipts-setting'),
|
||||
]);
|
||||
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
id: number,
|
||||
publicKey: identityKeyPair.pubKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
|
||||
await textsecure.storage.put('identityKey', identityKeyPair);
|
||||
await textsecure.storage.put('signaling_key', signalingKey);
|
||||
await textsecure.storage.put('password', password);
|
||||
await textsecure.storage.put('registrationId', registrationId);
|
||||
if (profileKey) {
|
||||
await textsecure.storage.put('profileKey', profileKey);
|
||||
}
|
||||
if (userAgent) {
|
||||
await textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
||||
await textsecure.storage.put(
|
||||
'read-receipt-setting',
|
||||
Boolean(readReceipts)
|
||||
try {
|
||||
await textsecure.storage.protocol.removeAllData();
|
||||
window.log.info('Successfully deleted previous data');
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Something went wrong deleting data from previous number',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await textsecure.storage.user.setNumberAndDeviceId(
|
||||
number,
|
||||
response.deviceId || 1,
|
||||
deviceName
|
||||
);
|
||||
await textsecure.storage.put(
|
||||
'regionCode',
|
||||
libphonenumber.util.getRegionCodeForNumber(number)
|
||||
);
|
||||
});
|
||||
await Promise.all([
|
||||
textsecure.storage.remove('identityKey'),
|
||||
textsecure.storage.remove('signaling_key'),
|
||||
textsecure.storage.remove('password'),
|
||||
textsecure.storage.remove('registrationId'),
|
||||
textsecure.storage.remove('number_id'),
|
||||
textsecure.storage.remove('device_name'),
|
||||
textsecure.storage.remove('regionCode'),
|
||||
textsecure.storage.remove('userAgent'),
|
||||
textsecure.storage.remove('profileKey'),
|
||||
textsecure.storage.remove('read-receipts-setting'),
|
||||
]);
|
||||
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
id: number,
|
||||
publicKey: identityKeyPair.pubKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
|
||||
await textsecure.storage.put('identityKey', identityKeyPair);
|
||||
await textsecure.storage.put('signaling_key', signalingKey);
|
||||
await textsecure.storage.put('password', password);
|
||||
await textsecure.storage.put('registrationId', registrationId);
|
||||
if (profileKey) {
|
||||
await textsecure.storage.put('profileKey', profileKey);
|
||||
}
|
||||
if (userAgent) {
|
||||
await textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
||||
await textsecure.storage.put(
|
||||
'read-receipt-setting',
|
||||
Boolean(readReceipts)
|
||||
);
|
||||
|
||||
await textsecure.storage.user.setNumberAndDeviceId(
|
||||
number,
|
||||
response.deviceId || 1,
|
||||
deviceName
|
||||
);
|
||||
await textsecure.storage.put(
|
||||
'regionCode',
|
||||
libphonenumber.util.getRegionCodeForNumber(number)
|
||||
);
|
||||
},
|
||||
clearSessionsAndPreKeys() {
|
||||
async clearSessionsAndPreKeys() {
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
window.log.info('clearing all sessions, prekeys, and signed prekeys');
|
||||
return Promise.all([
|
||||
await Promise.all([
|
||||
store.clearPreKeyStore(),
|
||||
store.clearSignedPreKeysStore(),
|
||||
store.clearSessionStore(),
|
||||
]);
|
||||
},
|
||||
// Takes the same object returned by generateKeys
|
||||
confirmKeys(keys) {
|
||||
async confirmKeys(keys) {
|
||||
const store = textsecure.storage.protocol;
|
||||
const key = keys.signedPreKey;
|
||||
const confirmed = true;
|
||||
|
||||
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) {
|
||||
const progressCallback =
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
loadProtoBufs('SubProtocol.proto');
|
||||
loadProtoBufs('DeviceMessages.proto');
|
||||
|
||||
// Just for encrypting device names
|
||||
loadProtoBufs('DeviceName.proto');
|
||||
|
||||
// Metadata-specific protos
|
||||
loadProtoBufs('UnidentifiedDelivery.proto');
|
||||
})();
|
||||
|
|
|
@ -31,5 +31,13 @@
|
|||
getDeviceName() {
|
||||
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', () => {
|
||||
let accountManager;
|
||||
|
||||
|
@ -10,9 +12,14 @@ describe('AccountManager', () => {
|
|||
let signedPreKeys;
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
|
||||
originalProtocolStorage = window.textsecure.storage.protocol;
|
||||
window.textsecure.storage.protocol = {
|
||||
getIdentityKeyPair() {
|
||||
return identityKey;
|
||||
},
|
||||
loadSignedPreKeys() {
|
||||
return Promise.resolve(signedPreKeys);
|
||||
},
|
||||
|
@ -22,6 +29,17 @@ describe('AccountManager', () => {
|
|||
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', () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
|
|
7
protos/DeviceName.proto
Normal file
7
protos/DeviceName.proto
Normal file
|
@ -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';
|
||||
|
||||
|
@ -109,4 +109,41 @@ describe('Crypto', () => {
|
|||
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(",
|
||||
"path": "js/background.js",
|
||||
"line": " wrap(",
|
||||
"lineNumber": 727,
|
||||
"lineNumber": 728,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-18T22:23:00.485Z"
|
||||
},
|
||||
|
@ -252,7 +252,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/background.js",
|
||||
"line": " await wrap(",
|
||||
"lineNumber": 1257,
|
||||
"lineNumber": 1258,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-26T22:43:23.229Z"
|
||||
},
|
||||
|
@ -319,7 +319,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
||||
"lineNumber": 271,
|
||||
"lineNumber": 322,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -327,7 +327,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();",
|
||||
"lineNumber": 274,
|
||||
"lineNumber": 325,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -335,7 +335,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();",
|
||||
"lineNumber": 278,
|
||||
"lineNumber": 329,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -343,7 +343,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();",
|
||||
"lineNumber": 282,
|
||||
"lineNumber": 333,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -351,7 +351,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');",
|
||||
"lineNumber": 285,
|
||||
"lineNumber": 336,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue