Multi-device part 1: make primary account generate and send authorisation to secondary

This commit is contained in:
sachaaaaa 2019-08-06 14:49:01 +10:00
parent a7f999aeb1
commit d208614909
9 changed files with 288 additions and 2 deletions

View file

@ -72,6 +72,8 @@ module.exports = {
removeContactSignedPreKeyByIdentityKey,
removeAllContactSignedPreKeys,
createOrUpdatePairingAuthorisation,
createOrUpdateItem,
getItemById,
getAllItems,
@ -737,6 +739,28 @@ async function updateToSchemaVersion11(currentVersion, instance) {
console.log('updateToSchemaVersion11: success!');
}
async function updateToSchemaVersion12(currentVersion, instance) {
if (currentVersion >= 12) {
return;
}
console.log('updateToSchemaVersion12: starting...');
await instance.run('BEGIN TRANSACTION;');
await instance.run(
`CREATE TABLE pairingAuthorisations(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
issuerPubKey VARCHAR(255),
secondaryDevicePubKey VARCHAR(255),
signature VARCHAR(255),
json TEXT
);`
);
await instance.run('PRAGMA schema_version = 12;');
await instance.run('COMMIT TRANSACTION;');
console.log('updateToSchemaVersion12: success!');
}
const SCHEMA_VERSIONS = [
updateToSchemaVersion1,
updateToSchemaVersion2,
@ -749,6 +773,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion9,
updateToSchemaVersion10,
updateToSchemaVersion11,
updateToSchemaVersion12,
];
async function updateSchema(instance) {
@ -1135,6 +1160,54 @@ async function removeAllSignedPreKeys() {
return removeAllFromTable(SIGNED_PRE_KEYS_TABLE);
}
const PAIRING_AUTHORISATIONS_TABLE = 'pairingAuthorisations';
async function getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey) {
const row = await db.get(
`SELECT * FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE
issuerPubKey = $issuerPubKey AND secondaryDevicePubKey = $secondaryDevicePubKey
LIMIT 1;`,
{
$issuerPubKey: issuerPubKey,
$secondaryDevicePubKey: secondaryDevicePubKey,
}
);
if (!row) {
return null;
}
return jsonToObject(row.json);
}
async function createOrUpdatePairingAuthorisation(data) {
const { issuerPubKey, secondaryDevicePubKey, signature } = data;
const existing = await getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey);
// prevent adding duplicate entries
if (existing) {
return;
}
await db.run(
`INSERT INTO ${PAIRING_AUTHORISATIONS_TABLE} (
issuerPubKey,
secondaryDevicePubKey,
signature,
json
) values (
$issuerPubKey,
$secondaryDevicePubKey,
$signature,
$json
)`,
{
$issuerPubKey: issuerPubKey,
$secondaryDevicePubKey: secondaryDevicePubKey,
$signature: signature,
$json: objectToJSON(data),
}
);
}
const ITEMS_TABLE = 'items';
async function createOrUpdateItem(data) {
return createOrUpdate(ITEMS_TABLE, data);

View file

@ -12,6 +12,7 @@ const {
merge,
set,
omit,
isArrayBuffer,
} = require('lodash');
const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto');
@ -88,6 +89,8 @@ module.exports = {
removeContactSignedPreKeyByIdentityKey,
removeAllContactSignedPreKeys,
createOrUpdatePairingAuthorisation,
createOrUpdateItem,
getItemById,
getAllItems,
@ -570,6 +573,23 @@ async function removeAllContactSignedPreKeys() {
await channels.removeAllContactSignedPreKeys();
}
async function createOrUpdatePairingAuthorisation(data) {
let sig;
if (isArrayBuffer(data.signature)) {
sig = arrayBufferToBase64(data.signature);
} else if (typeof signature === 'string') {
sig = data.signature;
} else {
throw new Error(
'Invalid signature provided in createOrUpdatePairingAuthorisation. Needs to be either ArrayBuffer or string.'
);
}
return channels.createOrUpdatePairingAuthorisation({
...data,
signature: sig,
});
}
// Items
const ITEM_KEYS = {

View file

@ -63,9 +63,37 @@
await outgoingMessage.sendToNumber(pubKey);
}
async function sendPairingAuthorisation(secondaryDevicePubKey, signature) {
const pairingAuthorisation = new textsecure.protobuf.PairingAuthorisationMessage(
{
signature,
primaryDevicePubKey: textsecure.storage.user.getNumber(),
secondaryDevicePubKey,
type:
textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST,
}
);
const content = new textsecure.protobuf.Content({
pairingAuthorisation,
});
const options = {};
// Send a empty message with information about how to contact us directly
const outgoingMessage = new textsecure.OutgoingMessage(
null, // server
Date.now(), // timestamp,
[secondaryDevicePubKey], // numbers
content, // message
true, // silent
() => null, // callback
options
);
await outgoingMessage.sendToNumber(secondaryDevicePubKey);
}
window.libloki.api = {
sendBackgroundMessage,
sendOnlineBroadcastMessage,
broadcastOnlineStatus,
sendPairingAuthorisation,
};
})();

View file

@ -158,6 +158,47 @@
}
}
async function generateSignatureForPairing(secondaryPubKey, type) {
const pubKeyArrayBuffer = StringView.hexToArrayBuffer(secondaryPubKey);
// Make sure the signature includes the pairing action (pairing or unpairing)
const len = pubKeyArrayBuffer.byteLength;
const data = new Uint8Array(len + 1);
data.set(new Uint8Array(pubKeyArrayBuffer), 0);
data[len] = type;
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const signature = await libsignal.Curve.async.calculateSignature(
myKeyPair.privKey,
data.buffer
);
return new Uint8Array(signature);
}
async function verifyPairingAuthorisation(
issuerPubKey,
secondaryPubKey,
signature,
type
) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
if (StringView.arrayBufferToHex(myKeyPair.pubKey) !== secondaryPubKey) {
throw new Error(
'Invalid pairing authorisation: we are not the recipient of the authorisation!'
);
}
const len = myKeyPair.pubKey.byteLength;
const data = new Uint8Array(len + 1);
data.set(new Uint8Array(myKeyPair.pubKey), 0);
data[len] = type;
const issuerPubKeyArrayBuffer = StringView.hexToArrayBuffer(issuerPubKey);
// Throws for invalid signature
await libsignal.Curve.async.verifySignature(
issuerPubKeyArrayBuffer,
data.buffer,
signature
);
}
const snodeCipher = new LokiSnodeChannel();
window.libloki.crypto = {
@ -166,6 +207,8 @@
FallBackSessionCipher,
FallBackDecryptionError,
snodeCipher,
generateSignatureForPairing,
verifyPairingAuthorisation,
// for testing
_LokiSnodeChannel: LokiSnodeChannel,
_decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,

View file

@ -113,11 +113,24 @@
}
}
async function savePairingAuthorisation(
issuerPubKey,
secondaryDevicePubKey,
signature
) {
return textsecure.storage.protocol.storePairingAuthorisation(
issuerPubKey,
secondaryDevicePubKey,
signature
);
}
window.libloki.storage = {
getPreKeyBundleForContact,
saveContactPreKeyBundle,
removeContactPreKeyBundle,
verifyFriendRequestAcceptPreKey,
savePairingAuthorisation,
};
// Libloki protocol store
@ -243,4 +256,15 @@
store.clearContactSignedPreKeysStore = async () => {
await window.Signal.Data.removeAllContactSignedPreKeys();
};
store.storePairingAuthorisation = (
issuerPubKey,
secondaryDevicePubKey,
signature
) =>
window.Signal.Data.createOrUpdatePairingAuthorisation({
issuerPubKey,
secondaryDevicePubKey,
signature,
});
})();

View file

@ -2,6 +2,8 @@
window,
textsecure,
libsignal,
libloki,
Whisper,
mnemonic,
btoa,
Signal,
@ -553,6 +555,40 @@
this.dispatchEvent(new Event('registration'));
},
async authoriseSecondaryDevice(secondaryDevicePubKey) {
if (secondaryDevicePubKey === textsecure.storage.user.getNumber()) {
throw new Error(
'Cannot register primary device pubkey as secondary device'
);
}
// Validate pubKey
const c = new Whisper.Conversation({
id: secondaryDevicePubKey,
type: 'private',
});
const validationError = c.validateNumber();
if (validationError) {
throw new Error('Invalid secondary device pubkey provided');
}
// Ensure there is a conversation existing
try {
await ConversationController.getOrCreateAndWait(
secondaryDevicePubKey,
'private'
);
} catch (e) {
window.log.error(e);
}
const signature = await libloki.crypto.generateSignatureForPairing(
secondaryDevicePubKey,
textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST
);
await libloki.api.sendPairingAuthorisation(
secondaryDevicePubKey,
signature
);
},
});
textsecure.AccountManager = AccountManager;
})();

View file

@ -1022,6 +1022,49 @@ MessageReceiver.prototype.extend({
}
return this.removeFromCache(envelope);
},
async handlePairingAuthorisationMessage(envelope, pairingAuthorisation) {
const {
type,
primaryDevicePubKey,
secondaryDevicePubKey,
signature,
} = pairingAuthorisation;
const sigArrayBuffer = dcodeIO.ByteBuffer.wrap(signature).toArrayBuffer();
if (
type ===
textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST
) {
window.log.info(
`Received pairing authorisation from ${primaryDevicePubKey}`
);
let validAuthorisation = false;
try {
await libloki.crypto.verifyPairingAuthorisation(
primaryDevicePubKey,
secondaryDevicePubKey,
sigArrayBuffer,
type
);
validAuthorisation = true;
} catch (e) {
window.log.error(e);
}
if (validAuthorisation) {
await libloki.storage.savePairingAuthorisation(
primaryDevicePubKey,
secondaryDevicePubKey,
sigArrayBuffer
);
} else {
window.log.warn(
'Could not verify pairing authorisation signature. Ignoring message.'
);
}
} else {
window.log.warn('Unimplemented pairing authorisation message type');
}
return this.removeFromCache(envelope);
},
handleDataMessage(envelope, msg) {
if (!envelope.isP2p) {
const timestamp = envelope.timestamp.toNumber();
@ -1133,6 +1176,12 @@ MessageReceiver.prototype.extend({
content.lokiAddressMessage
);
}
if (content.pairingAuthorisation) {
return this.handlePairingAuthorisationMessage(
envelope,
content.pairingAuthorisation
);
}
if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage);
}

View file

@ -302,12 +302,13 @@ OutgoingMessage.prototype = {
// Check if we need to attach the preKeys
let sessionCipher;
const isFriendRequest = this.messageType === 'friend-request';
this.fallBackEncryption = this.fallBackEncryption || isFriendRequest;
const flags = this.message.dataMessage
? this.message.dataMessage.get_flags()
: null;
const isEndSession =
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
if (isFriendRequest || isEndSession) {
if (this.fallBackEncryption || isEndSession) {
// Encrypt them with the fallback
const pkb = await libloki.storage.getPreKeyBundleForContact(number);
const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage(
@ -316,7 +317,7 @@ OutgoingMessage.prototype = {
this.message.preKeyBundleMessage = preKeyBundleMessage;
window.log.info('attaching prekeys to outgoing message');
}
if (isFriendRequest) {
if (this.fallBackEncryption) {
sessionCipher = fallBackCipher;
} else {
sessionCipher = new libsignal.SessionCipher(

View file

@ -36,6 +36,7 @@ message Content {
optional TypingMessage typingMessage = 6;
optional PreKeyBundleMessage preKeyBundleMessage = 101;
optional LokiAddressMessage lokiAddressMessage = 102;
optional PairingAuthorisationMessage pairingAuthorisation = 103;
}
message LokiAddressMessage {
@ -48,6 +49,17 @@ message LokiAddressMessage {
optional Type type = 3;
}
message PairingAuthorisationMessage {
enum Type {
PAIRING_REQUEST = 1;
UNPAIRING_REQUEST = 2;
}
optional string primaryDevicePubKey = 1;
optional string secondaryDevicePubKey = 2;
optional bytes signature = 3;
optional Type type = 4;
}
message PreKeyBundleMessage {
optional bytes identityKey = 1;
optional uint32 deviceId = 2;