2023-01-03 19:55:46 +00:00
// Copyright 2016 Signal Messenger, LLC
2021-02-26 23:42:45 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2021-04-16 23:13:13 +00:00
import PQueue from 'p-queue' ;
2022-07-28 16:35:29 +00:00
import { isNumber , omit } from 'lodash' ;
2021-05-25 22:40:04 +00:00
import { z } from 'zod' ;
2022-10-20 19:16:37 +00:00
import { EventEmitter } from 'events' ;
2021-04-16 23:13:13 +00:00
import {
2021-05-14 01:18:43 +00:00
Direction ,
2022-08-02 01:31:24 +00:00
IdentityKeyPair ,
2023-07-14 16:53:20 +00:00
KyberPreKeyRecord ,
2021-04-16 23:13:13 +00:00
PreKeyRecord ,
PrivateKey ,
PublicKey ,
2021-05-14 01:18:43 +00:00
SenderKeyRecord ,
SessionRecord ,
2021-04-16 23:13:13 +00:00
SignedPreKeyRecord ,
2022-03-24 21:47:21 +00:00
} from '@signalapp/libsignal-client' ;
2021-04-16 23:13:13 +00:00
2024-07-22 18:16:33 +00:00
import { DataReader , DataWriter } from './sql/Client' ;
import type { ItemType } from './sql/Interface' ;
2021-09-24 00:49:05 +00:00
import * as Bytes from './Bytes' ;
2022-08-19 18:05:31 +00:00
import { constantTimeEqual , sha256 } from './Crypto' ;
2022-09-15 19:17:15 +00:00
import { assertDev , strictAssert } from './util/assert' ;
2021-02-26 23:42:45 +00:00
import { isNotNil } from './util/isNotNil' ;
2024-07-22 18:16:33 +00:00
import { drop } from './util/drop' ;
2021-05-19 21:25:56 +00:00
import { Zone } from './util/Zone' ;
2021-03-22 21:08:52 +00:00
import { isMoreRecentThan } from './util/timestamp' ;
2021-04-05 22:18:19 +00:00
import {
2021-04-16 23:13:13 +00:00
sessionRecordToProtobuf ,
2021-09-24 00:49:05 +00:00
sessionStructureToBytes ,
2021-04-16 23:13:13 +00:00
} from './util/sessionTranslation' ;
2021-10-26 19:15:33 +00:00
import type {
2021-05-25 22:40:04 +00:00
DeviceType ,
2021-04-05 22:18:19 +00:00
IdentityKeyType ,
2021-09-10 02:38:11 +00:00
IdentityKeyIdType ,
KeyPairType ,
2023-07-14 16:53:20 +00:00
KyberPreKeyType ,
2021-09-10 02:38:11 +00:00
OuterSignedPrekeyType ,
2022-08-02 01:31:24 +00:00
PniKeyMaterialType ,
2022-08-15 21:53:33 +00:00
PniSignatureMessageType ,
2021-09-10 02:38:11 +00:00
PreKeyIdType ,
PreKeyType ,
SenderKeyIdType ,
2021-05-14 01:18:43 +00:00
SenderKeyType ,
2021-09-10 02:38:11 +00:00
SessionIdType ,
2021-06-15 00:09:37 +00:00
SessionResetsType ,
2021-04-16 23:13:13 +00:00
SessionType ,
2021-09-10 02:38:11 +00:00
SignedPreKeyIdType ,
2021-04-05 22:18:19 +00:00
SignedPreKeyType ,
UnprocessedType ,
2021-04-16 23:13:13 +00:00
UnprocessedUpdateType ,
2023-07-14 16:53:20 +00:00
CompatPreKeyType ,
2021-04-16 23:13:13 +00:00
} from './textsecure/Types.d' ;
2023-08-10 16:43:33 +00:00
import type { ServiceIdString , PniString , AciString } from './types/ServiceId' ;
import { isServiceIdString , ServiceIdKind } from './types/ServiceId' ;
2021-10-26 19:15:33 +00:00
import type { Address } from './types/Address' ;
import type { QualifiedAddressStringType } from './types/QualifiedAddress' ;
import { QualifiedAddress } from './types/QualifiedAddress' ;
2021-09-17 18:27:53 +00:00
import * as log from './logging/log' ;
2022-01-14 21:34:52 +00:00
import * as Errors from './types/errors' ;
2022-06-27 16:46:43 +00:00
import { MINUTE } from './util/durations' ;
2023-03-14 20:25:05 +00:00
import { conversationJobQueue } from './jobs/conversationJobQueue' ;
2023-07-14 16:53:20 +00:00
import {
KYBER_KEY_ID_KEY ,
SIGNED_PRE_KEY_ID_KEY ,
} from './textsecure/AccountManager' ;
2023-10-19 21:52:35 +00:00
import { formatGroups , groupWhile } from './util/groupWhile' ;
2021-02-26 23:42:45 +00:00
const TIMESTAMP_THRESHOLD = 5 * 1000 ; // 5 seconds
2023-07-14 16:53:20 +00:00
const LOW_KEYS_THRESHOLD = 25 ;
2021-02-26 23:42:45 +00:00
const VerifiedStatus = {
DEFAULT : 0 ,
VERIFIED : 1 ,
UNVERIFIED : 2 ,
} ;
function validateVerifiedStatus ( status : number ) : boolean {
if (
status === VerifiedStatus . DEFAULT ||
status === VerifiedStatus . VERIFIED ||
status === VerifiedStatus . UNVERIFIED
) {
return true ;
}
return false ;
}
2021-04-16 23:13:13 +00:00
const identityKeySchema = z . object ( {
id : z.string ( ) ,
2021-09-24 00:49:05 +00:00
publicKey : z.instanceof ( Uint8Array ) ,
2021-04-16 23:13:13 +00:00
firstUse : z.boolean ( ) ,
timestamp : z.number ( ) . refine ( ( value : number ) = > value % 1 === 0 && value > 0 ) ,
verified : z.number ( ) . refine ( validateVerifiedStatus ) ,
nonblockingApproval : z.boolean ( ) ,
2021-02-26 23:42:45 +00:00
} ) ;
2021-04-16 23:13:13 +00:00
function validateIdentityKey ( attrs : unknown ) : attrs is IdentityKeyType {
// We'll throw if this doesn't match
identityKeySchema . parse ( attrs ) ;
return true ;
}
2023-10-20 16:39:28 +00:00
/ *
* Potentially hundreds of items , so we ' ll group together sequences ,
* take the first 10 of the sequences , format them as ranges ,
* and log that once .
* = > '1-10, 12, 14-20'
* /
function formatKeys ( keys : Array < number > ) : string {
return formatGroups (
groupWhile ( keys . sort ( ) , ( a , b ) = > a + 1 === b ) . slice ( 0 , 10 ) ,
'-' ,
', ' ,
String
) ;
}
2021-04-16 23:13:13 +00:00
2021-05-14 01:18:43 +00:00
type HasIdType < T > = {
id : T ;
2021-02-26 23:42:45 +00:00
} ;
2021-04-16 23:13:13 +00:00
type CacheEntryType < DBType , HydratedType > =
| {
hydrated : false ;
fromDB : DBType ;
}
| { hydrated : true ; fromDB : DBType ; item : HydratedType } ;
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
type MapFields =
2023-07-14 16:53:20 +00:00
| 'kyberPreKeys'
2021-05-17 18:03:42 +00:00
| 'identityKeys'
| 'preKeys'
| 'senderKeys'
| 'sessions'
| 'signedPreKeys' ;
2022-08-15 21:53:33 +00:00
export type SessionTransactionOptions = Readonly < {
zone? : Zone ;
} > ;
2024-03-12 01:08:38 +00:00
export type SaveIdentityOptions = Readonly < {
zone? : Zone ;
noOverwrite? : boolean ;
} > ;
2022-08-15 21:53:33 +00:00
export type VerifyAlternateIdentityOptionsType = Readonly < {
2023-08-10 16:43:33 +00:00
aci : AciString ;
pni : PniString ;
2022-08-15 21:53:33 +00:00
signature : Uint8Array ;
} > ;
2021-05-17 18:03:42 +00:00
2022-11-07 23:21:12 +00:00
export type SetVerifiedExtra = Readonly < {
firstUse? : boolean ;
nonblockingApproval? : boolean ;
} > ;
2021-05-19 21:25:56 +00:00
export const GLOBAL_ZONE = new Zone ( 'GLOBAL_ZONE' ) ;
2021-05-17 18:03:42 +00:00
2021-05-14 01:18:43 +00:00
async function _fillCaches < ID , T extends HasIdType < ID > , HydratedType > (
2021-02-26 23:42:45 +00:00
object : SignalProtocolStore ,
2021-05-17 18:03:42 +00:00
field : MapFields ,
2021-02-26 23:42:45 +00:00
itemsPromise : Promise < Array < T > >
) : Promise < void > {
const items = await itemsPromise ;
2021-05-14 01:18:43 +00:00
const cache = new Map < ID , CacheEntryType < T , HydratedType > > ( ) ;
2021-02-26 23:42:45 +00:00
for ( let i = 0 , max = items . length ; i < max ; i += 1 ) {
2021-04-16 23:13:13 +00:00
const fromDB = items [ i ] ;
const { id } = fromDB ;
2021-02-26 23:42:45 +00:00
2021-05-14 01:18:43 +00:00
cache . set ( id , {
2021-04-16 23:13:13 +00:00
fromDB ,
hydrated : false ,
2021-05-14 01:18:43 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-17 18:27:53 +00:00
log . info ( ` SignalProtocolStore: Finished caching ${ field } data ` ) ;
2021-02-26 23:42:45 +00:00
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
object [ field ] = cache as any ;
}
2021-04-16 23:13:13 +00:00
export function hydrateSession ( session : SessionType ) : SessionRecord {
return SessionRecord . deserialize ( Buffer . from ( session . record , 'base64' ) ) ;
}
export function hydratePublicKey ( identityKey : IdentityKeyType ) : PublicKey {
return PublicKey . deserialize ( Buffer . from ( identityKey . publicKey ) ) ;
}
export function hydratePreKey ( preKey : PreKeyType ) : PreKeyRecord {
const publicKey = PublicKey . deserialize ( Buffer . from ( preKey . publicKey ) ) ;
const privateKey = PrivateKey . deserialize ( Buffer . from ( preKey . privateKey ) ) ;
2021-09-10 02:38:11 +00:00
return PreKeyRecord . new ( preKey . keyId , publicKey , privateKey ) ;
2021-04-16 23:13:13 +00:00
}
export function hydrateSignedPreKey (
signedPreKey : SignedPreKeyType
) : SignedPreKeyRecord {
const createdAt = signedPreKey . created_at ;
const pubKey = PublicKey . deserialize ( Buffer . from ( signedPreKey . publicKey ) ) ;
const privKey = PrivateKey . deserialize ( Buffer . from ( signedPreKey . privateKey ) ) ;
const signature = Buffer . from ( [ ] ) ;
return SignedPreKeyRecord . new (
2021-09-10 02:38:11 +00:00
signedPreKey . keyId ,
2021-04-16 23:13:13 +00:00
createdAt ,
pubKey ,
privKey ,
signature
) ;
}
2021-02-26 23:42:45 +00:00
2021-04-16 23:13:13 +00:00
export function freezeSession ( session : SessionRecord ) : string {
return session . serialize ( ) . toString ( 'base64' ) ;
}
2021-09-24 00:49:05 +00:00
export function freezePublicKey ( publicKey : PublicKey ) : Uint8Array {
return publicKey . serialize ( ) ;
2021-04-16 23:13:13 +00:00
}
export function freezePreKey ( preKey : PreKeyRecord ) : KeyPairType {
const keyPair = {
2021-09-24 00:49:05 +00:00
pubKey : preKey.publicKey ( ) . serialize ( ) ,
privKey : preKey.privateKey ( ) . serialize ( ) ,
2021-04-16 23:13:13 +00:00
} ;
return keyPair ;
}
export function freezeSignedPreKey (
signedPreKey : SignedPreKeyRecord
) : KeyPairType {
const keyPair = {
2021-09-24 00:49:05 +00:00
pubKey : signedPreKey.publicKey ( ) . serialize ( ) ,
privKey : signedPreKey.privateKey ( ) . serialize ( ) ,
2021-04-16 23:13:13 +00:00
} ;
return keyPair ;
}
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
type SessionCacheEntry = CacheEntryType < SessionType , SessionRecord > ;
2022-01-08 02:12:13 +00:00
type SenderKeyCacheEntry = CacheEntryType < SenderKeyType , SenderKeyRecord > ;
2021-05-17 18:03:42 +00:00
2021-08-24 21:07:40 +00:00
type ZoneQueueEntryType = Readonly < {
zone : Zone ;
callback ( ) : void ;
} > ;
2022-10-20 19:16:37 +00:00
export class SignalProtocolStore extends EventEmitter {
2021-02-26 23:42:45 +00:00
// Enums used across the app
VerifiedStatus = VerifiedStatus ;
// Cached values
2023-08-10 16:43:33 +00:00
private ourIdentityKeys = new Map < ServiceIdString , KeyPairType > ( ) ;
2021-02-26 23:42:45 +00:00
2023-08-10 16:43:33 +00:00
private ourRegistrationIds = new Map < ServiceIdString , number > ( ) ;
2021-02-26 23:42:45 +00:00
2022-08-15 21:53:33 +00:00
private cachedPniSignatureMessage : PniSignatureMessageType | undefined ;
2021-09-10 02:38:11 +00:00
identityKeys? : Map <
IdentityKeyIdType ,
CacheEntryType < IdentityKeyType , PublicKey >
> ;
2021-05-14 01:18:43 +00:00
2023-07-14 16:53:20 +00:00
kyberPreKeys? : Map <
PreKeyIdType ,
CacheEntryType < KyberPreKeyType , KyberPreKeyRecord >
> ;
2022-01-08 02:12:13 +00:00
senderKeys? : Map < SenderKeyIdType , SenderKeyCacheEntry > ;
2021-04-16 23:13:13 +00:00
2021-09-10 02:38:11 +00:00
sessions? : Map < SessionIdType , SessionCacheEntry > ;
2021-05-17 18:03:42 +00:00
2021-09-10 02:38:11 +00:00
preKeys? : Map < PreKeyIdType , CacheEntryType < PreKeyType , PreKeyRecord > > ;
2021-02-26 23:42:45 +00:00
2021-05-14 01:18:43 +00:00
signedPreKeys? : Map <
2021-09-10 02:38:11 +00:00
SignedPreKeyIdType ,
2021-04-16 23:13:13 +00:00
CacheEntryType < SignedPreKeyType , SignedPreKeyRecord >
> ;
2021-02-26 23:42:45 +00:00
2021-09-10 02:38:11 +00:00
senderKeyQueues = new Map < QualifiedAddressStringType , PQueue > ( ) ;
2021-05-14 01:18:43 +00:00
2021-09-10 02:38:11 +00:00
sessionQueues = new Map < SessionIdType , PQueue > ( ) ;
2021-02-26 23:42:45 +00:00
2023-06-14 20:51:49 +00:00
sessionQueueJobCounter = 0 ;
2023-08-10 16:43:33 +00:00
private readonly identityQueues = new Map < ServiceIdString , PQueue > ( ) ;
2023-04-19 16:13:48 +00:00
2021-05-19 21:25:56 +00:00
private currentZone? : Zone ;
private currentZoneDepth = 0 ;
2021-08-24 21:07:40 +00:00
private readonly zoneQueue : Array < ZoneQueueEntryType > = [ ] ;
2021-05-19 21:25:56 +00:00
2021-09-10 02:38:11 +00:00
private pendingSessions = new Map < SessionIdType , SessionCacheEntry > ( ) ;
2021-05-19 21:25:56 +00:00
2022-01-08 02:12:13 +00:00
private pendingSenderKeys = new Map < SenderKeyIdType , SenderKeyCacheEntry > ( ) ;
2021-05-19 21:25:56 +00:00
private pendingUnprocessed = new Map < string , UnprocessedType > ( ) ;
2021-02-26 23:42:45 +00:00
async hydrateCaches ( ) : Promise < void > {
await Promise . all ( [
( async ( ) = > {
2021-09-10 02:38:11 +00:00
this . ourIdentityKeys . clear ( ) ;
2024-07-22 18:16:33 +00:00
const map = ( await DataReader . getItemById (
'identityKeyMap'
) ) as unknown as ItemType < 'identityKeyMap' > ;
2021-09-10 02:38:11 +00:00
if ( ! map ) {
return ;
}
2023-08-10 16:43:33 +00:00
for ( const serviceId of Object . keys ( map . value ) ) {
strictAssert (
isServiceIdString ( serviceId ) ,
'Invalid identity key serviceId'
) ;
const { privKey , pubKey } = map . value [ serviceId ] ;
this . ourIdentityKeys . set ( serviceId , {
2022-07-28 16:35:29 +00:00
privKey ,
pubKey ,
2021-09-10 02:38:11 +00:00
} ) ;
}
2021-02-26 23:42:45 +00:00
} ) ( ) ,
( async ( ) = > {
2021-09-10 02:38:11 +00:00
this . ourRegistrationIds . clear ( ) ;
2024-07-22 18:16:33 +00:00
const map = ( await DataReader . getItemById (
'registrationIdMap'
) ) as unknown as ItemType < 'registrationIdMap' > ;
2021-09-10 02:38:11 +00:00
if ( ! map ) {
return ;
}
2023-08-10 16:43:33 +00:00
for ( const serviceId of Object . keys ( map . value ) ) {
strictAssert (
isServiceIdString ( serviceId ) ,
'Invalid registration id serviceId'
) ;
this . ourRegistrationIds . set ( serviceId , map . value [ serviceId ] ) ;
2021-09-10 02:38:11 +00:00
}
2021-02-26 23:42:45 +00:00
} ) ( ) ,
2021-05-14 01:18:43 +00:00
_fillCaches < string , IdentityKeyType , PublicKey > (
2021-02-26 23:42:45 +00:00
this ,
'identityKeys' ,
2024-07-22 18:16:33 +00:00
DataReader . getAllIdentityKeys ( )
2021-02-26 23:42:45 +00:00
) ,
2023-07-14 16:53:20 +00:00
_fillCaches < string , KyberPreKeyType , KyberPreKeyRecord > (
this ,
'kyberPreKeys' ,
2024-07-22 18:16:33 +00:00
DataReader . getAllKyberPreKeys ( )
2023-07-14 16:53:20 +00:00
) ,
2021-05-14 01:18:43 +00:00
_fillCaches < string , SessionType , SessionRecord > (
2021-02-26 23:42:45 +00:00
this ,
'sessions' ,
2024-07-22 18:16:33 +00:00
DataReader . getAllSessions ( )
2021-02-26 23:42:45 +00:00
) ,
2021-09-10 02:38:11 +00:00
_fillCaches < string , PreKeyType , PreKeyRecord > (
2021-02-26 23:42:45 +00:00
this ,
'preKeys' ,
2024-07-22 18:16:33 +00:00
DataReader . getAllPreKeys ( )
2021-02-26 23:42:45 +00:00
) ,
2021-05-14 01:18:43 +00:00
_fillCaches < string , SenderKeyType , SenderKeyRecord > (
this ,
'senderKeys' ,
2024-07-22 18:16:33 +00:00
DataReader . getAllSenderKeys ( )
2021-05-14 01:18:43 +00:00
) ,
2021-09-10 02:38:11 +00:00
_fillCaches < string , SignedPreKeyType , SignedPreKeyRecord > (
2021-02-26 23:42:45 +00:00
this ,
'signedPreKeys' ,
2024-07-22 18:16:33 +00:00
DataReader . getAllSignedPreKeys ( )
2021-02-26 23:42:45 +00:00
) ,
] ) ;
}
2023-08-10 16:43:33 +00:00
getIdentityKeyPair ( ourServiceId : ServiceIdString ) : KeyPairType | undefined {
return this . ourIdentityKeys . get ( ourServiceId ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
async getLocalRegistrationId (
ourServiceId : ServiceIdString
) : Promise < number | undefined > {
return this . ourRegistrationIds . get ( ourServiceId ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
private _getKeyId (
ourServiceId : ServiceIdString ,
keyId : number
) : PreKeyIdType {
return ` ${ ourServiceId } : ${ keyId } ` ;
2023-07-14 16:53:20 +00:00
}
// KyberPreKeys
private _getKyberPreKeyEntry (
id : PreKeyIdType ,
logContext : string
) :
| { hydrated : true ; fromDB : KyberPreKeyType ; item : KyberPreKeyRecord }
| undefined {
if ( ! this . kyberPreKeys ) {
throw new Error ( ` ${ logContext } : this.kyberPreKeys not yet cached! ` ) ;
}
const entry = this . kyberPreKeys . get ( id ) ;
if ( ! entry ) {
log . error ( ` ${ logContext } : Failed to fetch kyber prekey: ${ id } ` ) ;
return undefined ;
}
if ( entry . hydrated ) {
log . info (
` ${ logContext } : Successfully fetched kyber prekey (cache hit): ${ id } `
) ;
return entry ;
}
const item = KyberPreKeyRecord . deserialize ( Buffer . from ( entry . fromDB . data ) ) ;
const newEntry = {
hydrated : true as const ,
fromDB : entry.fromDB ,
item ,
} ;
this . kyberPreKeys . set ( id , newEntry ) ;
log . info (
` ${ logContext } : Successfully fetched kyberPreKey (cache miss): ${ id } `
) ;
return newEntry ;
}
async loadKyberPreKey (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2023-07-14 16:53:20 +00:00
keyId : number
) : Promise < KyberPreKeyRecord | undefined > {
2023-08-10 16:43:33 +00:00
const id : PreKeyIdType = this . _getKeyId ( ourServiceId , keyId ) ;
2023-07-14 16:53:20 +00:00
const entry = this . _getKyberPreKeyEntry ( id , 'loadKyberPreKey' ) ;
return entry ? . item ;
}
loadKyberPreKeys (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2023-07-14 16:53:20 +00:00
{ isLastResort } : { isLastResort : boolean }
) : Array < KyberPreKeyType > {
if ( ! this . kyberPreKeys ) {
throw new Error ( 'loadKyberPreKeys: this.kyberPreKeys not yet cached!' ) ;
}
if ( arguments . length > 2 ) {
throw new Error ( 'loadKyberPreKeys takes two arguments' ) ;
}
const entries = Array . from ( this . kyberPreKeys . values ( ) ) ;
return entries
. map ( item = > item . fromDB )
. filter (
item = >
2023-08-16 20:54:39 +00:00
item . ourServiceId === ourServiceId &&
item . isLastResort === isLastResort
2023-07-14 16:53:20 +00:00
) ;
}
2023-08-10 16:43:33 +00:00
async confirmKyberPreKey (
ourServiceId : ServiceIdString ,
keyId : number
) : Promise < void > {
2023-07-14 16:53:20 +00:00
const kyberPreKeyCache = this . kyberPreKeys ;
if ( ! kyberPreKeyCache ) {
throw new Error ( 'storeKyberPreKey: this.kyberPreKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const id : PreKeyIdType = this . _getKeyId ( ourServiceId , keyId ) ;
2023-07-14 16:53:20 +00:00
const item = kyberPreKeyCache . get ( id ) ;
if ( ! item ) {
throw new Error ( ` confirmKyberPreKey: missing kyber prekey ${ id } ! ` ) ;
}
const confirmedItem = {
. . . item ,
fromDB : {
. . . item . fromDB ,
isConfirmed : true ,
} ,
} ;
2024-07-22 18:16:33 +00:00
await DataWriter . createOrUpdateKyberPreKey ( confirmedItem . fromDB ) ;
2023-07-14 16:53:20 +00:00
kyberPreKeyCache . set ( id , confirmedItem ) ;
}
async storeKyberPreKeys (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2023-07-14 16:53:20 +00:00
keys : Array < Omit < KyberPreKeyType , ' id ' > >
) : Promise < void > {
const kyberPreKeyCache = this . kyberPreKeys ;
if ( ! kyberPreKeyCache ) {
throw new Error ( 'storeKyberPreKey: this.kyberPreKeys not yet cached!' ) ;
}
const toSave : Array < KyberPreKeyType > = [ ] ;
keys . forEach ( key = > {
2023-08-10 16:43:33 +00:00
const id : PreKeyIdType = this . _getKeyId ( ourServiceId , key . keyId ) ;
2023-07-14 16:53:20 +00:00
if ( kyberPreKeyCache . has ( id ) ) {
throw new Error ( ` storeKyberPreKey: kyber prekey ${ id } already exists! ` ) ;
}
const kyberPreKey = {
id ,
createdAt : key.createdAt ,
data : key.data ,
isConfirmed : key.isConfirmed ,
isLastResort : key.isLastResort ,
keyId : key.keyId ,
2023-08-16 20:54:39 +00:00
ourServiceId ,
2023-07-14 16:53:20 +00:00
} ;
toSave . push ( kyberPreKey ) ;
} ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . bulkAddKyberPreKeys ( toSave ) ;
2023-07-14 16:53:20 +00:00
toSave . forEach ( kyberPreKey = > {
kyberPreKeyCache . set ( kyberPreKey . id , {
hydrated : false ,
fromDB : kyberPreKey ,
} ) ;
} ) ;
}
2023-08-10 16:43:33 +00:00
async maybeRemoveKyberPreKey (
ourServiceId : ServiceIdString ,
keyId : number
) : Promise < void > {
const id : PreKeyIdType = this . _getKeyId ( ourServiceId , keyId ) ;
2023-07-14 16:53:20 +00:00
const entry = this . _getKyberPreKeyEntry ( id , 'maybeRemoveKyberPreKey' ) ;
if ( ! entry ) {
return ;
}
if ( entry . fromDB . isLastResort ) {
log . info (
` maybeRemoveKyberPreKey: Not removing kyber prekey ${ id } ; it's a last resort key `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
await this . removeKyberPreKeys ( ourServiceId , [ keyId ] ) ;
2023-07-14 16:53:20 +00:00
}
async removeKyberPreKeys (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2023-07-14 16:53:20 +00:00
keyIds : Array < number >
) : Promise < void > {
const kyberPreKeyCache = this . kyberPreKeys ;
if ( ! kyberPreKeyCache ) {
throw new Error ( 'removeKyberPreKeys: this.kyberPreKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const ids = keyIds . map ( keyId = > this . _getKeyId ( ourServiceId , keyId ) ) ;
2023-07-14 16:53:20 +00:00
2023-10-20 16:39:28 +00:00
log . info ( 'removeKyberPreKeys: Removing kyber prekeys:' , formatKeys ( keyIds ) ) ;
2024-07-22 18:16:33 +00:00
const changes = await DataWriter . removeKyberPreKeyById ( ids ) ;
2023-10-19 21:52:35 +00:00
log . info ( ` removeKyberPreKeys: Removed ${ changes } kyber prekeys ` ) ;
2023-07-14 16:53:20 +00:00
ids . forEach ( id = > {
kyberPreKeyCache . delete ( id ) ;
} ) ;
if ( kyberPreKeyCache . size < LOW_KEYS_THRESHOLD ) {
2023-08-10 16:43:33 +00:00
this . emitLowKeys (
ourServiceId ,
` removeKyberPreKeys@ ${ kyberPreKeyCache . size } `
) ;
2023-07-14 16:53:20 +00:00
}
}
async clearKyberPreKeyStore ( ) : Promise < void > {
if ( this . kyberPreKeys ) {
this . kyberPreKeys . clear ( ) ;
}
2024-07-22 18:16:33 +00:00
const changes = await DataWriter . removeAllKyberPreKeys ( ) ;
2023-10-19 21:52:35 +00:00
log . info ( ` clearKyberPreKeyStore: Removed ${ changes } kyber prekeys ` ) ;
2023-07-14 16:53:20 +00:00
}
2021-02-26 23:42:45 +00:00
// PreKeys
2021-09-10 02:38:11 +00:00
async loadPreKey (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2021-09-10 02:38:11 +00:00
keyId : number
) : Promise < PreKeyRecord | undefined > {
2021-02-26 23:42:45 +00:00
if ( ! this . preKeys ) {
throw new Error ( 'loadPreKey: this.preKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const id : PreKeyIdType = this . _getKeyId ( ourServiceId , keyId ) ;
2021-09-10 02:38:11 +00:00
const entry = this . preKeys . get ( id ) ;
2021-04-16 23:13:13 +00:00
if ( ! entry ) {
2021-09-17 18:27:53 +00:00
log . error ( 'Failed to fetch prekey:' , id ) ;
2021-04-16 23:13:13 +00:00
return undefined ;
2021-02-26 23:42:45 +00:00
}
2021-04-16 23:13:13 +00:00
if ( entry . hydrated ) {
2021-09-17 18:27:53 +00:00
log . info ( 'Successfully fetched prekey (cache hit):' , id ) ;
2021-04-16 23:13:13 +00:00
return entry . item ;
}
const item = hydratePreKey ( entry . fromDB ) ;
2021-09-10 02:38:11 +00:00
this . preKeys . set ( id , {
2021-04-16 23:13:13 +00:00
hydrated : true ,
fromDB : entry.fromDB ,
item ,
2021-05-14 01:18:43 +00:00
} ) ;
2021-09-17 18:27:53 +00:00
log . info ( 'Successfully fetched prekey (cache miss):' , id ) ;
2021-04-16 23:13:13 +00:00
return item ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
loadPreKeys ( ourServiceId : ServiceIdString ) : Array < PreKeyType > {
2023-07-14 16:53:20 +00:00
if ( ! this . preKeys ) {
throw new Error ( 'loadPreKeys: this.preKeys not yet cached!' ) ;
}
if ( arguments . length > 1 ) {
throw new Error ( 'loadPreKeys takes one argument' ) ;
}
const entries = Array . from ( this . preKeys . values ( ) ) ;
return entries
. map ( item = > item . fromDB )
2023-08-16 20:54:39 +00:00
. filter ( item = > item . ourServiceId === ourServiceId ) ;
2023-07-14 16:53:20 +00:00
}
async storePreKeys (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2023-07-14 16:53:20 +00:00
keys : Array < CompatPreKeyType >
2021-09-10 02:38:11 +00:00
) : Promise < void > {
2023-07-14 16:53:20 +00:00
const preKeyCache = this . preKeys ;
if ( ! preKeyCache ) {
2021-02-26 23:42:45 +00:00
throw new Error ( 'storePreKey: this.preKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
2023-07-14 16:53:20 +00:00
const now = Date . now ( ) ;
const toSave : Array < PreKeyType > = [ ] ;
keys . forEach ( key = > {
2023-08-10 16:43:33 +00:00
const id : PreKeyIdType = this . _getKeyId ( ourServiceId , key . keyId ) ;
2021-02-26 23:42:45 +00:00
2023-07-14 16:53:20 +00:00
if ( preKeyCache . has ( id ) ) {
throw new Error ( ` storePreKeys: prekey ${ id } already exists! ` ) ;
}
2021-02-26 23:42:45 +00:00
2023-07-14 16:53:20 +00:00
const preKey = {
id ,
keyId : key.keyId ,
2023-08-16 20:54:39 +00:00
ourServiceId ,
2023-07-14 16:53:20 +00:00
publicKey : key.keyPair.pubKey ,
privateKey : key.keyPair.privKey ,
createdAt : now ,
} ;
toSave . push ( preKey ) ;
} ) ;
2023-10-19 21:52:35 +00:00
log . info ( ` storePreKeys: Saving ${ toSave . length } prekeys ` ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . bulkAddPreKeys ( toSave ) ;
2023-07-14 16:53:20 +00:00
toSave . forEach ( preKey = > {
preKeyCache . set ( preKey . id , {
hydrated : false ,
fromDB : preKey ,
} ) ;
2021-05-14 01:18:43 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
async removePreKeys (
ourServiceId : ServiceIdString ,
keyIds : Array < number >
) : Promise < void > {
2023-07-14 16:53:20 +00:00
const preKeyCache = this . preKeys ;
if ( ! preKeyCache ) {
throw new Error ( 'removePreKeys: this.preKeys not yet cached!' ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
const ids = keyIds . map ( keyId = > this . _getKeyId ( ourServiceId , keyId ) ) ;
2021-09-10 02:38:11 +00:00
2023-10-20 16:39:28 +00:00
log . info ( 'removePreKeys: Removing prekeys:' , formatKeys ( keyIds ) ) ;
2023-10-19 21:52:35 +00:00
2024-07-22 18:16:33 +00:00
const changes = await DataWriter . removePreKeyById ( ids ) ;
2023-10-19 21:52:35 +00:00
log . info ( ` removePreKeys: Removed ${ changes } prekeys ` ) ;
2023-07-14 16:53:20 +00:00
ids . forEach ( id = > {
preKeyCache . delete ( id ) ;
} ) ;
2021-02-26 23:42:45 +00:00
2023-07-14 16:53:20 +00:00
if ( preKeyCache . size < LOW_KEYS_THRESHOLD ) {
2023-08-10 16:43:33 +00:00
this . emitLowKeys ( ourServiceId , ` removePreKeys@ ${ preKeyCache . size } ` ) ;
2023-07-14 16:53:20 +00:00
}
2021-02-26 23:42:45 +00:00
}
async clearPreKeyStore ( ) : Promise < void > {
2021-05-14 01:18:43 +00:00
if ( this . preKeys ) {
this . preKeys . clear ( ) ;
}
2024-07-22 18:16:33 +00:00
const changes = await DataWriter . removeAllPreKeys ( ) ;
2023-10-19 21:52:35 +00:00
log . info ( ` clearPreKeyStore: Removed ${ changes } prekeys ` ) ;
2021-02-26 23:42:45 +00:00
}
// Signed PreKeys
async loadSignedPreKey (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2021-02-26 23:42:45 +00:00
keyId : number
2021-04-16 23:13:13 +00:00
) : Promise < SignedPreKeyRecord | undefined > {
2021-02-26 23:42:45 +00:00
if ( ! this . signedPreKeys ) {
throw new Error ( 'loadSignedPreKey: this.signedPreKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const id : SignedPreKeyIdType = ` ${ ourServiceId } : ${ keyId } ` ;
2021-09-10 02:38:11 +00:00
const entry = this . signedPreKeys . get ( id ) ;
2021-04-16 23:13:13 +00:00
if ( ! entry ) {
2021-09-17 18:27:53 +00:00
log . error ( 'Failed to fetch signed prekey:' , id ) ;
2021-04-16 23:13:13 +00:00
return undefined ;
2021-02-26 23:42:45 +00:00
}
2021-04-16 23:13:13 +00:00
if ( entry . hydrated ) {
2021-09-17 18:27:53 +00:00
log . info ( 'Successfully fetched signed prekey (cache hit):' , id ) ;
2021-04-16 23:13:13 +00:00
return entry . item ;
}
const item = hydrateSignedPreKey ( entry . fromDB ) ;
2021-09-10 02:38:11 +00:00
this . signedPreKeys . set ( id , {
2021-04-16 23:13:13 +00:00
hydrated : true ,
item ,
fromDB : entry.fromDB ,
2021-05-14 01:18:43 +00:00
} ) ;
2021-09-17 18:27:53 +00:00
log . info ( 'Successfully fetched signed prekey (cache miss):' , id ) ;
2021-04-16 23:13:13 +00:00
return item ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
loadSignedPreKeys (
ourServiceId : ServiceIdString
) : Array < OuterSignedPrekeyType > {
2021-02-26 23:42:45 +00:00
if ( ! this . signedPreKeys ) {
throw new Error ( 'loadSignedPreKeys: this.signedPreKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
if ( arguments . length > 1 ) {
throw new Error ( 'loadSignedPreKeys takes one argument' ) ;
2021-02-26 23:42:45 +00:00
}
2021-05-14 01:18:43 +00:00
const entries = Array . from ( this . signedPreKeys . values ( ) ) ;
2021-09-10 02:38:11 +00:00
return entries
2023-08-16 20:54:39 +00:00
. filter ( ( { fromDB } ) = > fromDB . ourServiceId === ourServiceId )
2021-09-10 02:38:11 +00:00
. map ( entry = > {
const preKey = entry . fromDB ;
return {
pubKey : preKey.publicKey ,
privKey : preKey.privateKey ,
created_at : preKey.created_at ,
keyId : preKey.keyId ,
confirmed : preKey.confirmed ,
} ;
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
async confirmSignedPreKey (
ourServiceId : ServiceIdString ,
keyId : number
) : Promise < void > {
2023-07-14 16:53:20 +00:00
const signedPreKeyCache = this . signedPreKeys ;
if ( ! signedPreKeyCache ) {
throw new Error ( 'storeKyberPreKey: this.signedPreKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const id : PreKeyIdType = this . _getKeyId ( ourServiceId , keyId ) ;
2023-07-14 16:53:20 +00:00
const item = signedPreKeyCache . get ( id ) ;
if ( ! item ) {
throw new Error ( ` confirmSignedPreKey: missing prekey ${ id } ! ` ) ;
}
const confirmedItem = {
. . . item ,
fromDB : {
. . . item . fromDB ,
confirmed : true ,
} ,
} ;
2024-07-22 18:16:33 +00:00
await DataWriter . createOrUpdateSignedPreKey ( confirmedItem . fromDB ) ;
2023-07-14 16:53:20 +00:00
signedPreKeyCache . set ( id , confirmedItem ) ;
}
2021-02-26 23:42:45 +00:00
async storeSignedPreKey (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2021-02-26 23:42:45 +00:00
keyId : number ,
keyPair : KeyPairType ,
2022-07-28 16:35:29 +00:00
confirmed? : boolean ,
createdAt = Date . now ( )
2021-02-26 23:42:45 +00:00
) : Promise < void > {
if ( ! this . signedPreKeys ) {
throw new Error ( 'storeSignedPreKey: this.signedPreKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const id : SignedPreKeyIdType = this . _getKeyId ( ourServiceId , keyId ) ;
2021-09-10 02:38:11 +00:00
2021-04-16 23:13:13 +00:00
const fromDB = {
2021-09-10 02:38:11 +00:00
id ,
2023-08-16 20:54:39 +00:00
ourServiceId ,
2021-09-10 02:38:11 +00:00
keyId ,
2021-02-26 23:42:45 +00:00
publicKey : keyPair.pubKey ,
privateKey : keyPair.privKey ,
2022-07-28 16:35:29 +00:00
created_at : createdAt ,
2021-02-26 23:42:45 +00:00
confirmed : Boolean ( confirmed ) ,
} ;
2024-07-22 18:16:33 +00:00
await DataWriter . createOrUpdateSignedPreKey ( fromDB ) ;
2021-09-10 02:38:11 +00:00
this . signedPreKeys . set ( id , {
2021-04-16 23:13:13 +00:00
hydrated : false ,
fromDB ,
2021-05-14 01:18:43 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-07-14 16:53:20 +00:00
async removeSignedPreKeys (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
2023-07-14 16:53:20 +00:00
keyIds : Array < number >
) : Promise < void > {
const signedPreKeyCache = this . signedPreKeys ;
if ( ! signedPreKeyCache ) {
2021-02-26 23:42:45 +00:00
throw new Error ( 'removeSignedPreKey: this.signedPreKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const ids = keyIds . map ( keyId = > this . _getKeyId ( ourServiceId , keyId ) ) ;
2023-07-14 16:53:20 +00:00
2023-10-20 16:39:28 +00:00
log . info (
'removeSignedPreKeys: Removing signed prekeys:' ,
formatKeys ( keyIds )
) ;
2024-07-22 18:16:33 +00:00
await DataWriter . removeSignedPreKeyById ( ids ) ;
2023-07-14 16:53:20 +00:00
ids . forEach ( id = > {
signedPreKeyCache . delete ( id ) ;
} ) ;
2021-02-26 23:42:45 +00:00
}
async clearSignedPreKeysStore ( ) : Promise < void > {
2021-05-14 01:18:43 +00:00
if ( this . signedPreKeys ) {
this . signedPreKeys . clear ( ) ;
}
2024-07-22 18:16:33 +00:00
const changes = await DataWriter . removeAllSignedPreKeys ( ) ;
2023-10-19 21:52:35 +00:00
log . info ( ` clearSignedPreKeysStore: Removed ${ changes } signed prekeys ` ) ;
2021-02-26 23:42:45 +00:00
}
2022-01-08 02:12:13 +00:00
// Sender Key
// Re-entrant sender key transaction routine. Only one sender key transaction could
// be running at the same time.
//
// While in transaction:
//
// - `saveSenderKey()` adds the updated session to the `pendingSenderKeys`
// - `getSenderKey()` looks up the session first in `pendingSenderKeys` and only
// then in the main `senderKeys` store
//
// When transaction ends:
//
// - successfully: pending sender key stores are batched into the database
// - with an error: pending sender key stores are reverted
2021-05-14 01:18:43 +00:00
async enqueueSenderKeyJob < T > (
2021-09-10 02:38:11 +00:00
qualifiedAddress : QualifiedAddress ,
2021-05-20 23:49:08 +00:00
task : ( ) = > Promise < T > ,
zone = GLOBAL_ZONE
2021-05-14 01:18:43 +00:00
) : Promise < T > {
2021-05-20 23:49:08 +00:00
return this . withZone ( zone , 'enqueueSenderKeyJob' , async ( ) = > {
2021-09-10 02:38:11 +00:00
const queue = this . _getSenderKeyQueue ( qualifiedAddress ) ;
2021-05-14 01:18:43 +00:00
2021-05-20 23:49:08 +00:00
return queue . add < T > ( task ) ;
} ) ;
2021-05-14 01:18:43 +00:00
}
private _createSenderKeyQueue ( ) : PQueue {
2021-11-23 22:01:03 +00:00
return new PQueue ( {
concurrency : 1 ,
2022-06-27 16:46:43 +00:00
timeout : MINUTE * 30 ,
2021-11-23 22:01:03 +00:00
throwOnTimeout : true ,
} ) ;
2021-05-14 01:18:43 +00:00
}
2021-09-10 02:38:11 +00:00
private _getSenderKeyQueue ( senderId : QualifiedAddress ) : PQueue {
const cachedQueue = this . senderKeyQueues . get ( senderId . toString ( ) ) ;
2021-05-14 01:18:43 +00:00
if ( cachedQueue ) {
return cachedQueue ;
}
const freshQueue = this . _createSenderKeyQueue ( ) ;
2021-09-10 02:38:11 +00:00
this . senderKeyQueues . set ( senderId . toString ( ) , freshQueue ) ;
2021-05-14 01:18:43 +00:00
return freshQueue ;
}
2021-09-10 02:38:11 +00:00
private getSenderKeyId (
senderKeyId : QualifiedAddress ,
distributionId : string
) : SenderKeyIdType {
return ` ${ senderKeyId . toString ( ) } -- ${ distributionId } ` ;
2021-05-14 01:18:43 +00:00
}
async saveSenderKey (
2021-09-10 02:38:11 +00:00
qualifiedAddress : QualifiedAddress ,
2021-05-14 01:18:43 +00:00
distributionId : string ,
2022-01-08 02:12:13 +00:00
record : SenderKeyRecord ,
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-14 01:18:43 +00:00
) : Promise < void > {
2022-01-08 02:12:13 +00:00
await this . withZone ( zone , 'saveSenderKey' , async ( ) = > {
if ( ! this . senderKeys ) {
throw new Error ( 'saveSenderKey: this.senderKeys not yet cached!' ) ;
}
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
const senderId = qualifiedAddress . toString ( ) ;
2021-09-10 02:38:11 +00:00
2022-01-08 02:12:13 +00:00
try {
const id = this . getSenderKeyId ( qualifiedAddress , distributionId ) ;
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
const fromDB : SenderKeyType = {
id ,
senderId ,
distributionId ,
data : record.serialize ( ) ,
lastUpdatedDate : Date.now ( ) ,
} ;
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
this . pendingSenderKeys . set ( id , {
hydrated : true ,
fromDB ,
item : record ,
} ) ;
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
// Current zone doesn't support pending sessions - commit immediately
if ( ! zone . supportsPendingSenderKeys ( ) ) {
await this . commitZoneChanges ( 'saveSenderKey' ) ;
}
} catch ( error ) {
2022-11-22 18:43:43 +00:00
const errorString = Errors . toLogFormat ( error ) ;
2022-01-08 02:12:13 +00:00
log . error (
` saveSenderKey: failed to save senderKey ${ senderId } / ${ distributionId } : ${ errorString } `
) ;
}
} ) ;
2021-05-14 01:18:43 +00:00
}
async getSenderKey (
2021-09-10 02:38:11 +00:00
qualifiedAddress : QualifiedAddress ,
2022-01-08 02:12:13 +00:00
distributionId : string ,
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-14 01:18:43 +00:00
) : Promise < SenderKeyRecord | undefined > {
2022-01-08 02:12:13 +00:00
return this . withZone ( zone , 'getSenderKey' , async ( ) = > {
if ( ! this . senderKeys ) {
throw new Error ( 'getSenderKey: this.senderKeys not yet cached!' ) ;
}
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
const senderId = qualifiedAddress . toString ( ) ;
2021-09-10 02:38:11 +00:00
2022-01-08 02:12:13 +00:00
try {
const id = this . getSenderKeyId ( qualifiedAddress , distributionId ) ;
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
const map = this . pendingSenderKeys . has ( id )
? this . pendingSenderKeys
: this . senderKeys ;
const entry = map . get ( id ) ;
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
if ( ! entry ) {
log . error ( 'Failed to fetch sender key:' , id ) ;
return undefined ;
}
2021-05-14 01:18:43 +00:00
2022-01-08 02:12:13 +00:00
if ( entry . hydrated ) {
log . info ( 'Successfully fetched sender key (cache hit):' , id ) ;
return entry . item ;
}
const item = SenderKeyRecord . deserialize (
Buffer . from ( entry . fromDB . data )
) ;
this . senderKeys . set ( id , {
hydrated : true ,
item ,
fromDB : entry.fromDB ,
} ) ;
log . info ( 'Successfully fetched sender key(cache miss):' , id ) ;
return item ;
} catch ( error ) {
2022-11-22 18:43:43 +00:00
const errorString = Errors . toLogFormat ( error ) ;
2022-01-08 02:12:13 +00:00
log . error (
` getSenderKey: failed to load sender key ${ senderId } / ${ distributionId } : ${ errorString } `
) ;
return undefined ;
}
} ) ;
2021-05-14 01:18:43 +00:00
}
2021-05-25 22:40:04 +00:00
async removeSenderKey (
2021-09-10 02:38:11 +00:00
qualifiedAddress : QualifiedAddress ,
2021-05-25 22:40:04 +00:00
distributionId : string
) : Promise < void > {
if ( ! this . senderKeys ) {
throw new Error ( 'getSenderKey: this.senderKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
const senderId = qualifiedAddress . toString ( ) ;
2021-05-25 22:40:04 +00:00
try {
2021-09-10 02:38:11 +00:00
const id = this . getSenderKeyId ( qualifiedAddress , distributionId ) ;
2021-05-25 22:40:04 +00:00
2024-07-22 18:16:33 +00:00
await DataWriter . removeSenderKeyById ( id ) ;
2021-05-25 22:40:04 +00:00
this . senderKeys . delete ( id ) ;
} catch ( error ) {
2022-11-22 18:43:43 +00:00
const errorString = Errors . toLogFormat ( error ) ;
2021-09-17 18:27:53 +00:00
log . error (
2021-09-10 02:38:11 +00:00
` removeSenderKey: failed to remove senderKey ${ senderId } / ${ distributionId } : ${ errorString } `
2021-05-25 22:40:04 +00:00
) ;
}
}
2022-01-08 02:12:13 +00:00
async removeAllSenderKeys ( ) : Promise < void > {
return this . withZone ( GLOBAL_ZONE , 'removeAllSenderKeys' , async ( ) = > {
if ( this . senderKeys ) {
this . senderKeys . clear ( ) ;
}
if ( this . pendingSenderKeys ) {
this . pendingSenderKeys . clear ( ) ;
}
2024-07-22 18:16:33 +00:00
await DataWriter . removeAllSenderKeys ( ) ;
2022-01-08 02:12:13 +00:00
} ) ;
2021-07-15 23:48:09 +00:00
}
2021-04-16 23:13:13 +00:00
// Session Queue
async enqueueSessionJob < T > (
2021-09-10 02:38:11 +00:00
qualifiedAddress : QualifiedAddress ,
2023-06-14 20:51:49 +00:00
name : string ,
2021-05-20 23:49:08 +00:00
task : ( ) = > Promise < T > ,
zone : Zone = GLOBAL_ZONE
2021-04-16 23:13:13 +00:00
) : Promise < T > {
2023-06-14 20:51:49 +00:00
this . sessionQueueJobCounter += 1 ;
const id = this . sessionQueueJobCounter ;
const waitStart = Date . now ( ) ;
2021-05-20 23:49:08 +00:00
return this . withZone ( zone , 'enqueueSessionJob' , async ( ) = > {
2021-09-10 02:38:11 +00:00
const queue = this . _getSessionQueue ( qualifiedAddress ) ;
2021-04-16 23:13:13 +00:00
2023-06-14 20:51:49 +00:00
const waitTime = Date . now ( ) - waitStart ;
log . info (
` enqueueSessionJob( ${ id } ): queuing task ${ name } , waited ${ waitTime } ms `
) ;
const queueStart = Date . now ( ) ;
return queue . add < T > ( ( ) = > {
const queueTime = Date . now ( ) - queueStart ;
log . info (
` enqueueSessionJob( ${ id } ): running task ${ name } , waited ${ queueTime } ms `
) ;
return task ( ) ;
} ) ;
2021-05-20 23:49:08 +00:00
} ) ;
2021-04-16 23:13:13 +00:00
}
private _createSessionQueue ( ) : PQueue {
2021-11-23 22:01:03 +00:00
return new PQueue ( {
concurrency : 1 ,
2022-06-27 16:46:43 +00:00
timeout : MINUTE * 30 ,
2021-11-23 22:01:03 +00:00
throwOnTimeout : true ,
} ) ;
2021-04-16 23:13:13 +00:00
}
2021-09-10 02:38:11 +00:00
private _getSessionQueue ( id : QualifiedAddress ) : PQueue {
const cachedQueue = this . sessionQueues . get ( id . toString ( ) ) ;
2021-04-16 23:13:13 +00:00
if ( cachedQueue ) {
return cachedQueue ;
}
const freshQueue = this . _createSessionQueue ( ) ;
2021-09-10 02:38:11 +00:00
this . sessionQueues . set ( id . toString ( ) , freshQueue ) ;
2021-04-16 23:13:13 +00:00
return freshQueue ;
}
2023-04-19 16:13:48 +00:00
// Identity Queue
private _createIdentityQueue ( ) : PQueue {
return new PQueue ( {
concurrency : 1 ,
timeout : MINUTE * 30 ,
throwOnTimeout : true ,
} ) ;
}
2023-10-05 00:39:09 +00:00
private _runOnIdentityQueue < T > (
serviceId : ServiceIdString ,
zone : Zone ,
name : string ,
body : ( ) = > Promise < T >
) : Promise < T > {
let queue : PQueue ;
2023-08-10 16:43:33 +00:00
const cachedQueue = this . identityQueues . get ( serviceId ) ;
2023-04-19 16:13:48 +00:00
if ( cachedQueue ) {
2023-10-05 00:39:09 +00:00
queue = cachedQueue ;
} else {
queue = this . _createIdentityQueue ( ) ;
this . identityQueues . set ( serviceId , queue ) ;
2023-04-19 16:13:48 +00:00
}
2023-10-05 00:39:09 +00:00
// We run the identity queue task in zone because `saveIdentity` needs to
// be able to archive sibling sessions on keychange. Not entering the zone
// now would mean that we can take locks in different order here and in
// MessageReceiver which will lead to a deadlock.
return this . withZone ( zone , name , ( ) = > queue . add ( body ) ) ;
2023-04-19 16:13:48 +00:00
}
2021-02-26 23:42:45 +00:00
// Sessions
2021-05-17 18:03:42 +00:00
// Re-entrant session transaction routine. Only one session transaction could
// be running at the same time.
//
// While in transaction:
//
// - `storeSession()` adds the updated session to the `pendingSessions`
// - `loadSession()` looks up the session first in `pendingSessions` and only
// then in the main `sessions` store
//
// When transaction ends:
//
// - successfully: pending session stores are batched into the database
// - with an error: pending session stores are reverted
2022-01-08 02:12:13 +00:00
2021-05-19 21:25:56 +00:00
public async withZone < T > (
zone : Zone ,
2021-05-17 18:03:42 +00:00
name : string ,
2021-05-19 21:25:56 +00:00
body : ( ) = > Promise < T >
2021-05-17 18:03:42 +00:00
) : Promise < T > {
2021-05-19 21:25:56 +00:00
const debugName = ` withZone( ${ zone . name } : ${ name } ) ` ;
2021-05-19 18:33:14 +00:00
2021-05-17 18:03:42 +00:00
// Allow re-entering from LibSignalStores
2021-05-19 21:25:56 +00:00
if ( this . currentZone && this . currentZone !== zone ) {
2021-05-19 18:33:14 +00:00
const start = Date . now ( ) ;
2021-09-17 18:27:53 +00:00
log . info ( ` ${ debugName } : locked by ${ this . currentZone . name } , waiting ` ) ;
2021-05-19 18:33:14 +00:00
2021-05-24 22:59:36 +00:00
return new Promise < T > ( ( resolve , reject ) = > {
2021-08-24 21:07:40 +00:00
const callback = async ( ) = > {
2021-05-24 22:59:36 +00:00
const duration = Date . now ( ) - start ;
2021-09-17 18:27:53 +00:00
log . info ( ` ${ debugName } : unlocked after ${ duration } ms ` ) ;
2021-05-24 22:59:36 +00:00
// Call `.withZone` synchronously from `this.zoneQueue` to avoid
// extra in-between ticks while we are on microtasks queue.
try {
resolve ( await this . withZone ( zone , name , body ) ) ;
} catch ( error ) {
reject ( error ) ;
}
2021-08-24 21:07:40 +00:00
} ;
this . zoneQueue . push ( { zone , callback } ) ;
2021-05-24 22:59:36 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-05-19 21:25:56 +00:00
this . enterZone ( zone , name ) ;
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
let result : T ;
2021-02-26 23:42:45 +00:00
try {
2021-05-17 18:03:42 +00:00
result = await body ( ) ;
} catch ( error ) {
2021-05-19 21:25:56 +00:00
if ( this . isInTopLevelZone ( ) ) {
await this . revertZoneChanges ( name , error ) ;
2021-04-16 23:13:13 +00:00
}
2021-05-19 21:25:56 +00:00
this . leaveZone ( zone ) ;
2021-05-17 18:03:42 +00:00
throw error ;
}
2021-02-26 23:42:45 +00:00
2021-05-19 21:25:56 +00:00
if ( this . isInTopLevelZone ( ) ) {
await this . commitZoneChanges ( name ) ;
2021-05-17 18:03:42 +00:00
}
2021-05-19 21:25:56 +00:00
this . leaveZone ( zone ) ;
2021-04-16 23:13:13 +00:00
2021-05-17 18:03:42 +00:00
return result ;
}
2021-05-19 21:25:56 +00:00
private async commitZoneChanges ( name : string ) : Promise < void > {
2022-01-08 02:12:13 +00:00
const { pendingSenderKeys , pendingSessions , pendingUnprocessed } = this ;
2021-05-17 18:03:42 +00:00
2022-01-08 02:12:13 +00:00
if (
pendingSenderKeys . size === 0 &&
pendingSessions . size === 0 &&
pendingUnprocessed . size === 0
) {
2021-05-17 18:03:42 +00:00
return ;
2021-02-26 23:42:45 +00:00
}
2021-05-17 18:03:42 +00:00
2021-09-17 18:27:53 +00:00
log . info (
2022-01-08 02:12:13 +00:00
` commitZoneChanges( ${ name } ): ` +
` pending sender keys ${ pendingSenderKeys . size } , ` +
` pending sessions ${ pendingSessions . size } , ` +
2021-05-17 18:03:42 +00:00
` pending unprocessed ${ pendingUnprocessed . size } `
) ;
2022-01-08 02:12:13 +00:00
this . pendingSenderKeys = new Map ( ) ;
2021-05-17 18:03:42 +00:00
this . pendingSessions = new Map ( ) ;
this . pendingUnprocessed = new Map ( ) ;
2022-01-08 02:12:13 +00:00
// Commit both sender keys, sessions and unprocessed in the same database transaction
// to unroll both on error.
2024-07-22 18:16:33 +00:00
await DataWriter . commitDecryptResult ( {
2022-01-08 02:12:13 +00:00
senderKeys : Array.from ( pendingSenderKeys . values ( ) ) . map (
( { fromDB } ) = > fromDB
) ,
2021-05-17 18:03:42 +00:00
sessions : Array.from ( pendingSessions . values ( ) ) . map (
( { fromDB } ) = > fromDB
) ,
unprocessed : Array.from ( pendingUnprocessed . values ( ) ) ,
} ) ;
// Apply changes to in-memory storage after successful DB write.
2022-01-08 02:12:13 +00:00
const { sessions } = this ;
2022-09-15 19:17:15 +00:00
assertDev (
sessions !== undefined ,
"Can't commit unhydrated session storage"
) ;
2021-05-17 18:03:42 +00:00
pendingSessions . forEach ( ( value , key ) = > {
sessions . set ( key , value ) ;
} ) ;
2022-01-08 02:12:13 +00:00
const { senderKeys } = this ;
2022-09-15 19:17:15 +00:00
assertDev (
2022-01-08 02:12:13 +00:00
senderKeys !== undefined ,
"Can't commit unhydrated sender key storage"
) ;
pendingSenderKeys . forEach ( ( value , key ) = > {
senderKeys . set ( key , value ) ;
} ) ;
2021-05-17 18:03:42 +00:00
}
2021-05-19 21:25:56 +00:00
private async revertZoneChanges ( name : string , error : Error ) : Promise < void > {
2021-09-17 18:27:53 +00:00
log . info (
2021-05-19 21:25:56 +00:00
` revertZoneChanges( ${ name } ): ` +
2022-01-08 02:12:13 +00:00
` pending sender keys size ${ this . pendingSenderKeys . size } , ` +
` pending sessions size ${ this . pendingSessions . size } , ` +
2021-05-19 21:25:56 +00:00
` pending unprocessed size ${ this . pendingUnprocessed . size } ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-05-17 18:03:42 +00:00
) ;
2022-01-08 02:12:13 +00:00
this . pendingSenderKeys . clear ( ) ;
2021-05-17 18:03:42 +00:00
this . pendingSessions . clear ( ) ;
this . pendingUnprocessed . clear ( ) ;
}
2021-05-19 21:25:56 +00:00
private isInTopLevelZone ( ) : boolean {
return this . currentZoneDepth === 1 ;
}
private enterZone ( zone : Zone , name : string ) : void {
this . currentZoneDepth += 1 ;
if ( this . currentZoneDepth === 1 ) {
2022-09-15 19:17:15 +00:00
assertDev ( this . currentZone === undefined , 'Should not be in the zone' ) ;
2021-05-19 21:25:56 +00:00
this . currentZone = zone ;
if ( zone !== GLOBAL_ZONE ) {
2021-09-17 18:27:53 +00:00
log . info ( ` SignalProtocolStore.enterZone( ${ zone . name } : ${ name } ) ` ) ;
2021-05-19 21:25:56 +00:00
}
}
}
private leaveZone ( zone : Zone ) : void {
2022-09-15 19:17:15 +00:00
assertDev ( this . currentZone === zone , 'Should be in the correct zone' ) ;
2021-05-19 21:25:56 +00:00
this . currentZoneDepth -= 1 ;
2022-09-15 19:17:15 +00:00
assertDev (
this . currentZoneDepth >= 0 ,
'Unmatched number of leaveZone calls'
) ;
2021-05-19 21:25:56 +00:00
// Since we allow re-entering zones we might actually be in two overlapping
// async calls. Leave the zone and yield to another one only if there are
// no active zone users anymore.
if ( this . currentZoneDepth !== 0 ) {
return ;
}
if ( zone !== GLOBAL_ZONE ) {
2021-09-17 18:27:53 +00:00
log . info ( ` SignalProtocolStore.leaveZone( ${ zone . name } ) ` ) ;
2021-05-19 21:25:56 +00:00
}
this . currentZone = undefined ;
2021-08-24 21:07:40 +00:00
2021-05-19 21:25:56 +00:00
const next = this . zoneQueue . shift ( ) ;
2021-08-24 21:07:40 +00:00
if ( ! next ) {
return ;
}
const toEnter = [ next ] ;
while ( this . zoneQueue [ 0 ] ? . zone === next . zone ) {
const elem = this . zoneQueue . shift ( ) ;
2022-09-15 19:17:15 +00:00
assertDev ( elem , 'Zone element should be present' ) ;
2021-08-24 21:07:40 +00:00
toEnter . push ( elem ) ;
}
2021-09-17 18:27:53 +00:00
log . info (
2021-08-24 21:07:40 +00:00
` SignalProtocolStore: running blocked ${ toEnter . length } jobs in ` +
` zone ${ next . zone . name } `
) ;
for ( const { callback } of toEnter ) {
callback ( ) ;
2021-05-17 18:03:42 +00:00
}
}
async loadSession (
2021-09-10 02:38:11 +00:00
qualifiedAddress : QualifiedAddress ,
2021-05-19 21:25:56 +00:00
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-17 18:03:42 +00:00
) : Promise < SessionRecord | undefined > {
2021-05-19 21:25:56 +00:00
return this . withZone ( zone , 'loadSession' , async ( ) = > {
if ( ! this . sessions ) {
throw new Error ( 'loadSession: this.sessions not yet cached!' ) ;
}
2021-05-17 18:03:42 +00:00
2022-09-14 21:40:44 +00:00
if ( qualifiedAddress == null ) {
2021-09-10 02:38:11 +00:00
throw new Error ( 'loadSession: qualifiedAddress was undefined/null' ) ;
2021-05-19 21:25:56 +00:00
}
2021-05-17 18:03:42 +00:00
2021-09-10 02:38:11 +00:00
const id = qualifiedAddress . toString ( ) ;
2021-05-19 21:25:56 +00:00
try {
const map = this . pendingSessions . has ( id )
? this . pendingSessions
: this . sessions ;
const entry = map . get ( id ) ;
2021-05-17 18:03:42 +00:00
2021-05-19 21:25:56 +00:00
if ( ! entry ) {
2021-05-17 18:03:42 +00:00
return undefined ;
}
2021-05-19 21:25:56 +00:00
if ( entry . hydrated ) {
return entry . item ;
}
2021-08-03 22:42:23 +00:00
// We'll either just hydrate the item or we'll fully migrate the session
// and save it to the database.
return await this . _maybeMigrateSession ( entry . fromDB , { zone } ) ;
2021-05-19 21:25:56 +00:00
} catch ( error ) {
2022-11-22 18:43:43 +00:00
const errorString = Errors . toLogFormat ( error ) ;
2021-09-17 18:27:53 +00:00
log . error ( ` loadSession: failed to load session ${ id } : ${ errorString } ` ) ;
2021-05-19 21:25:56 +00:00
return undefined ;
}
} ) ;
2021-04-16 23:13:13 +00:00
}
2021-02-26 23:42:45 +00:00
2021-05-25 22:40:04 +00:00
async loadSessions (
2021-09-10 02:38:11 +00:00
qualifiedAddresses : Array < QualifiedAddress > ,
2021-05-25 22:40:04 +00:00
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
) : Promise < Array < SessionRecord > > {
2021-05-28 23:09:17 +00:00
return this . withZone ( zone , 'loadSessions' , async ( ) = > {
2021-05-25 22:40:04 +00:00
const sessions = await Promise . all (
2021-09-10 02:38:11 +00:00
qualifiedAddresses . map ( async address = >
2021-05-25 22:40:04 +00:00
this . loadSession ( address , { zone } )
)
) ;
return sessions . filter ( isNotNil ) ;
} ) ;
}
2021-04-16 23:13:13 +00:00
private async _maybeMigrateSession (
2021-08-03 22:42:23 +00:00
session : SessionType ,
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-04-16 23:13:13 +00:00
) : Promise < SessionRecord > {
2021-08-03 22:42:23 +00:00
if ( ! this . sessions ) {
throw new Error ( '_maybeMigrateSession: this.sessions not yet cached!' ) ;
}
// Already migrated, hydrate and update cache
2021-04-16 23:13:13 +00:00
if ( session . version === 2 ) {
2021-08-03 22:42:23 +00:00
const item = hydrateSession ( session ) ;
const map = this . pendingSessions . has ( session . id )
? this . pendingSessions
: this . sessions ;
map . set ( session . id , {
hydrated : true ,
item ,
fromDB : session ,
} ) ;
return item ;
2021-04-16 23:13:13 +00:00
}
2021-08-03 22:42:23 +00:00
// Not yet converted, need to translate to new format and save
2021-04-16 23:13:13 +00:00
if ( session . version !== undefined ) {
throw new Error ( '_maybeMigrateSession: Unknown session version type!' ) ;
}
2023-08-16 20:54:39 +00:00
const { ourServiceId } = session ;
2021-09-10 02:38:11 +00:00
2023-08-10 16:43:33 +00:00
const keyPair = this . getIdentityKeyPair ( ourServiceId ) ;
2021-04-16 23:13:13 +00:00
if ( ! keyPair ) {
throw new Error ( '_maybeMigrateSession: No identity key for ourself!' ) ;
}
2023-08-10 16:43:33 +00:00
const localRegistrationId = await this . getLocalRegistrationId ( ourServiceId ) ;
2021-04-16 23:13:13 +00:00
if ( ! isNumber ( localRegistrationId ) ) {
throw new Error ( '_maybeMigrateSession: No registration id for ourself!' ) ;
}
const localUserData = {
identityKeyPublic : keyPair.pubKey ,
registrationId : localRegistrationId ,
} ;
2021-09-17 18:27:53 +00:00
log . info ( ` _maybeMigrateSession: Migrating session with id ${ session . id } ` ) ;
2021-04-16 23:13:13 +00:00
const sessionProto = sessionRecordToProtobuf (
JSON . parse ( session . record ) ,
localUserData
) ;
2021-08-03 22:42:23 +00:00
const record = SessionRecord . deserialize (
2021-09-24 00:49:05 +00:00
Buffer . from ( sessionStructureToBytes ( sessionProto ) )
2021-04-16 23:13:13 +00:00
) ;
2021-08-03 22:42:23 +00:00
2021-09-10 02:38:11 +00:00
await this . storeSession ( QualifiedAddress . parse ( session . id ) , record , {
zone ,
} ) ;
2021-08-03 22:42:23 +00:00
return record ;
2021-02-26 23:42:45 +00:00
}
2021-04-16 23:13:13 +00:00
async storeSession (
2021-09-10 02:38:11 +00:00
qualifiedAddress : QualifiedAddress ,
2021-05-17 18:03:42 +00:00
record : SessionRecord ,
2021-05-19 21:25:56 +00:00
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-04-16 23:13:13 +00:00
) : Promise < void > {
2021-05-19 21:25:56 +00:00
await this . withZone ( zone , 'storeSession' , async ( ) = > {
if ( ! this . sessions ) {
throw new Error ( 'storeSession: this.sessions not yet cached!' ) ;
}
2021-02-26 23:42:45 +00:00
2022-09-14 21:40:44 +00:00
if ( qualifiedAddress == null ) {
2021-09-10 02:38:11 +00:00
throw new Error ( 'storeSession: qualifiedAddress was undefined/null' ) ;
2021-05-19 21:25:56 +00:00
}
2023-08-10 16:43:33 +00:00
const { serviceId , deviceId } = qualifiedAddress ;
2021-09-10 02:38:11 +00:00
2022-08-09 21:39:00 +00:00
const conversation = window . ConversationController . lookupOrCreate ( {
2023-08-16 20:54:39 +00:00
serviceId ,
2022-12-03 01:05:27 +00:00
reason : 'SignalProtocolStore.storeSession' ,
2021-09-10 17:17:32 +00:00
} ) ;
2021-09-10 02:38:11 +00:00
strictAssert (
2022-08-09 21:39:00 +00:00
conversation !== undefined ,
2021-09-10 17:17:32 +00:00
'storeSession: Ensure contact ids failed'
2021-09-10 02:38:11 +00:00
) ;
const id = qualifiedAddress . toString ( ) ;
2021-02-26 23:42:45 +00:00
2021-05-19 21:25:56 +00:00
try {
const fromDB = {
id ,
version : 2 ,
2023-08-16 20:54:39 +00:00
ourServiceId : qualifiedAddress.ourServiceId ,
2022-08-09 21:39:00 +00:00
conversationId : conversation.id ,
2023-08-16 20:54:39 +00:00
serviceId ,
2021-05-19 21:25:56 +00:00
deviceId ,
record : record.serialize ( ) . toString ( 'base64' ) ,
} ;
const newSession = {
hydrated : true ,
fromDB ,
item : record ,
} ;
2022-09-15 19:17:15 +00:00
assertDev ( this . currentZone , 'Must run in the zone' ) ;
2021-05-19 21:25:56 +00:00
this . pendingSessions . set ( id , newSession ) ;
// Current zone doesn't support pending sessions - commit immediately
if ( ! zone . supportsPendingSessions ( ) ) {
await this . commitZoneChanges ( 'storeSession' ) ;
2021-05-17 18:03:42 +00:00
}
2021-05-19 21:25:56 +00:00
} catch ( error ) {
2022-11-22 18:43:43 +00:00
const errorString = Errors . toLogFormat ( error ) ;
2021-09-17 18:27:53 +00:00
log . error ( ` storeSession: Save failed for ${ id } : ${ errorString } ` ) ;
2021-05-19 21:25:56 +00:00
throw error ;
}
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-10-23 17:40:42 +00:00
async hasSessionWith ( serviceId : ServiceIdString ) : Promise < boolean > {
return this . withZone ( GLOBAL_ZONE , 'hasSessionWith' , async ( ) = > {
if ( ! this . sessions ) {
throw new Error ( 'getOpenDevices: this.sessions not yet cached!' ) ;
}
return this . _getAllSessions ( ) . some (
( { fromDB } ) = > fromDB . serviceId === serviceId
) ;
} ) ;
}
2021-05-25 22:40:04 +00:00
async getOpenDevices (
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ,
serviceIds : ReadonlyArray < ServiceIdString > ,
2021-08-03 22:42:23 +00:00
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-25 22:40:04 +00:00
) : Promise < {
devices : Array < DeviceType > ;
2023-08-10 16:43:33 +00:00
emptyServiceIds : Array < ServiceIdString > ;
2021-05-25 22:40:04 +00:00
} > {
2021-08-03 22:42:23 +00:00
return this . withZone ( zone , 'getOpenDevices' , async ( ) = > {
2021-05-17 18:03:42 +00:00
if ( ! this . sessions ) {
2021-05-25 22:40:04 +00:00
throw new Error ( 'getOpenDevices: this.sessions not yet cached!' ) ;
2021-05-17 18:03:42 +00:00
}
2023-08-10 16:43:33 +00:00
if ( serviceIds . length === 0 ) {
return { devices : [ ] , emptyServiceIds : [ ] } ;
2021-05-17 18:03:42 +00:00
}
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
try {
2023-08-10 16:43:33 +00:00
const serviceIdSet = new Set ( serviceIds ) ;
2021-05-17 18:03:42 +00:00
const allSessions = this . _getAllSessions ( ) ;
2021-09-10 02:38:11 +00:00
const entries = allSessions . filter (
( { fromDB } ) = >
2023-08-16 20:54:39 +00:00
fromDB . ourServiceId === ourServiceId &&
serviceIdSet . has ( fromDB . serviceId )
2021-04-16 23:13:13 +00:00
) ;
2021-05-25 22:40:04 +00:00
const openEntries : Array <
2021-07-30 18:35:25 +00:00
| undefined
| {
entry : SessionCacheEntry ;
record : SessionRecord ;
}
2021-05-25 22:40:04 +00:00
> = await Promise . all (
2021-05-17 18:03:42 +00:00
entries . map ( async entry = > {
if ( entry . hydrated ) {
const record = entry . item ;
if ( record . hasCurrentState ( ) ) {
2021-07-30 18:35:25 +00:00
return { record , entry } ;
2021-05-17 18:03:42 +00:00
}
return undefined ;
}
2021-04-16 23:13:13 +00:00
2021-08-03 22:42:23 +00:00
const record = await this . _maybeMigrateSession ( entry . fromDB , {
zone ,
} ) ;
2021-04-16 23:13:13 +00:00
if ( record . hasCurrentState ( ) ) {
2021-07-30 18:35:25 +00:00
return { record , entry } ;
2021-04-16 23:13:13 +00:00
}
return undefined ;
2021-05-17 18:03:42 +00:00
} )
) ;
2021-02-26 23:42:45 +00:00
2021-05-25 22:40:04 +00:00
const devices = openEntries
2021-07-30 18:35:25 +00:00
. map ( item = > {
if ( ! item ) {
2021-05-25 22:40:04 +00:00
return undefined ;
}
2021-07-30 18:35:25 +00:00
const { entry , record } = item ;
2021-05-25 22:40:04 +00:00
2023-08-16 20:54:39 +00:00
const { serviceId } = entry . fromDB ;
serviceIdSet . delete ( serviceId ) ;
2021-05-25 22:40:04 +00:00
const id = entry . fromDB . deviceId ;
2021-07-30 18:35:25 +00:00
const registrationId = record . remoteRegistrationId ( ) ;
2021-05-25 22:40:04 +00:00
return {
2023-08-16 20:54:39 +00:00
serviceId ,
2021-05-25 22:40:04 +00:00
id ,
2021-07-30 18:35:25 +00:00
registrationId ,
2021-05-25 22:40:04 +00:00
} ;
} )
. filter ( isNotNil ) ;
2023-08-10 16:43:33 +00:00
const emptyServiceIds = Array . from ( serviceIdSet . values ( ) ) ;
2021-05-25 22:40:04 +00:00
return {
devices ,
2023-08-10 16:43:33 +00:00
emptyServiceIds ,
2021-05-25 22:40:04 +00:00
} ;
2021-05-17 18:03:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2021-05-25 22:40:04 +00:00
'getOpenDevices: Failed to get devices' ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-05-17 18:03:42 +00:00
) ;
2021-05-25 22:40:04 +00:00
throw error ;
2021-05-17 18:03:42 +00:00
}
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
async getDeviceIds ( {
2023-08-10 16:43:33 +00:00
ourServiceId ,
serviceId ,
2021-09-10 02:38:11 +00:00
} : Readonly < {
2023-08-10 16:43:33 +00:00
ourServiceId : ServiceIdString ;
serviceId : ServiceIdString ;
2021-09-10 02:38:11 +00:00
} > ) : Promise < Array < number > > {
2023-08-10 16:43:33 +00:00
const { devices } = await this . getOpenDevices ( ourServiceId , [ serviceId ] ) ;
2021-05-25 22:40:04 +00:00
return devices . map ( ( device : DeviceType ) = > device . id ) ;
}
2021-09-10 02:38:11 +00:00
async removeSession ( qualifiedAddress : QualifiedAddress ) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'removeSession' , async ( ) = > {
2021-05-17 18:03:42 +00:00
if ( ! this . sessions ) {
throw new Error ( 'removeSession: this.sessions not yet cached!' ) ;
}
2021-02-26 23:42:45 +00:00
2021-09-10 02:38:11 +00:00
const id = qualifiedAddress . toString ( ) ;
2021-09-17 18:27:53 +00:00
log . info ( 'removeSession: deleting session for' , id ) ;
2021-05-17 18:03:42 +00:00
try {
2024-07-22 18:16:33 +00:00
await DataWriter . removeSessionById ( id ) ;
2021-05-17 18:03:42 +00:00
this . sessions . delete ( id ) ;
this . pendingSessions . delete ( id ) ;
} catch ( e ) {
2021-09-17 18:27:53 +00:00
log . error ( ` removeSession: Failed to delete session for ${ id } ` ) ;
2021-05-17 18:03:42 +00:00
}
} ) ;
2021-02-26 23:42:45 +00:00
}
2022-12-12 22:06:16 +00:00
async removeSessionsByConversation ( identifier : string ) : Promise < void > {
return this . withZone (
GLOBAL_ZONE ,
'removeSessionsByConversation' ,
async ( ) = > {
if ( ! this . sessions ) {
throw new Error (
'removeSessionsByConversation: this.sessions not yet cached!'
) ;
}
if ( identifier == null ) {
throw new Error (
'removeSessionsByConversation: identifier was undefined/null'
) ;
}
log . info (
'removeSessionsByConversation: deleting sessions for' ,
identifier
) ;
const id = window . ConversationController . getConversationId ( identifier ) ;
strictAssert (
id ,
` removeSessionsByConversation: Conversation not found: ${ identifier } `
) ;
const entries = Array . from ( this . sessions . values ( ) ) ;
2021-02-26 23:42:45 +00:00
2022-12-12 22:06:16 +00:00
for ( let i = 0 , max = entries . length ; i < max ; i += 1 ) {
const entry = entries [ i ] ;
if ( entry . fromDB . conversationId === id ) {
this . sessions . delete ( entry . fromDB . id ) ;
this . pendingSessions . delete ( entry . fromDB . id ) ;
}
}
2024-07-22 18:16:33 +00:00
await DataWriter . removeSessionsByConversation ( id ) ;
2021-05-17 18:03:42 +00:00
}
2022-12-12 22:06:16 +00:00
) ;
}
2021-02-26 23:42:45 +00:00
2023-08-10 16:43:33 +00:00
async removeSessionsByServiceId ( serviceId : ServiceIdString ) : Promise < void > {
return this . withZone ( GLOBAL_ZONE , 'removeSessionsByServiceId' , async ( ) = > {
2022-12-12 22:06:16 +00:00
if ( ! this . sessions ) {
2023-08-10 16:43:33 +00:00
throw new Error (
'removeSessionsByServiceId: this.sessions not yet cached!'
) ;
2022-12-12 22:06:16 +00:00
}
2021-02-26 23:42:45 +00:00
2023-08-10 16:43:33 +00:00
log . info ( 'removeSessionsByServiceId: deleting sessions for' , serviceId ) ;
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
const entries = Array . from ( this . sessions . values ( ) ) ;
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
for ( let i = 0 , max = entries . length ; i < max ; i += 1 ) {
const entry = entries [ i ] ;
2023-08-16 20:54:39 +00:00
if ( entry . fromDB . serviceId === serviceId ) {
2021-05-17 18:03:42 +00:00
this . sessions . delete ( entry . fromDB . id ) ;
this . pendingSessions . delete ( entry . fromDB . id ) ;
}
2021-02-26 23:42:45 +00:00
}
2024-07-22 18:16:33 +00:00
await DataWriter . removeSessionsByServiceId ( serviceId ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-05-21 16:03:01 +00:00
private async _archiveSession ( entry? : SessionCacheEntry , zone? : Zone ) {
2021-04-16 23:13:13 +00:00
if ( ! entry ) {
return ;
}
2021-09-10 02:38:11 +00:00
const addr = QualifiedAddress . parse ( entry . fromDB . id ) ;
2021-05-21 16:03:01 +00:00
await this . enqueueSessionJob (
2021-09-10 02:38:11 +00:00
addr ,
2023-06-14 20:51:49 +00:00
` _archiveSession( ${ addr . toString ( ) } ) ` ,
2021-05-21 16:03:01 +00:00
async ( ) = > {
const item = entry . hydrated
? entry . item
2021-08-03 22:42:23 +00:00
: await this . _maybeMigrateSession ( entry . fromDB , { zone } ) ;
2021-04-16 23:13:13 +00:00
2021-05-21 16:03:01 +00:00
if ( ! item . hasCurrentState ( ) ) {
return ;
}
2021-04-16 23:13:13 +00:00
2021-05-21 16:03:01 +00:00
item . archiveCurrentState ( ) ;
2021-04-16 23:13:13 +00:00
2021-09-10 02:38:11 +00:00
await this . storeSession ( addr , item , { zone } ) ;
2021-05-21 16:03:01 +00:00
} ,
zone
) ;
2021-04-16 23:13:13 +00:00
}
2021-09-10 02:38:11 +00:00
async archiveSession ( qualifiedAddress : QualifiedAddress ) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'archiveSession' , async ( ) = > {
2021-05-17 18:03:42 +00:00
if ( ! this . sessions ) {
throw new Error ( 'archiveSession: this.sessions not yet cached!' ) ;
}
2021-04-16 23:13:13 +00:00
2021-09-10 02:38:11 +00:00
const id = qualifiedAddress . toString ( ) ;
2021-04-16 23:13:13 +00:00
2021-09-17 18:27:53 +00:00
log . info ( ` archiveSession: session for ${ id } ` ) ;
2021-05-17 18:03:42 +00:00
const entry = this . pendingSessions . get ( id ) || this . sessions . get ( id ) ;
2021-04-16 23:13:13 +00:00
2021-05-17 18:03:42 +00:00
await this . _archiveSession ( entry ) ;
} ) ;
2021-04-16 23:13:13 +00:00
}
2021-05-18 00:41:28 +00:00
async archiveSiblingSessions (
2021-09-10 02:38:11 +00:00
encodedAddress : Address ,
2021-05-19 21:25:56 +00:00
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-18 00:41:28 +00:00
) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( zone , 'archiveSiblingSessions' , async ( ) = > {
if ( ! this . sessions ) {
throw new Error (
'archiveSiblingSessions: this.sessions not yet cached!'
2021-05-18 00:41:28 +00:00
) ;
2021-05-19 21:25:56 +00:00
}
2021-02-26 23:42:45 +00:00
2021-09-17 18:27:53 +00:00
log . info (
2021-05-19 21:25:56 +00:00
'archiveSiblingSessions: archiving sibling sessions for' ,
2021-09-10 02:38:11 +00:00
encodedAddress . toString ( )
2021-05-19 21:25:56 +00:00
) ;
2021-05-17 18:03:42 +00:00
2023-08-10 16:43:33 +00:00
const { serviceId , deviceId } = encodedAddress ;
2021-02-26 23:42:45 +00:00
2021-05-19 21:25:56 +00:00
const allEntries = this . _getAllSessions ( ) ;
const entries = allEntries . filter (
entry = >
2023-08-16 20:54:39 +00:00
entry . fromDB . serviceId === serviceId &&
entry . fromDB . deviceId !== deviceId
2021-05-19 21:25:56 +00:00
) ;
await Promise . all (
entries . map ( async entry = > {
2021-05-21 16:03:01 +00:00
await this . _archiveSession ( entry , zone ) ;
2021-05-19 21:25:56 +00:00
} )
) ;
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
async archiveAllSessions ( serviceId : ServiceIdString ) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'archiveAllSessions' , async ( ) = > {
2021-05-17 18:03:42 +00:00
if ( ! this . sessions ) {
throw new Error ( 'archiveAllSessions: this.sessions not yet cached!' ) ;
}
2021-02-26 23:42:45 +00:00
2023-08-10 16:43:33 +00:00
log . info ( 'archiveAllSessions: archiving all sessions for' , serviceId ) ;
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
const allEntries = this . _getAllSessions ( ) ;
const entries = allEntries . filter (
2023-08-16 20:54:39 +00:00
entry = > entry . fromDB . serviceId === serviceId
2021-05-17 18:03:42 +00:00
) ;
await Promise . all (
entries . map ( async entry = > {
await this . _archiveSession ( entry ) ;
} )
) ;
} ) ;
2021-02-26 23:42:45 +00:00
}
async clearSessionStore ( ) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'clearSessionStore' , async ( ) = > {
2021-05-17 18:03:42 +00:00
if ( this . sessions ) {
this . sessions . clear ( ) ;
}
this . pendingSessions . clear ( ) ;
2024-07-22 18:16:33 +00:00
const changes = await DataWriter . removeAllSessions ( ) ;
2023-10-19 21:52:35 +00:00
log . info ( ` clearSessionStore: Removed ${ changes } sessions ` ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
async lightSessionReset ( qualifiedAddress : QualifiedAddress ) : Promise < void > {
const id = qualifiedAddress . toString ( ) ;
2021-05-18 00:41:28 +00:00
const sessionResets = window . storage . get (
'sessionResets' ,
2023-03-28 18:26:46 +00:00
{ } as SessionResetsType
2021-06-15 00:09:37 +00:00
) ;
2021-05-18 00:41:28 +00:00
const lastReset = sessionResets [ id ] ;
const ONE_HOUR = 60 * 60 * 1000 ;
if ( lastReset && isMoreRecentThan ( lastReset , ONE_HOUR ) ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-05-18 00:41:28 +00:00
` lightSessionReset/ ${ id } : Skipping session reset, last reset at ${ lastReset } `
) ;
return ;
}
sessionResets [ id ] = Date . now ( ) ;
2022-12-21 18:41:48 +00:00
await window . storage . put ( 'sessionResets' , sessionResets ) ;
2021-05-18 00:41:28 +00:00
try {
2023-08-10 16:43:33 +00:00
const { serviceId } = qualifiedAddress ;
2021-09-10 02:38:11 +00:00
2021-05-18 00:41:28 +00:00
// First, fetch this conversation
2022-08-09 21:39:00 +00:00
const conversation = window . ConversationController . lookupOrCreate ( {
2023-08-16 20:54:39 +00:00
serviceId ,
2022-12-03 01:05:27 +00:00
reason : 'SignalProtocolStore.lightSessionReset' ,
2021-05-18 00:41:28 +00:00
} ) ;
2022-09-15 19:17:15 +00:00
assertDev ( conversation , ` lightSessionReset/ ${ id } : missing conversation ` ) ;
2021-05-18 00:41:28 +00:00
2021-09-17 18:27:53 +00:00
log . warn ( ` lightSessionReset/ ${ id } : Resetting session ` ) ;
2021-05-18 00:41:28 +00:00
// Archive open session with this device
2021-09-10 02:38:11 +00:00
await this . archiveSession ( qualifiedAddress ) ;
2021-05-18 00:41:28 +00:00
2022-01-14 21:34:52 +00:00
// Enqueue a null message with newly-created session
2023-03-14 20:25:05 +00:00
await conversationJobQueue . add ( {
type : 'NullMessage' ,
conversationId : conversation.id ,
idForTracking : id ,
} ) ;
2021-05-18 00:41:28 +00:00
} catch ( error ) {
2023-03-14 20:25:05 +00:00
// If we failed to queue the session reset, then we'll allow another attempt sooner
2021-05-18 00:41:28 +00:00
// than one hour from now.
delete sessionResets [ id ] ;
2022-12-21 18:41:48 +00:00
await window . storage . put ( 'sessionResets' , sessionResets ) ;
2021-05-18 00:41:28 +00:00
2022-01-14 21:34:52 +00:00
log . error (
` lightSessionReset/ ${ id } : Encountered error ` ,
Errors . toLogFormat ( error )
) ;
2021-05-18 00:41:28 +00:00
}
}
2021-02-26 23:42:45 +00:00
// Identity Keys
2023-08-10 16:43:33 +00:00
getIdentityRecord ( serviceId : ServiceIdString ) : IdentityKeyType | undefined {
2021-02-26 23:42:45 +00:00
if ( ! this . identityKeys ) {
throw new Error ( 'getIdentityRecord: this.identityKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
try {
2023-08-10 16:43:33 +00:00
const entry = this . identityKeys . get ( serviceId ) ;
2021-04-16 23:13:13 +00:00
if ( ! entry ) {
return undefined ;
2021-02-26 23:42:45 +00:00
}
2021-04-16 23:13:13 +00:00
return entry . fromDB ;
2021-02-26 23:42:45 +00:00
} catch ( e ) {
2021-09-17 18:27:53 +00:00
log . error (
2023-08-10 16:43:33 +00:00
` getIdentityRecord: Failed to get identity record for serviceId ${ serviceId } `
2021-02-26 23:42:45 +00:00
) ;
2021-04-16 23:13:13 +00:00
return undefined ;
2021-02-26 23:42:45 +00:00
}
}
2021-09-10 02:38:11 +00:00
async getOrMigrateIdentityRecord (
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString
2021-09-10 02:38:11 +00:00
) : Promise < IdentityKeyType | undefined > {
if ( ! this . identityKeys ) {
throw new Error (
'getOrMigrateIdentityRecord: this.identityKeys not yet cached!'
) ;
}
2023-08-10 16:43:33 +00:00
const result = this . getIdentityRecord ( serviceId ) ;
2021-09-10 02:38:11 +00:00
if ( result ) {
return result ;
}
2023-08-10 16:43:33 +00:00
const newId = serviceId ;
2021-09-10 02:38:11 +00:00
const conversation = window . ConversationController . get ( newId ) ;
if ( ! conversation ) {
return undefined ;
}
2021-10-26 22:59:08 +00:00
const conversationId = conversation . id ;
2021-09-10 02:38:11 +00:00
const record = this . identityKeys . get ( ` conversation: ${ conversationId } ` ) ;
if ( ! record ) {
return undefined ;
}
const newRecord = {
. . . record . fromDB ,
id : newId ,
} ;
2021-09-17 18:27:53 +00:00
log . info (
2021-09-10 02:38:11 +00:00
` SignalProtocolStore: migrating identity key from ${ record . fromDB . id } ` +
` to ${ newRecord . id } `
) ;
await this . _saveIdentityKey ( newRecord ) ;
this . identityKeys . delete ( record . fromDB . id ) ;
2024-07-22 18:16:33 +00:00
const changes = await DataWriter . removeIdentityKeyById ( record . fromDB . id ) ;
2023-10-19 21:52:35 +00:00
log . info (
` getOrMigrateIdentityRecord: Removed ${ changes } old identity keys for ${ record . fromDB . id } `
) ;
2021-09-10 02:38:11 +00:00
return newRecord ;
}
2022-11-07 23:21:12 +00:00
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L128
2021-02-26 23:42:45 +00:00
async isTrustedIdentity (
2021-09-10 02:38:11 +00:00
encodedAddress : Address ,
2021-09-24 00:49:05 +00:00
publicKey : Uint8Array ,
2021-02-26 23:42:45 +00:00
direction : number
) : Promise < boolean > {
if ( ! this . identityKeys ) {
2021-09-10 02:38:11 +00:00
throw new Error ( 'isTrustedIdentity: this.identityKeys not yet cached!' ) ;
2021-02-26 23:42:45 +00:00
}
2022-09-14 21:40:44 +00:00
if ( encodedAddress == null ) {
2021-04-16 23:13:13 +00:00
throw new Error ( 'isTrustedIdentity: encodedAddress was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
const isOurIdentifier = window . textsecure . storage . user . isOurServiceId (
encodedAddress . serviceId
2022-11-07 23:21:12 +00:00
) ;
2021-02-26 23:42:45 +00:00
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord (
2023-08-10 16:43:33 +00:00
encodedAddress . serviceId
2021-09-10 02:38:11 +00:00
) ;
2021-02-26 23:42:45 +00:00
if ( isOurIdentifier ) {
if ( identityRecord && identityRecord . publicKey ) {
return constantTimeEqual ( identityRecord . publicKey , publicKey ) ;
}
2021-09-17 18:27:53 +00:00
log . warn (
2021-02-26 23:42:45 +00:00
'isTrustedIdentity: No local record for our own identifier. Returning true.'
) ;
return true ;
}
switch ( direction ) {
2021-04-16 23:13:13 +00:00
case Direction . Sending :
2022-12-05 22:46:54 +00:00
return this . isTrustedForSending (
2023-08-10 16:43:33 +00:00
encodedAddress . serviceId ,
2022-12-05 22:46:54 +00:00
publicKey ,
identityRecord
) ;
2021-04-16 23:13:13 +00:00
case Direction . Receiving :
2021-02-26 23:42:45 +00:00
return true ;
default :
2021-04-16 23:13:13 +00:00
throw new Error ( ` isTrustedIdentity: Unknown direction: ${ direction } ` ) ;
2021-02-26 23:42:45 +00:00
}
}
2022-11-07 23:21:12 +00:00
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L233
2021-02-26 23:42:45 +00:00
isTrustedForSending (
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ,
2021-09-24 00:49:05 +00:00
publicKey : Uint8Array ,
2021-02-26 23:42:45 +00:00
identityRecord? : IdentityKeyType
) : boolean {
if ( ! identityRecord ) {
2022-12-05 22:46:54 +00:00
// To track key changes across session switches, we save an old identity key on the
// conversation.
2023-08-10 16:43:33 +00:00
const conversation = window . ConversationController . get ( serviceId ) ;
2022-12-05 22:46:54 +00:00
const previousIdentityKeyBase64 = conversation ? . get (
'previousIdentityKey'
) ;
if ( conversation && previousIdentityKeyBase64 ) {
const previousIdentityKey = Bytes . fromBase64 ( previousIdentityKeyBase64 ) ;
if ( ! constantTimeEqual ( previousIdentityKey , publicKey ) ) {
log . info (
'isTrustedForSending: previousIdentityKey does not match, returning false'
) ;
return false ;
}
}
log . info (
'isTrustedForSending: No previous record or previousIdentityKey, returning true'
) ;
2021-02-26 23:42:45 +00:00
return true ;
}
const existing = identityRecord . publicKey ;
if ( ! existing ) {
2021-09-17 18:27:53 +00:00
log . info ( 'isTrustedForSending: Nothing here, returning true...' ) ;
2021-02-26 23:42:45 +00:00
return true ;
}
if ( ! constantTimeEqual ( existing , publicKey ) ) {
2021-09-17 18:27:53 +00:00
log . info ( "isTrustedForSending: Identity keys don't match..." ) ;
2021-02-26 23:42:45 +00:00
return false ;
}
if ( identityRecord . verified === VerifiedStatus . UNVERIFIED ) {
2022-12-05 22:46:54 +00:00
log . error ( 'isTrustedForSending: Needs unverified approval!' ) ;
2021-02-26 23:42:45 +00:00
return false ;
}
if ( this . isNonBlockingApprovalRequired ( identityRecord ) ) {
2021-09-17 18:27:53 +00:00
log . error ( 'isTrustedForSending: Needs non-blocking approval!' ) ;
2021-02-26 23:42:45 +00:00
return false ;
}
return true ;
}
2023-08-10 16:43:33 +00:00
async loadIdentityKey (
serviceId : ServiceIdString
) : Promise < Uint8Array | undefined > {
if ( serviceId == null ) {
throw new Error ( 'loadIdentityKey: serviceId was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( serviceId ) ;
2021-02-26 23:42:45 +00:00
if ( identityRecord ) {
return identityRecord . publicKey ;
}
return undefined ;
}
2023-08-10 16:43:33 +00:00
async getFingerprint (
serviceId : ServiceIdString
) : Promise < string | undefined > {
if ( serviceId == null ) {
throw new Error ( 'loadIdentityKey: serviceId was undefined/null' ) ;
2022-08-19 18:05:31 +00:00
}
2023-08-10 16:43:33 +00:00
const pubKey = await this . loadIdentityKey ( serviceId ) ;
2022-08-19 18:05:31 +00:00
if ( ! pubKey ) {
return ;
}
const hash = sha256 ( pubKey ) ;
const fingerprint = hash . slice ( 0 , 4 ) ;
return Bytes . toBase64 ( fingerprint ) ;
}
2021-02-26 23:42:45 +00:00
private async _saveIdentityKey ( data : IdentityKeyType ) : Promise < void > {
if ( ! this . identityKeys ) {
throw new Error ( '_saveIdentityKey: this.identityKeys not yet cached!' ) ;
}
const { id } = data ;
2024-07-22 18:16:33 +00:00
await DataWriter . createOrUpdateIdentityKey ( data ) ;
2021-05-14 01:18:43 +00:00
this . identityKeys . set ( id , {
2021-04-16 23:13:13 +00:00
hydrated : false ,
fromDB : data ,
2021-05-14 01:18:43 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2022-11-07 23:21:12 +00:00
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L69
2021-02-26 23:42:45 +00:00
async saveIdentity (
2021-09-10 02:38:11 +00:00
encodedAddress : Address ,
2021-09-24 00:49:05 +00:00
publicKey : Uint8Array ,
2021-05-18 00:41:28 +00:00
nonblockingApproval = false ,
2024-03-12 01:08:38 +00:00
{ zone = GLOBAL_ZONE , noOverwrite = false } : SaveIdentityOptions = { }
2021-02-26 23:42:45 +00:00
) : Promise < boolean > {
if ( ! this . identityKeys ) {
throw new Error ( 'saveIdentity: this.identityKeys not yet cached!' ) ;
}
2022-09-14 21:40:44 +00:00
if ( encodedAddress == null ) {
2021-04-16 23:13:13 +00:00
throw new Error ( 'saveIdentity: encodedAddress was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-24 00:49:05 +00:00
if ( ! ( publicKey instanceof Uint8Array ) ) {
2021-02-26 23:42:45 +00:00
// eslint-disable-next-line no-param-reassign
2021-09-24 00:49:05 +00:00
publicKey = Bytes . fromBinary ( publicKey ) ;
2021-02-26 23:42:45 +00:00
}
if ( typeof nonblockingApproval !== 'boolean' ) {
// eslint-disable-next-line no-param-reassign
nonblockingApproval = false ;
}
2023-10-05 00:39:09 +00:00
return this . _runOnIdentityQueue (
encodedAddress . serviceId ,
zone ,
'saveIdentity' ,
async ( ) = > {
const identityRecord = await this . getOrMigrateIdentityRecord (
encodedAddress . serviceId
) ;
2021-09-10 02:38:11 +00:00
2023-10-05 00:39:09 +00:00
const id = encodedAddress . serviceId ;
const logId = ` saveIdentity( ${ id } ) ` ;
if ( ! identityRecord || ! identityRecord . publicKey ) {
// Lookup failed, or the current key was removed, so save this one.
log . info ( ` ${ logId } : Saving new identity... ` ) ;
await this . _saveIdentityKey ( {
id ,
publicKey ,
firstUse : true ,
timestamp : Date.now ( ) ,
verified : VerifiedStatus.DEFAULT ,
nonblockingApproval ,
} ) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
this . checkPreviousKey (
encodedAddress . serviceId ,
publicKey ,
'saveIdentity'
) ;
2022-12-05 22:46:54 +00:00
2023-10-05 00:39:09 +00:00
return false ;
}
2024-03-12 01:08:38 +00:00
if ( noOverwrite ) {
return false ;
}
2023-10-05 00:39:09 +00:00
const identityKeyChanged = ! constantTimeEqual (
identityRecord . publicKey ,
publicKey
2023-08-10 16:43:33 +00:00
) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
if ( identityKeyChanged ) {
const isOurIdentifier = window . textsecure . storage . user . isOurServiceId (
encodedAddress . serviceId
) ;
2022-11-07 23:21:12 +00:00
2023-10-05 00:39:09 +00:00
if ( isOurIdentifier && identityKeyChanged ) {
log . warn ( ` ${ logId } : ignoring identity for ourselves ` ) ;
return false ;
}
2022-11-07 23:21:12 +00:00
2023-10-05 00:39:09 +00:00
log . info ( ` ${ logId } : Replacing existing identity... ` ) ;
const previousStatus = identityRecord . verified ;
let verifiedStatus ;
if (
previousStatus === VerifiedStatus . VERIFIED ||
previousStatus === VerifiedStatus . UNVERIFIED
) {
verifiedStatus = VerifiedStatus . UNVERIFIED ;
} else {
verifiedStatus = VerifiedStatus . DEFAULT ;
}
2022-11-07 23:21:12 +00:00
2023-10-05 00:39:09 +00:00
await this . _saveIdentityKey ( {
id ,
publicKey ,
firstUse : false ,
timestamp : Date.now ( ) ,
verified : verifiedStatus ,
nonblockingApproval ,
} ) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
// See `addKeyChange` in `ts/models/conversations.ts` for sender key info
// update caused by this.
try {
this . emit (
'keychange' ,
encodedAddress . serviceId ,
'saveIdentity - change'
) ;
} catch ( error ) {
log . error (
` ${ logId } : error triggering keychange: ` ,
Errors . toLogFormat ( error )
) ;
}
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
// Pass the zone to facilitate transactional session use in
// MessageReceiver.ts
await this . archiveSiblingSessions ( encodedAddress , {
zone ,
} ) ;
2021-05-18 00:41:28 +00:00
2023-10-05 00:39:09 +00:00
return true ;
2023-04-19 16:13:48 +00:00
}
2023-10-05 00:39:09 +00:00
if ( this . isNonBlockingApprovalRequired ( identityRecord ) ) {
log . info ( ` ${ logId } : Setting approval status... ` ) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
identityRecord . nonblockingApproval = nonblockingApproval ;
await this . _saveIdentityKey ( identityRecord ) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
return false ;
}
2021-02-26 23:42:45 +00:00
2023-04-19 16:13:48 +00:00
return false ;
}
2023-10-05 00:39:09 +00:00
) ;
2021-02-26 23:42:45 +00:00
}
2022-11-07 23:21:12 +00:00
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L257
private isNonBlockingApprovalRequired (
identityRecord : IdentityKeyType
) : boolean {
2021-02-26 23:42:45 +00:00
return (
! identityRecord . firstUse &&
2021-03-22 21:08:52 +00:00
isMoreRecentThan ( identityRecord . timestamp , TIMESTAMP_THRESHOLD ) &&
2021-02-26 23:42:45 +00:00
! identityRecord . nonblockingApproval
) ;
}
async saveIdentityWithAttributes (
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ,
2021-04-16 23:13:13 +00:00
attributes : Partial < IdentityKeyType >
2023-04-19 16:13:48 +00:00
) : Promise < void > {
2023-10-05 00:39:09 +00:00
return this . _runOnIdentityQueue (
serviceId ,
GLOBAL_ZONE ,
'saveIdentityWithAttributes' ,
async ( ) = > {
return this . saveIdentityWithAttributesOnQueue ( serviceId , attributes ) ;
}
) ;
2023-04-19 16:13:48 +00:00
}
private async saveIdentityWithAttributesOnQueue (
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ,
2023-04-19 16:13:48 +00:00
attributes : Partial < IdentityKeyType >
2021-02-26 23:42:45 +00:00
) : Promise < void > {
2023-08-10 16:43:33 +00:00
if ( serviceId == null ) {
throw new Error (
'saveIdentityWithAttributes: serviceId was undefined/null'
) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( serviceId ) ;
const id = serviceId ;
2021-09-10 02:38:11 +00:00
2022-04-20 19:35:53 +00:00
// When saving a PNI identity - don't create a separate conversation
2023-08-10 16:43:33 +00:00
const serviceIdKind =
window . textsecure . storage . user . getOurServiceIdKind ( serviceId ) ;
if ( serviceIdKind !== ServiceIdKind . PNI ) {
2022-04-20 19:35:53 +00:00
window . ConversationController . getOrCreate ( id , 'private' ) ;
}
2021-02-26 23:42:45 +00:00
2021-04-16 23:13:13 +00:00
const updates : Partial < IdentityKeyType > = {
2021-02-26 23:42:45 +00:00
. . . identityRecord ,
. . . attributes ,
id ,
} ;
2021-04-16 23:13:13 +00:00
if ( validateIdentityKey ( updates ) ) {
2021-02-26 23:42:45 +00:00
await this . _saveIdentityKey ( updates ) ;
}
}
2023-08-10 16:43:33 +00:00
async setApproval (
serviceId : ServiceIdString ,
nonblockingApproval : boolean
) : Promise < void > {
if ( serviceId == null ) {
throw new Error ( 'setApproval: serviceId was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
if ( typeof nonblockingApproval !== 'boolean' ) {
2021-04-16 23:13:13 +00:00
throw new Error ( 'setApproval: Invalid approval status' ) ;
2021-02-26 23:42:45 +00:00
}
2023-10-05 00:39:09 +00:00
return this . _runOnIdentityQueue (
serviceId ,
GLOBAL_ZONE ,
'setApproval' ,
async ( ) = > {
const identityRecord = await this . getOrMigrateIdentityRecord ( serviceId ) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
if ( ! identityRecord ) {
throw new Error ( ` setApproval: No identity record for ${ serviceId } ` ) ;
}
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
identityRecord . nonblockingApproval = nonblockingApproval ;
await this . _saveIdentityKey ( identityRecord ) ;
}
) ;
2021-02-26 23:42:45 +00:00
}
2022-11-07 23:21:12 +00:00
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L215
// and https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.java#L544
2021-02-26 23:42:45 +00:00
async setVerified (
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ,
2021-02-26 23:42:45 +00:00
verifiedStatus : number ,
2022-11-07 23:21:12 +00:00
extra : SetVerifiedExtra = { }
2021-02-26 23:42:45 +00:00
) : Promise < void > {
2023-08-10 16:43:33 +00:00
if ( serviceId == null ) {
throw new Error ( 'setVerified: serviceId was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
if ( ! validateVerifiedStatus ( verifiedStatus ) ) {
2021-04-16 23:13:13 +00:00
throw new Error ( 'setVerified: Invalid verified status' ) ;
2021-02-26 23:42:45 +00:00
}
2023-10-05 00:39:09 +00:00
return this . _runOnIdentityQueue (
serviceId ,
GLOBAL_ZONE ,
'setVerified' ,
async ( ) = > {
const identityRecord = await this . getOrMigrateIdentityRecord ( serviceId ) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
if ( ! identityRecord ) {
throw new Error ( ` setVerified: No identity record for ${ serviceId } ` ) ;
}
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
if ( validateIdentityKey ( identityRecord ) ) {
await this . _saveIdentityKey ( {
. . . identityRecord ,
. . . extra ,
verified : verifiedStatus ,
} ) ;
}
2023-04-19 16:13:48 +00:00
}
2023-10-05 00:39:09 +00:00
) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
async getVerified ( serviceId : ServiceIdString ) : Promise < number > {
if ( serviceId == null ) {
throw new Error ( 'getVerified: serviceId was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( serviceId ) ;
2021-02-26 23:42:45 +00:00
if ( ! identityRecord ) {
2023-08-10 16:43:33 +00:00
throw new Error ( ` getVerified: No identity record for ${ serviceId } ` ) ;
2021-02-26 23:42:45 +00:00
}
const verifiedStatus = identityRecord . verified ;
if ( validateVerifiedStatus ( verifiedStatus ) ) {
return verifiedStatus ;
}
return VerifiedStatus . DEFAULT ;
}
2022-12-05 22:46:54 +00:00
// To track key changes across session switches, we save an old identity key on the
// conversation. Whenever we get a new identity key for that contact, we need to
// check it against that saved key - no need to pop a key change warning if it is
// the same!
2023-08-10 16:43:33 +00:00
checkPreviousKey (
serviceId : ServiceIdString ,
publicKey : Uint8Array ,
context : string
) : void {
const conversation = window . ConversationController . get ( serviceId ) ;
2022-12-05 22:46:54 +00:00
const previousIdentityKeyBase64 = conversation ? . get ( 'previousIdentityKey' ) ;
if ( conversation && previousIdentityKeyBase64 ) {
const previousIdentityKey = Bytes . fromBase64 ( previousIdentityKeyBase64 ) ;
try {
if ( ! constantTimeEqual ( previousIdentityKey , publicKey ) ) {
this . emit (
'keychange' ,
2023-08-10 16:43:33 +00:00
serviceId ,
2022-12-05 22:46:54 +00:00
` ${ context } - previousIdentityKey check `
) ;
}
// We only want to clear previousIdentityKey on a match, or on successfully emit.
conversation . set ( { previousIdentityKey : undefined } ) ;
2024-07-22 18:16:33 +00:00
drop ( DataWriter . updateConversation ( conversation . attributes ) ) ;
2022-12-05 22:46:54 +00:00
} catch ( error ) {
log . error (
'saveIdentity: error triggering keychange:' ,
error && error . stack ? error.stack : error
) ;
}
}
}
2022-11-07 23:21:12 +00:00
// See https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java#L184
async updateIdentityAfterSync (
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ,
2021-02-26 23:42:45 +00:00
verifiedStatus : number ,
2022-11-07 23:21:12 +00:00
publicKey : Uint8Array
2021-02-26 23:42:45 +00:00
) : Promise < boolean > {
2022-11-07 23:21:12 +00:00
strictAssert (
validateVerifiedStatus ( verifiedStatus ) ,
` Invalid verified status: ${ verifiedStatus } `
) ;
2021-02-26 23:42:45 +00:00
2023-10-05 00:39:09 +00:00
return this . _runOnIdentityQueue (
serviceId ,
GLOBAL_ZONE ,
'updateIdentityAfterSync' ,
async ( ) = > {
const identityRecord = await this . getOrMigrateIdentityRecord ( serviceId ) ;
const hadEntry = identityRecord !== undefined ;
const keyMatches = Boolean (
identityRecord ? . publicKey &&
constantTimeEqual ( publicKey , identityRecord . publicKey )
) ;
const statusMatches =
keyMatches && verifiedStatus === identityRecord ? . verified ;
if ( ! keyMatches || ! statusMatches ) {
await this . saveIdentityWithAttributesOnQueue ( serviceId , {
publicKey ,
verified : verifiedStatus ,
firstUse : ! hadEntry ,
timestamp : Date.now ( ) ,
nonblockingApproval : true ,
} ) ;
}
if ( ! hadEntry ) {
this . checkPreviousKey (
serviceId ,
publicKey ,
'updateIdentityAfterSync'
2023-04-19 16:13:48 +00:00
) ;
2023-10-05 00:39:09 +00:00
} else if ( hadEntry && ! keyMatches ) {
try {
this . emit (
'keychange' ,
serviceId ,
'updateIdentityAfterSync - change'
) ;
} catch ( error ) {
log . error (
'updateIdentityAfterSync: error triggering keychange:' ,
Errors . toLogFormat ( error )
) ;
}
2023-04-19 16:13:48 +00:00
}
2022-03-21 22:06:34 +00:00
2023-10-05 00:39:09 +00:00
// See: https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt#L921-L936
if (
verifiedStatus === VerifiedStatus . VERIFIED &&
( ! hadEntry || identityRecord ? . verified !== VerifiedStatus . VERIFIED )
) {
// Needs a notification.
return true ;
}
if (
verifiedStatus !== VerifiedStatus . VERIFIED &&
hadEntry &&
identityRecord ? . verified === VerifiedStatus . VERIFIED
) {
// Needs a notification.
return true ;
}
return false ;
2023-04-19 16:13:48 +00:00
}
2023-10-05 00:39:09 +00:00
) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
isUntrusted (
serviceId : ServiceIdString ,
timestampThreshold = TIMESTAMP_THRESHOLD
) : boolean {
if ( serviceId == null ) {
throw new Error ( 'isUntrusted: serviceId was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
const identityRecord = this . getIdentityRecord ( serviceId ) ;
2021-02-26 23:42:45 +00:00
if ( ! identityRecord ) {
2023-08-10 16:43:33 +00:00
throw new Error ( ` isUntrusted: No identity record for ${ serviceId } ` ) ;
2021-02-26 23:42:45 +00:00
}
if (
2022-08-19 18:05:31 +00:00
isMoreRecentThan ( identityRecord . timestamp , timestampThreshold ) &&
2021-02-26 23:42:45 +00:00
! identityRecord . nonblockingApproval &&
! identityRecord . firstUse
) {
return true ;
}
return false ;
}
2023-08-10 16:43:33 +00:00
async removeIdentityKey ( serviceId : ServiceIdString ) : Promise < void > {
2021-02-26 23:42:45 +00:00
if ( ! this . identityKeys ) {
throw new Error ( 'removeIdentityKey: this.identityKeys not yet cached!' ) ;
}
2023-08-10 16:43:33 +00:00
const id = serviceId ;
2021-09-10 02:38:11 +00:00
this . identityKeys . delete ( id ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . removeIdentityKeyById ( serviceId ) ;
2023-08-10 16:43:33 +00:00
await this . removeSessionsByServiceId ( serviceId ) ;
2021-02-26 23:42:45 +00:00
}
// Not yet processed messages - for resiliency
getUnprocessedCount ( ) : Promise < number > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'getUnprocessedCount' , async ( ) = > {
2024-07-22 18:16:33 +00:00
return DataReader . getUnprocessedCount ( ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-02-02 19:39:07 +00:00
getAllUnprocessedIds ( ) : Promise < Array < string > > {
return this . withZone ( GLOBAL_ZONE , 'getAllUnprocessedIds' , ( ) = > {
2024-07-22 18:16:33 +00:00
return DataWriter . getAllUnprocessedIds ( ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-02-02 19:39:07 +00:00
getUnprocessedByIdsAndIncrementAttempts (
ids : ReadonlyArray < string >
) : Promise < Array < UnprocessedType > > {
return this . withZone (
GLOBAL_ZONE ,
'getAllUnprocessedByIdsAndIncrementAttempts' ,
async ( ) = > {
2024-07-22 18:16:33 +00:00
return DataWriter . getUnprocessedByIdsAndIncrementAttempts ( ids ) ;
2023-02-02 19:39:07 +00:00
}
) ;
}
2021-02-26 23:42:45 +00:00
getUnprocessedById ( id : string ) : Promise < UnprocessedType | undefined > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'getUnprocessedById' , async ( ) = > {
2024-07-22 18:16:33 +00:00
return DataReader . getUnprocessedById ( id ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-05-17 18:03:42 +00:00
addUnprocessed (
data : UnprocessedType ,
2021-05-19 21:25:56 +00:00
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-17 18:03:42 +00:00
) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( zone , 'addUnprocessed' , async ( ) = > {
this . pendingUnprocessed . set ( data . id , data ) ;
// Current zone doesn't support pending unprocessed - commit immediately
if ( ! zone . supportsPendingUnprocessed ( ) ) {
await this . commitZoneChanges ( 'addUnprocessed' ) ;
}
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-05-17 18:03:42 +00:00
addMultipleUnprocessed (
array : Array < UnprocessedType > ,
2021-05-19 21:25:56 +00:00
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-17 18:03:42 +00:00
) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( zone , 'addMultipleUnprocessed' , async ( ) = > {
for ( const elem of array ) {
this . pendingUnprocessed . set ( elem . id , elem ) ;
}
// Current zone doesn't support pending unprocessed - commit immediately
if ( ! zone . supportsPendingUnprocessed ( ) ) {
await this . commitZoneChanges ( 'addMultipleUnprocessed' ) ;
}
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-04-16 23:13:13 +00:00
updateUnprocessedWithData (
id : string ,
data : UnprocessedUpdateType
) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'updateUnprocessedWithData' , async ( ) = > {
2024-07-22 18:16:33 +00:00
await DataWriter . updateUnprocessedWithData ( id , data ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2021-04-05 22:18:19 +00:00
updateUnprocessedsWithData (
2021-04-16 23:13:13 +00:00
items : Array < { id : string ; data : UnprocessedUpdateType } >
2021-04-05 22:18:19 +00:00
) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone (
GLOBAL_ZONE ,
'updateUnprocessedsWithData' ,
async ( ) = > {
2024-07-22 18:16:33 +00:00
await DataWriter . updateUnprocessedsWithData ( items ) ;
2021-05-19 21:25:56 +00:00
}
) ;
2021-02-26 23:42:45 +00:00
}
removeUnprocessed ( idOrArray : string | Array < string > ) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'removeUnprocessed' , async ( ) = > {
2024-07-22 18:16:33 +00:00
await DataWriter . removeUnprocessed ( idOrArray ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-02-02 19:39:07 +00:00
/** only for testing */
2021-02-26 23:42:45 +00:00
removeAllUnprocessed ( ) : Promise < void > {
2023-02-02 19:39:07 +00:00
log . info ( 'removeAllUnprocessed' ) ;
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'removeAllUnprocessed' , async ( ) = > {
2024-07-22 18:16:33 +00:00
await DataWriter . removeAllUnprocessed ( ) ;
2021-05-17 18:03:42 +00:00
} ) ;
2021-02-26 23:42:45 +00:00
}
2023-08-10 16:43:33 +00:00
async removeOurOldPni ( oldPni : PniString ) : Promise < void > {
2022-07-28 16:35:29 +00:00
const { storage } = window ;
log . info ( ` SignalProtocolStore.removeOurOldPni( ${ oldPni } ) ` ) ;
// Update caches
2023-08-10 16:43:33 +00:00
this . ourIdentityKeys . delete ( oldPni ) ;
this . ourRegistrationIds . delete ( oldPni ) ;
2022-07-28 16:35:29 +00:00
2023-08-10 16:43:33 +00:00
const preKeyPrefix = ` ${ oldPni } : ` ;
2022-07-28 16:35:29 +00:00
if ( this . preKeys ) {
for ( const key of this . preKeys . keys ( ) ) {
if ( key . startsWith ( preKeyPrefix ) ) {
this . preKeys . delete ( key ) ;
}
}
}
if ( this . signedPreKeys ) {
for ( const key of this . signedPreKeys . keys ( ) ) {
if ( key . startsWith ( preKeyPrefix ) ) {
this . signedPreKeys . delete ( key ) ;
}
}
}
2023-07-14 16:53:20 +00:00
if ( this . kyberPreKeys ) {
for ( const key of this . kyberPreKeys . keys ( ) ) {
if ( key . startsWith ( preKeyPrefix ) ) {
this . kyberPreKeys . delete ( key ) ;
}
}
}
2022-07-28 16:35:29 +00:00
// Update database
await Promise . all ( [
storage . put (
'identityKeyMap' ,
2023-08-10 16:43:33 +00:00
omit ( storage . get ( 'identityKeyMap' ) || { } , oldPni )
2022-07-28 16:35:29 +00:00
) ,
storage . put (
'registrationIdMap' ,
2023-08-10 16:43:33 +00:00
omit ( storage . get ( 'registrationIdMap' ) || { } , oldPni )
2022-07-28 16:35:29 +00:00
) ,
2024-07-22 18:16:33 +00:00
DataWriter . removePreKeysByServiceId ( oldPni ) ,
DataWriter . removeSignedPreKeysByServiceId ( oldPni ) ,
DataWriter . removeKyberPreKeysByServiceId ( oldPni ) ,
2022-07-28 16:35:29 +00:00
] ) ;
}
2022-08-02 01:31:24 +00:00
async updateOurPniKeyMaterial (
2023-08-10 16:43:33 +00:00
pni : PniString ,
2022-08-02 01:31:24 +00:00
{
identityKeyPair : identityBytes ,
2023-07-14 16:53:20 +00:00
lastResortKyberPreKey : lastResortKyberPreKeyBytes ,
2022-08-02 01:31:24 +00:00
signedPreKey : signedPreKeyBytes ,
registrationId ,
} : PniKeyMaterialType
) : Promise < void > {
2023-07-14 16:53:20 +00:00
const logId = ` SignalProtocolStore.updateOurPniKeyMaterial( ${ pni } ) ` ;
log . info ( ` ${ logId } : starting... ` ) ;
2022-08-02 01:31:24 +00:00
2022-07-28 16:35:29 +00:00
const identityKeyPair = IdentityKeyPair . deserialize (
Buffer . from ( identityBytes )
) ;
const signedPreKey = SignedPreKeyRecord . deserialize (
Buffer . from ( signedPreKeyBytes )
) ;
2023-07-14 16:53:20 +00:00
const lastResortKyberPreKey = lastResortKyberPreKeyBytes
? KyberPreKeyRecord . deserialize ( Buffer . from ( lastResortKyberPreKeyBytes ) )
: undefined ;
2022-08-02 01:31:24 +00:00
2022-07-28 16:35:29 +00:00
const { storage } = window ;
2022-08-02 01:31:24 +00:00
2022-07-28 16:35:29 +00:00
const pniPublicKey = identityKeyPair . publicKey . serialize ( ) ;
const pniPrivateKey = identityKeyPair . privateKey . serialize ( ) ;
2022-08-02 01:31:24 +00:00
2022-07-28 16:35:29 +00:00
// Update caches
2023-08-10 16:43:33 +00:00
this . ourIdentityKeys . set ( pni , {
2022-07-28 16:35:29 +00:00
pubKey : pniPublicKey ,
privKey : pniPrivateKey ,
} ) ;
2023-08-10 16:43:33 +00:00
this . ourRegistrationIds . set ( pni , registrationId ) ;
2022-08-02 01:31:24 +00:00
2022-07-28 16:35:29 +00:00
// Update database
2024-02-16 20:39:58 +00:00
await Promise . all < void > ( [
2022-07-28 16:35:29 +00:00
storage . put ( 'identityKeyMap' , {
. . . ( storage . get ( 'identityKeyMap' ) || { } ) ,
2023-08-10 16:43:33 +00:00
[ pni ] : {
2022-07-28 16:35:29 +00:00
pubKey : pniPublicKey ,
privKey : pniPrivateKey ,
} ,
} ) ,
storage . put ( 'registrationIdMap' , {
. . . ( storage . get ( 'registrationIdMap' ) || { } ) ,
2023-08-10 16:43:33 +00:00
[ pni ] : registrationId ,
2022-07-28 16:35:29 +00:00
} ) ,
2024-02-16 20:39:58 +00:00
( async ( ) = > {
2023-07-14 16:53:20 +00:00
const newId = signedPreKey . id ( ) + 1 ;
log . warn ( ` ${ logId } : Updating next signed pre key id to ${ newId } ` ) ;
2023-08-10 16:43:33 +00:00
await storage . put ( SIGNED_PRE_KEY_ID_KEY [ ServiceIdKind . PNI ] , newId ) ;
2024-02-16 20:39:58 +00:00
} ) ( ) ,
2022-07-28 16:35:29 +00:00
this . storeSignedPreKey (
pni ,
signedPreKey . id ( ) ,
{
privKey : signedPreKey.privateKey ( ) . serialize ( ) ,
pubKey : signedPreKey.publicKey ( ) . serialize ( ) ,
} ,
true ,
signedPreKey . timestamp ( )
) ,
2024-02-16 20:39:58 +00:00
( async ( ) = > {
2023-07-14 16:53:20 +00:00
if ( ! lastResortKyberPreKey ) {
return ;
}
const newId = lastResortKyberPreKey . id ( ) + 1 ;
log . warn ( ` ${ logId } : Updating next kyber pre key id to ${ newId } ` ) ;
2023-08-10 16:43:33 +00:00
await storage . put ( KYBER_KEY_ID_KEY [ ServiceIdKind . PNI ] , newId ) ;
2024-02-16 20:39:58 +00:00
} ) ( ) ,
2023-07-14 16:53:20 +00:00
lastResortKyberPreKeyBytes && lastResortKyberPreKey
? this . storeKyberPreKeys ( pni , [
{
createdAt : lastResortKyberPreKey.timestamp ( ) ,
data : lastResortKyberPreKeyBytes ,
isConfirmed : true ,
isLastResort : true ,
keyId : lastResortKyberPreKey.id ( ) ,
2023-08-16 20:54:39 +00:00
ourServiceId : pni ,
2023-07-14 16:53:20 +00:00
} ,
] )
: undefined ,
2022-07-28 16:35:29 +00:00
] ) ;
}
2021-02-26 23:42:45 +00:00
async removeAllData ( ) : Promise < void > {
2024-07-22 18:16:33 +00:00
await DataWriter . removeAll ( ) ;
2021-02-26 23:42:45 +00:00
await this . hydrateCaches ( ) ;
window . storage . reset ( ) ;
await window . storage . fetch ( ) ;
window . ConversationController . reset ( ) ;
await window . ConversationController . load ( ) ;
2022-10-20 19:16:37 +00:00
this . emit ( 'removeAllData' ) ;
2021-02-26 23:42:45 +00:00
}
2023-11-13 22:01:27 +00:00
async removeAllConfiguration ( ) : Promise < void > {
2024-05-31 14:15:43 +00:00
// Conversations. These properties are not present in redux.
window . getConversations ( ) . forEach ( conversation = > {
conversation . unset ( 'storageID' ) ;
conversation . unset ( 'needsStorageServiceSync' ) ;
conversation . unset ( 'storageUnknownFields' ) ;
conversation . unset ( 'senderKeyInfo' ) ;
} ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . removeAllConfiguration ( ) ;
2024-05-31 14:15:43 +00:00
2021-02-26 23:42:45 +00:00
await this . hydrateCaches ( ) ;
window . storage . reset ( ) ;
await window . storage . fetch ( ) ;
}
2021-05-17 18:03:42 +00:00
2022-08-15 21:53:33 +00:00
signAlternateIdentity ( ) : PniSignatureMessageType | undefined {
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
const ourPni = window . textsecure . storage . user . getPni ( ) ;
if ( ! ourPni ) {
2022-08-15 21:53:33 +00:00
log . error ( 'signAlternateIdentity: No local pni' ) ;
return undefined ;
}
2023-08-10 16:43:33 +00:00
if ( this . cachedPniSignatureMessage ? . pni === ourPni ) {
2022-08-15 21:53:33 +00:00
return this . cachedPniSignatureMessage ;
}
2023-08-10 16:43:33 +00:00
const aciKeyPair = this . getIdentityKeyPair ( ourAci ) ;
const pniKeyPair = this . getIdentityKeyPair ( ourPni ) ;
2022-08-15 21:53:33 +00:00
if ( ! aciKeyPair ) {
log . error ( 'signAlternateIdentity: No local ACI key pair' ) ;
return undefined ;
}
if ( ! pniKeyPair ) {
log . error ( 'signAlternateIdentity: No local PNI key pair' ) ;
return undefined ;
}
const pniIdentity = new IdentityKeyPair (
PublicKey . deserialize ( Buffer . from ( pniKeyPair . pubKey ) ) ,
PrivateKey . deserialize ( Buffer . from ( pniKeyPair . privKey ) )
) ;
const aciPubKey = PublicKey . deserialize ( Buffer . from ( aciKeyPair . pubKey ) ) ;
this . cachedPniSignatureMessage = {
2023-08-10 16:43:33 +00:00
pni : ourPni ,
2022-08-15 21:53:33 +00:00
signature : pniIdentity.signAlternateIdentity ( aciPubKey ) ,
} ;
return this . cachedPniSignatureMessage ;
}
async verifyAlternateIdentity ( {
aci ,
pni ,
signature ,
} : VerifyAlternateIdentityOptionsType ) : Promise < boolean > {
const logId = ` SignalProtocolStore.verifyAlternateIdentity( ${ aci } , ${ pni } ) ` ;
const aciPublicKeyBytes = await this . loadIdentityKey ( aci ) ;
if ( ! aciPublicKeyBytes ) {
log . warn ( ` ${ logId } : no ACI public key ` ) ;
return false ;
}
const pniPublicKeyBytes = await this . loadIdentityKey ( pni ) ;
if ( ! pniPublicKeyBytes ) {
log . warn ( ` ${ logId } : no PNI public key ` ) ;
return false ;
}
const aciPublicKey = PublicKey . deserialize ( Buffer . from ( aciPublicKeyBytes ) ) ;
const pniPublicKey = PublicKey . deserialize ( Buffer . from ( pniPublicKeyBytes ) ) ;
return pniPublicKey . verifyAlternateIdentity (
aciPublicKey ,
Buffer . from ( signature )
) ;
}
2021-05-17 18:03:42 +00:00
private _getAllSessions ( ) : Array < SessionCacheEntry > {
const union = new Map < string , SessionCacheEntry > ( ) ;
this . sessions ? . forEach ( ( value , key ) = > {
union . set ( key , value ) ;
} ) ;
this . pendingSessions . forEach ( ( value , key ) = > {
union . set ( key , value ) ;
} ) ;
return Array . from ( union . values ( ) ) ;
}
2023-07-14 16:53:20 +00:00
2023-08-10 16:43:33 +00:00
private emitLowKeys ( ourServiceId : ServiceIdString , source : string ) {
2023-07-14 16:53:20 +00:00
const logId = ` SignalProtocolStore.emitLowKeys/ ${ source } : ` ;
try {
log . info ( ` ${ logId } : Emitting event ` ) ;
2023-08-10 16:43:33 +00:00
this . emit ( 'lowKeys' , ourServiceId ) ;
2023-07-14 16:53:20 +00:00
} catch ( error ) {
log . error ( ` ${ logId } : Error thrown from emit ` , Errors . toLogFormat ( error ) ) ;
}
}
2022-10-20 19:16:37 +00:00
//
// EventEmitter types
//
public override on (
2023-07-14 16:53:20 +00:00
name : 'lowKeys' ,
2023-08-10 16:43:33 +00:00
handler : ( ourServiceId : ServiceIdString ) = > unknown
2022-10-20 19:16:37 +00:00
) : this ;
public override on (
name : 'keychange' ,
2023-08-10 16:43:33 +00:00
handler : ( theirServiceId : ServiceIdString , reason : string ) = > unknown
2022-10-20 19:16:37 +00:00
) : this ;
public override on ( name : 'removeAllData' , handler : ( ) = > unknown ) : this ;
public override on (
eventName : string | symbol ,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener : ( . . . args : Array < any > ) = > void
) : this {
return super . on ( eventName , listener ) ;
}
2023-08-10 16:43:33 +00:00
public override emit ( name : 'lowKeys' , ourServiceid : ServiceIdString ) : boolean ;
2022-10-20 19:16:37 +00:00
2022-12-05 22:46:54 +00:00
public override emit (
name : 'keychange' ,
2023-08-10 16:43:33 +00:00
theirServiceId : ServiceIdString ,
2022-12-05 22:46:54 +00:00
reason : string
) : boolean ;
2022-10-20 19:16:37 +00:00
public override emit ( name : 'removeAllData' ) : boolean ;
public override emit (
eventName : string | symbol ,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
. . . args : Array < any >
) : boolean {
return super . emit ( eventName , . . . args ) ;
}
2021-02-26 23:42:45 +00:00
}
2023-01-13 00:24:59 +00:00
export function getSignalProtocolStore ( ) : SignalProtocolStore {
return new SignalProtocolStore ( ) ;
}
2021-02-26 23:42:45 +00:00
window . SignalProtocolStore = SignalProtocolStore ;