@throneless/libsignal-service
Version:
A fork of the the libtextsecure components of Signal-Desktop, adapted for use by nodejs.
1,302 lines (1,091 loc) • 37.3 kB
JavaScript
/*
* vim: ts=2:sw=2:expandtab
*/
/* eslint-disable no-proto */
// eslint-disable-next-line func-names
const debug = require('debug')('libsignal-service:ProtocolStore');
const libsignal = require('@throneless/libsignal-protocol');
const _ = require('underscore');
const crypto = require('./crypto.js');
const helpers = require('./helpers.js');
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
const DEFAULT_POLL_DELAY = 1;
const DEFAULT_CACHE_TIMEOUT = 10;
const Direction = {
SENDING: 1,
RECEIVING: 2,
};
const VerifiedStatus = {
DEFAULT: 0,
VERIFIED: 1,
UNVERIFIED: 2,
};
const CacheStatus = {
DEHYDRATED: 0,
HYDRATING: 1,
HYDRATED: 2,
};
function validateVerifiedStatus(status) {
if (
status === VerifiedStatus.DEFAULT
|| status === VerifiedStatus.VERIFIED
|| status === VerifiedStatus.UNVERIFIED
) {
return true;
}
return false;
}
class IdentityRecord {
constructor(data) {
const {
id,
publicKey,
firstUse,
timestamp,
verified,
nonblockingApproval,
} = data;
if (!helpers.isString(id)) {
throw new Error('Invalid identity key id');
}
if (!(publicKey instanceof ArrayBuffer)) {
throw new Error('Invalid identity key publicKey');
}
if (typeof firstUse !== 'boolean') {
throw new Error('Invalid identity key firstUse');
}
if (typeof timestamp !== 'number' || !(timestamp >= 0)) {
throw new Error('Invalid identity key timestamp');
}
if (!validateVerifiedStatus(verified)) {
throw new Error('Invalid identity key verified');
}
if (typeof nonblockingApproval !== 'boolean') {
throw new Error('Invalid identity key nonblockingApproval');
}
this.id = id;
this.publicKey = publicKey;
this.firstUse = firstUse;
this.timestamp = timestamp;
this.verified = verified;
this.nonblockingApproval = nonblockingApproval;
}
}
async function _hydrateCache(object, field, items, idField) {
const cache = Object.create(null);
for (let i = 0, max = items.length; i < max; i += 1) {
const item = items[i];
const id = item[idField];
cache[id] = item;
}
debug(`ProtocolStore: Finished caching ${field} data`);
// eslint-disable-next-line no-param-reassign
object[field] = cache;
}
class ProtocolStore {
constructor(storage) {
this.storage = storage;
this.Direction = Direction;
this.status = CacheStatus.DEHYDRATED;
}
// Cache
async _hydrateCaches() {
const promises = [
_hydrateCache(
this,
'identityKeys',
await this.storage.getAllIdentityKeys(),
'id'
),
_hydrateCache(
this,
'sessions',
await this.storage.getAllSessions(),
'id'
),
_hydrateCache(this, 'preKeys', await this.storage.getAllPreKeys(), 'id'),
_hydrateCache(
this,
'signedPreKeys',
await this.storage.getAllSignedPreKeys(),
'id'
),
_hydrateCache(
this,
'configuration',
await this.storage.getAllConfiguration(),
'id'
),
];
// Check for group support
if (typeof this.storage.getAllGroups === 'function') {
promises.push(
_hydrateCache(this, 'groups', await this.storage.getAllGroups(), 'id')
);
}
await Promise.all(promises);
}
async _getFromCache(cache, id) {
return new Promise((resolve, reject) => {
if (this.status === CacheStatus.HYDRATED) {
resolve(this[cache][id]);
} else {
const interval = setInterval(() => {
if (this.status === CacheStatus.HYDRATED) {
clearInterval(interval);
clearTimeout(timeout);
resolve(this[cache][id]);
}
}, this.storage.pollDelay || DEFAULT_POLL_DELAY);
const timeout = setTimeout(() => {
clearInterval(interval);
reject(new Error('Timed out retrieving from cache.'));
}, DEFAULT_CACHE_TIMEOUT);
}
});
}
_saveToCache(cache, id, value) {
return new Promise((resolve, reject) => {
if (this.status === CacheStatus.HYDRATED) {
resolve((this[cache][id] = value));
} else {
const interval = setInterval(() => {
if (this.status === CacheStatus.HYDRATED) {
clearInterval(interval);
clearTimeout(timeout);
resolve((this[cache][id] = value));
}
}, this.storage.pollDelay || DEFAULT_POLL_DELAY);
const timeout = setTimeout(() => {
clearInterval(interval);
reject(new Error('Timed out saving to cache.'));
}, DEFAULT_CACHE_TIMEOUT);
}
});
}
async _removeFromCache(cache, id) {
return new Promise((resolve, reject) => {
if (this.status === CacheStatus.HYDRATED) {
resolve(delete this[cache][id]);
} else {
const interval = setInterval(() => {
if (this.status === CacheStatus.HYDRATED) {
clearInterval(interval);
clearTimeout(timeout);
resolve(delete this[cache][id]);
}
}, this.storage.pollDelay || DEFAULT_POLL_DELAY);
const timeout = setTimeout(() => {
clearInterval(interval);
reject(new Error('Timed out deleting from cache.'));
}, DEFAULT_CACHE_TIMEOUT);
}
});
}
async load() {
this.status = CacheStatus.HYDRATING;
return this._hydrateCaches().then(() => {
this.status = CacheStatus.HYDRATED;
});
}
hasGroups() {
// eslint-disable-next-line no-prototype-builtins
return this.hasOwnProperty('groups');
}
// PreKeys
async loadPreKey(keyId) {
const key = await this._getFromCache('preKeys', keyId);
if (key) {
debug('Successfully fetched prekey:', keyId);
return {
pubKey: helpers.convertToArrayBuffer(key.publicKey),
privKey: helpers.convertToArrayBuffer(key.privateKey),
};
}
debug('Failed to fetch prekey:', keyId);
return undefined;
}
async storePreKey(keyId, keyPair) {
const data = {
id: keyId,
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
};
await this._saveToCache('preKeys', keyId, data);
await this.storage.createOrUpdatePreKey(data);
}
async removePreKey(keyId) {
await this._removeFromCache('preKeys', keyId);
await this.storage.removePreKeyById(keyId);
}
async clearPreKeyStore() {
this.preKeys = Object.create(null);
await this.storage.removeAllPreKeys();
}
// Signed PreKeys
async loadSignedPreKey(keyId) {
const key = await this._getFromCache('signedPreKeys', keyId);
if (key) {
debug('Successfully fetched signed prekey:', key.id);
return {
pubKey: helpers.convertToArrayBuffer(key.publicKey),
privKey: helpers.convertToArrayBuffer(key.privateKey),
created_at: key.created_at,
keyId: key.id,
confirmed: key.confirmed,
};
}
debug('Failed to fetch signed prekey:', keyId);
return undefined;
}
async loadSignedPreKeys() {
if (arguments.length > 0) {
throw new Error('loadSignedPreKeys takes no arguments');
}
const keys = Object.values(this.signedPreKeys);
return keys.map(prekey => ({
pubKey: helpers.convertToArrayBuffer(prekey.publicKey),
privKey: helpers.convertToArrayBuffer(prekey.privateKey),
created_at: prekey.created_at,
keyId: prekey.id,
confirmed: prekey.confirmed,
}));
}
async storeSignedPreKey(keyId, keyPair, confirmed) {
const data = {
id: keyId,
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
created_at: Date.now(),
confirmed: Boolean(confirmed),
};
await this._saveToCache('signedPreKeys', keyId, data);
await this.storage.createOrUpdateSignedPreKey(data);
}
async removeSignedPreKey(keyId) {
await this._removeFromCache('signedPreKeys', keyId);
await this.storage.removeSignedPreKeyById(keyId);
}
async clearSignedPreKeysStore() {
this.signedPreKeys = Object.create(null);
await this.storage.removeAllSignedPreKeys();
}
// Sessions
async loadSession(encodedNumber) {
if (encodedNumber === null || encodedNumber === undefined) {
throw new Error('Tried to get session for undefined/null number');
}
debug('loadSession() => encodedNumber:', encodedNumber);
debug('loadSession() => this.sessions', this.sessions);
const session = await this._getFromCache('sessions', encodedNumber);
debug('loadSession() => session:', session);
if (session) {
return session.record;
}
return undefined;
}
async storeSession(encodedNumber, record) {
if (encodedNumber === null || encodedNumber === undefined) {
throw new Error('Tried to put session for undefined/null number');
}
const unencoded = helpers.unencodeNumber(encodedNumber);
const number = unencoded[0];
const deviceId = parseInt(unencoded[1], 10);
const data = {
id: encodedNumber,
number,
deviceId,
record,
};
await this._saveToCache('sessions', encodedNumber, data);
await this.storage.createOrUpdateSession(data);
}
async getDeviceIds(number) {
debug(`Getting device IDs for number ${number}`);
if (number === null || number === undefined) {
throw new Error('Tried to get device ids for undefined/null number');
}
const allSessions = Object.values(this.sessions);
debug('allSessions:', allSessions);
const sessions = allSessions.filter(session => session.number === number);
return _.pluck(sessions, 'deviceId');
}
async removeSession(encodedNumber) {
debug('deleting session for ', encodedNumber);
await this._removeFromCache('sessions', encodedNumber);
await this.storage.removeSessionById(encodedNumber);
}
async removeAllSessions(number) {
if (number === null || number === undefined) {
throw new Error('Tried to remove sessions for undefined/null number');
}
const allSessions = Object.values(this.sessions);
const removeFromCache = [];
for (let i = 0, max = allSessions.length; i < max; i += 1) {
const session = allSessions[i];
if (session.number === number) {
removeFromCache.push(this._removeFromCache('sessions', session.id));
}
}
await Promise.all(removeFromCache);
await this.storage.removeSessionsByNumber(number);
}
async archiveSiblingSessions(identifier) {
const address = libsignal.SignalProtocolAddress.fromString(identifier);
const deviceIds = await this.getDeviceIds(address.getName());
const siblings = _.without(deviceIds, address.getDeviceId());
await Promise.all(
siblings.map(async deviceId => {
const sibling = new libsignal.SignalProtocolAddress(
address.getName(),
deviceId
);
debug('closing session for', sibling.toString());
const sessionCipher = new libsignal.SessionCipher(this, sibling);
await sessionCipher.closeOpenSessionForDevice();
})
);
}
async archiveAllSessions(number) {
const deviceIds = await this.getDeviceIds(number);
await Promise.all(
deviceIds.map(async deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
debug('closing session for', address.toString());
const sessionCipher = new libsignal.SessionCipher(this, address);
await sessionCipher.closeOpenSessionForDevice();
})
);
}
async clearSessionStore() {
this.sessions = Object.create(null);
this.storage.removeAllSessions();
}
// Identity Keys
async isTrustedIdentity(identifier, publicKey, direction) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key');
}
const number = helpers.unencodeNumber(identifier)[0];
const isOurNumber = number === (await this.getNumber());
const identityRecord = await this._getFromCache('identityKeys', number);
if (isOurNumber) {
const existing = identityRecord
? helpers.convertToArrayBuffer(identityRecord.publicKey)
: null;
return helpers.equalArrayBuffers(existing, publicKey);
}
switch (direction) {
case Direction.SENDING:
return this.isTrustedForSending(publicKey, identityRecord);
case Direction.RECEIVING:
return true;
default:
throw new Error(`Unknown direction: ${direction}`);
}
}
isTrustedForSending(publicKey, identityRecord) {
if (!identityRecord) {
debug('isTrustedForSending: No previous record, returning true...');
return true;
}
const existing = helpers.convertToArrayBuffer(identityRecord.publicKey);
if (!existing) {
debug('isTrustedForSending: Nothing here, returning true...');
return true;
}
if (!helpers.equalArrayBuffers(existing, publicKey)) {
debug("isTrustedForSending: Identity keys don't match...");
return false;
}
if (identityRecord.verified === VerifiedStatus.UNVERIFIED) {
debug('Needs unverified approval!');
return false;
}
if (this.isNonBlockingApprovalRequired(identityRecord)) {
debug('isTrustedForSending: Needs non-blocking approval!');
return false;
}
return true;
}
async loadIdentityKey(identifier) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key');
}
const number = helpers.unencodeNumber(identifier)[0];
const identityRecord = await this._getFromCache('identityKeys', number);
if (identityRecord) {
return helpers.convertToArrayBuffer(identityRecord.publicKey);
}
return undefined;
}
async _saveIdentityKey(data) {
const { id } = data;
this._saveToCache('identityKeys', id, data);
await this.storage.createOrUpdateIdentityKey(data);
}
async saveIdentity(identifier, publicKey, nonblockingApproval) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to put identity key for undefined/null key');
}
if (!(publicKey instanceof ArrayBuffer)) {
// eslint-disable-next-line no-param-reassign
publicKey = helpers.convertToArrayBuffer(publicKey);
}
if (typeof nonblockingApproval !== 'boolean') {
// eslint-disable-next-line no-param-reassign
nonblockingApproval = false;
}
const number = helpers.unencodeNumber(identifier)[0];
const identityRecord = await this._getFromCache('identityKeys', number);
if (!identityRecord || !identityRecord.publicKey) {
// Lookup failed, or the current key was removed, so save this one.
debug('Saving new identity...');
await this._saveIdentityKey({
id: number,
publicKey,
firstUse: true,
timestamp: Date.now(),
verified: VerifiedStatus.DEFAULT,
nonblockingApproval,
});
return false;
}
const oldpublicKey = helpers.convertToArrayBuffer(identityRecord.publicKey);
if (!helpers.equalArrayBuffers(oldpublicKey, publicKey)) {
debug('Replacing existing identity...');
const previousStatus = identityRecord.verified;
let verifiedStatus;
if (
previousStatus === VerifiedStatus.VERIFIED
|| previousStatus === VerifiedStatus.UNVERIFIED
) {
verifiedStatus = VerifiedStatus.UNVERIFIED;
} else {
verifiedStatus = VerifiedStatus.DEFAULT;
}
await this._saveIdentityKey({
id: number,
publicKey,
firstUse: false,
timestamp: Date.now(),
verified: verifiedStatus,
nonblockingApproval,
});
try {
this.trigger('keychange', number);
} catch (error) {
debug(
'saveIdentity error triggering keychange:',
error && error.stack ? error.stack : error
);
}
await this.archiveSiblingSessions(identifier);
return true;
} if (this.isNonBlockingApprovalRequired(identityRecord)) {
debug('Setting approval status...');
identityRecord.nonblockingApproval = nonblockingApproval;
await this._saveIdentityKey(identityRecord);
return false;
}
return false;
}
isNonBlockingApprovalRequired(identityRecord) {
return (
!identityRecord.firstUse
&& Date.now() - identityRecord.timestamp < TIMESTAMP_THRESHOLD
&& !identityRecord.nonblockingApproval
);
}
async saveIdentityWithAttributes(identifier, attributes) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to put identity key for undefined/null key');
}
const number = helpers.unencodeNumber(identifier)[0];
const identityRecord = await this._getFromCache('identityKeys', number);
const updates = {
id: number,
...identityRecord,
...attributes,
};
// eslint-disable-next-line no-unused-vars
const model = new IdentityRecord(updates);
await this._saveIdentityKey(updates);
}
async setApproval(identifier, nonblockingApproval) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to set approval for undefined/null identifier');
}
if (typeof nonblockingApproval !== 'boolean') {
throw new Error('Invalid approval status');
}
const number = helpers.unencodeNumber(identifier)[0];
const identityRecord = await this._getFromCache('identityKeys', number);
if (!identityRecord) {
throw new Error(`No identity record for ${number}`);
}
identityRecord.nonblockingApproval = nonblockingApproval;
await this._saveIdentityKey(identityRecord);
}
async setVerified(number, verifiedStatus, publicKey) {
if (number === null || number === undefined) {
throw new Error('Tried to set verified for undefined/null key');
}
if (!validateVerifiedStatus(verifiedStatus)) {
throw new Error('Invalid verified status');
}
if (arguments.length > 2 && !(publicKey instanceof ArrayBuffer)) {
throw new Error('Invalid public key');
}
const identityRecord = await this._getFromCache('identityKeys', number);
if (!identityRecord) {
throw new Error(`No identity record for ${number}`);
}
if (
!publicKey
|| helpers.equalArrayBuffers(identityRecord.publicKey, publicKey)
) {
identityRecord.verified = verifiedStatus;
// eslint-disable-next-line no-unused-vars
const model = new IdentityRecord(identityRecord);
await this._saveIdentityKey(identityRecord);
} else {
debug('No identity record for specified publicKey');
}
}
async getVerified(number) {
if (number === null || number === undefined) {
throw new Error('Tried to set verified for undefined/null key');
}
const identityRecord = await this._getFromCache('identityKeys', number);
if (!identityRecord) {
throw new Error(`No identity record for ${number}`);
}
const verifiedStatus = identityRecord.verified;
if (validateVerifiedStatus(verifiedStatus)) {
return verifiedStatus;
}
return VerifiedStatus.DEFAULT;
}
// Resolves to true if a new identity key was saved
processContactSyncVerificationState(identifier, verifiedStatus, publicKey) {
if (verifiedStatus === VerifiedStatus.UNVERIFIED) {
return this.processUnverifiedMessage(
identifier,
verifiedStatus,
publicKey
);
}
return this.processVerifiedMessage(identifier, verifiedStatus, publicKey);
}
// This function encapsulates the non-Java behavior, since the mobile apps don't
// currently receive contact syncs and therefore will see a verify sync with
// UNVERIFIED status
async processUnverifiedMessage(number, verifiedStatus, publicKey) {
if (number === null || number === undefined) {
throw new Error('Tried to set verified for undefined/null key');
}
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
throw new Error('Invalid public key');
}
const identityRecord = await this._getFromCache('identityKeys', number);
const isPresent = Boolean(identityRecord);
let isEqual = false;
if (isPresent && publicKey) {
isEqual = helpers.equalArrayBuffers(publicKey, identityRecord.publicKey);
}
if (
isPresent
&& isEqual
&& identityRecord.verified !== VerifiedStatus.UNVERIFIED
) {
await this.setVerified(number, verifiedStatus, publicKey);
return false;
}
if (!isPresent || !isEqual) {
await this.saveIdentityWithAttributes(number, {
publicKey,
verified: verifiedStatus,
firstUse: false,
timestamp: Date.now(),
nonblockingApproval: true,
});
if (isPresent && !isEqual) {
try {
this.trigger('keychange', number);
} catch (error) {
debug(
'processUnverifiedMessage error triggering keychange:',
error && error.stack ? error.stack : error
);
}
await this.archiveAllSessions(number);
return true;
}
}
// The situation which could get us here is:
// 1. had a previous key
// 2. new key is the same
// 3. desired new status is same as what we had before
return false;
}
// This matches the Java method as of
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
async processVerifiedMessage(number, verifiedStatus, publicKey) {
if (number === null || number === undefined) {
throw new Error('Tried to set verified for undefined/null key');
}
if (!validateVerifiedStatus(verifiedStatus)) {
throw new Error('Invalid verified status');
}
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
throw new Error('Invalid public key');
}
const identityRecord = await this._getFromCache('identityKeys', number);
const isPresent = Boolean(identityRecord);
let isEqual = false;
if (isPresent && publicKey) {
isEqual = helpers.equalArrayBuffers(publicKey, identityRecord.publicKey);
}
if (!isPresent && verifiedStatus === VerifiedStatus.DEFAULT) {
debug('No existing record for default status');
return false;
}
if (
isPresent
&& isEqual
&& identityRecord.verified !== VerifiedStatus.DEFAULT
&& verifiedStatus === VerifiedStatus.DEFAULT
) {
await this.setVerified(number, verifiedStatus, publicKey);
return false;
}
if (
verifiedStatus === VerifiedStatus.VERIFIED
&& (!isPresent
|| (isPresent && !isEqual)
|| (isPresent && identityRecord.verified !== VerifiedStatus.VERIFIED))
) {
await this.saveIdentityWithAttributes(number, {
publicKey,
verified: verifiedStatus,
firstUse: false,
timestamp: Date.now(),
nonblockingApproval: true,
});
if (isPresent && !isEqual) {
try {
this.trigger('keychange', number);
} catch (error) {
debug(
'processVerifiedMessage error triggering keychange:',
error && error.stack ? error.stack : error
);
}
await this.archiveAllSessions(number);
// true signifies that we overwrote a previous key with a new one
return true;
}
}
// We get here if we got a new key and the status is DEFAULT. If the
// message is out of date, we don't want to lose whatever more-secure
// state we had before.
return false;
}
async isUntrusted(number) {
if (number === null || number === undefined) {
throw new Error('Tried to set verified for undefined/null key');
}
const identityRecord = await this._getFromCache('identityKeys', number);
if (!identityRecord) {
throw new Error(`No identity record for ${number}`);
}
if (
Date.now() - identityRecord.timestamp < TIMESTAMP_THRESHOLD
&& !identityRecord.nonblockingApproval
&& !identityRecord.firstUse
) {
return true;
}
return false;
}
async removeIdentityKey(number) {
await this._removeFromCache('identityKeys', number);
await this.storage.removeIdentityKeyById(number);
await this.removeAllSessions(number);
}
// Not yet processed messages - for resiliency
async getUnprocessedCount() {
return this.storage.getUnprocessedCount();
}
async getAllUnprocessed() {
return this.storage.getAllUnprocessed();
}
async getUnprocessedById(id) {
return this.storage.getUnprocessedById(id);
}
async addUnprocessed(data) {
// We need to pass forceSave because the data has an id already, which will cause
// an update instead of an insert.
return this.storage.saveUnprocessed(data, {
forceSave: true,
});
}
async batchAddUnprocessed(array) {
array.map(item => this.storage.saveUnprocessed(item, { forceSave: true }));
}
async updateUnprocessedAttempts(id, attempts) {
return this.storage.updateUnprocessedAttempts(id, attempts);
}
async updateUnprocessedWithData(id, data) {
return this.storage.updateUnprocessedWithData(id, data);
}
async updateUnprocessedsWithData(array) {
array.map(item => this.storage.updateUnprocessedWithData(item.id, item.data));
}
async removeUnprocessed(id) {
return this.storage.removeUnprocessed(id);
}
async removeAllUnprocessed() {
return this.storage.removeAllUnprocessed();
}
async removeAllData() {
await this.storage.removeAll();
await this.load();
}
async removeAllConfiguration() {
this.configuration = Object.create(null);
await this.storage.removeAllConfiguration();
}
// GROUPS
async getGroup(groupId) {
if (!this.hasGroups()) {
throw new Error('This storage backend does not support groups.');
}
return this.storage.getGroupById(groupId).then(group => {
if (!group) return undefined;
return { id: groupId, numbers: group.numbers };
});
}
async getGroupNumbers(groupId) {
if (!this.hasGroups()) {
throw new Error('This storage backend does not support groups.');
}
const group = await this.getGroup(groupId);
if (!group) return undefined;
return group.numbers;
}
async createNewGroup(groupId, numbers) {
if (!this.hasGroups()) {
throw new Error('This storage backend does not support groups.');
}
debug('Creating new group.');
return new Promise(resolve => {
if (!groupId) {
debug('No groupId specified, generating new groupId');
resolve(
crypto.generateGroupId().then(newGroupId => {
// eslint-disable-next-line no-param-reassign
groupId = newGroupId;
})
);
} else {
resolve(
this.getGroup(groupId).then(group => {
if (group !== undefined) {
throw new Error('Tried to recreate group');
}
})
);
}
})
.then(() => this.getNumber())
.then(me => {
let haveMe = false;
const finalNumbers = [];
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const i in numbers) {
const number = numbers[i];
if (!helpers.isNumberSane(number))
throw new Error('Invalid number in group');
if (number === me) haveMe = true;
if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number);
}
if (!haveMe) finalNumbers.push(me);
const groupObject = {
id: groupId,
numbers: finalNumbers,
numberRegistrationIds: {},
};
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const i in finalNumbers) {
groupObject.numberRegistrationIds[finalNumbers[i]] = {};
}
return this._saveToCache('groups', groupId, groupObject)
.then(this.storage.createOrUpdateGroup(groupObject))
.then(() => ({ id: groupId, numbers: finalNumbers }));
});
}
async deleteGroup(groupId) {
if (!this.hasGroups()) {
throw new Error('This storage backend does not support groups.');
}
await this._removeFromCache('groups', groupId);
await this.storage.removeGroupById(groupId);
}
async updateGroupNumbers(groupId, numbers) {
if (!this.hasGroups()) {
throw new Error('This storage backend does not support groups.');
}
return this.getGroup(groupId).then(group => {
if (group === undefined)
throw new Error('Tried to update numbers for unknown group');
if (numbers.filter(helpers.isNumberSane).length < numbers.length)
throw new Error('Invalid number in new group members');
const added = numbers.filter(number => group.numbers.indexOf(number) < 0);
return this.addGroupNumbers(groupId, added);
});
}
async addGroupNumbers(groupId, numbers) {
if (!this.hasGroups()) {
throw new Error('This storage backend does not support groups.');
}
return this.getGroup(groupId).then(group => {
if (group === undefined) return undefined;
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const i in numbers) {
const number = numbers[i];
if (!helpers.isNumberSane(number))
throw new Error('Invalid number in set to add to group');
if (group.numbers.indexOf(number) < 0) {
group.numbers.push(number);
// eslint-disable-next-line no-param-reassign
group.numberRegistrationIds[number] = {};
}
}
return this._saveToCache('groups', groupId, group).then(() => {
this.storage.createOrUpdateGroup(group);
return group.numbers;
});
});
}
async removeGroupNumber(groupId, number) {
if (!this.hasGroups()) {
throw new Error('This storage backend does not support groups.');
}
return this.getGroup(groupId).then(group => {
if (group === undefined) return undefined;
const me = this.getNumber();
if (number === me)
throw new Error(
'Cannot remove ourselves from a group, leave the group instead'
);
const i = group.numbers.indexOf(number);
if (i > -1) {
group.numbers.splice(i, 1);
// eslint-disable-next-line no-param-reassign
// delete group.numberRegistrationIds[number];
return this._saveToCache('groups', groupId, group).then(() => {
this.storage.createOrUpdateGroup(group);
return group.numbers;
});
}
return group.numbers;
});
}
// OPTIONS
async _saveConfiguration(id, value) {
await this._saveToCache('configuration', id, { id, value });
await this.storage.createOrUpdateConfiguration({ id, value });
}
async _removeConfiguration(id) {
await this._removeFromCache('configuration', id);
await this.storage.removeConfigurationById(id);
}
async _getConfiguration(id) {
const data = await this._getFromCache('configuration', id);
if (data === undefined) return undefined;
return data.value;
}
// User storage
async getIdentityKeyPair() {
const pair = await this._getConfiguration('identityKey');
let { pubKey, privKey } = pair;
if (!(pubKey instanceof ArrayBuffer)) {
// eslint-disable-next-line no-param-reassign
pubKey = helpers.convertToArrayBuffer(pubKey);
}
if (!(privKey instanceof ArrayBuffer)) {
// eslint-disable-next-line no-param-reassign
privKey = helpers.convertToArrayBuffer(privKey);
}
return { privKey, pubKey };
}
async getLocalRegistrationId() {
return this._getConfiguration('registrationId');
}
async setNumberAndDeviceId(number, deviceId, deviceName) {
await this._saveConfiguration('number_id', `${number }.${ deviceId}`);
if (deviceName) {
await this._saveConfiguration('device_name', deviceName);
}
}
async setUuidAndDeviceId(uuid, deviceId, deviceName) {
await this._saveConfiguration('uuid_id', `${uuid }.${ deviceId}`);
if (deviceName) {
await this._saveConfiguration('device_name', deviceName);
}
}
async getNumber() {
const number_id = await this._getConfiguration('number_id');
if (number_id === undefined) return undefined;
return helpers.unencodeNumber(number_id)[0];
}
async getUuid() {
const uuid_id = await this._getConfiguration('uuid_id');
if (uuid_id === undefined) return undefined;
return helpers.unencodeNumber(uuid_id)[0];
}
async _getDeviceIdFromUuid() {
const uuid_id = await this._getConfiguration('uuid_id');
if (uuid_id === undefined) return undefined;
return helpers.unencodeNumber(uuid_id)[1];
}
async _getDeviceIdFromNumber() {
const number_id = await this._getConfiguration('number_id');
if (number_id === undefined) return undefined;
return helpers.unencodeNumber(number_id)[1];
}
async getDeviceId() {
return this._getDeviceIdFromUuid() || this._getDeviceIdFromNumber();
}
async removeNumberAndDeviceId() {
await this._removeConfiguration('number_id');
}
async getDeviceName() {
return this._getConfiguration('device_name');
}
async removeDeviceName() {
await this._removeConfiguration('device_name');
}
async setDeviceNameEncrypted() {
await this._saveConfiguration('deviceNameEncrypted', true);
}
async getDeviceNameEncrypted() {
return this._getConfiguration('deviceNameEncrypted');
}
async getSignalingKey() {
return this._getConfiguration('signaling_key');
}
// Other options
async getSignedKeyId() {
const value = await this._getConfiguration('signedKeyId');
if (value === undefined) return 1;
return value;
}
async setSignedKeyId(value) {
await this._saveConfiguration('signedKeyId', value);
}
async removeSignedKeyId() {
await this._removeConfiguration('signedKeyId');
}
async getSignedKeyRotationRejected() {
const value = await this._getFromCache(
'configuration',
'signedKeyRotationRejected'
);
if (value === undefined) return 0;
return value;
}
async setSignedKeyRotationRejected(value) {
await this._saveConfiguration('signedKeyRotationRejected', value);
}
async removeSignedKeyRotationRejected() {
await this._removeConfiguration('signedKeyRotationRejected');
}
async getMaxPreKeyId() {
const value = await this._getConfiguration('maxPreKeyId');
if (value === undefined) return 1;
return value;
}
async setMaxPreKeyId(value) {
await this._saveConfiguration('maxPreKeyId', value);
}
async getBlocked() {
const value = await this._getConfiguration('blocked');
if (value === undefined) return [];
return value;
}
async setBlocked(value) {
await this._saveConfiguration('blocked', value);
}
async getBlockedUuids() {
const value = await this._getConfiguration('blocked-uuids');
if (value === undefined) return [];
return value;
}
async setBlockedUuids(value) {
await this._saveConfiguration('blocked-uuids', value);
}
async getBlockedGroups() {
const value = await this._getConfiguration('blocked-groups');
if (value === undefined) return [];
return value;
}
async setBlockedGroups(value) {
await this._saveConfiguration('blockedGroups', value);
}
async setIdentityKeyPair(value) {
await this._saveConfiguration('identityKey', value);
}
async removeIdentityKeyPair() {
await this._removeConfiguration('identityKey');
}
async getPassword() {
return this._getConfiguration('password');
}
async setPassword(value) {
await this._saveConfiguration('password', value);
}
async removePassword() {
await this._removeConfiguration('password');
}
async setLocalRegistrationId(value) {
await this._saveConfiguration('registrationId', value);
}
async removeLocalRegistrationId() {
await this._saveConfiguration('registrationId');
}
async getProfileKey() {
return this._getConfiguration('profileKey');
}
async setProfileKey(value) {
await this._saveConfiguration('profileKey', value);
}
async removeProfileKey() {
await this._removeConfiguration('profileKey');
}
async setUserAgent(value) {
await this._saveConfiguration('userAgent', value);
}
async removeUserAgent() {
await this._removeConfiguration('userAgent');
}
async setReadReceiptSetting(value) {
await this._saveConfiguration('read-receipts-setting', value);
}
async removeReadReceiptsSetting() {
await this._removeConfiguration('read-receipts-setting');
}
async setRegionCode(value) {
await this._saveConfiguration('regionCode', value);
}
async removeRegionCode() {
await this._removeConfiguration('regionCode');
}
async setSignalingKey(value) {
await this._saveConfiguration('signaling_key', value);
}
// Groups
}
exports = module.exports = ProtocolStore;