mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Multi-device part 1: make primary account generate and send authorisation to secondary
This commit is contained in:
parent
a7f999aeb1
commit
d208614909
9 changed files with 288 additions and 2 deletions
73
app/sql.js
73
app/sql.js
|
@ -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);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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;
|
||||
})();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue