2022-01-14 21:34:52 +00:00
// Copyright 2016-2022 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' ;
import { isNumber } from 'lodash' ;
2021-05-25 22:40:04 +00:00
import { z } from 'zod' ;
2021-04-16 23:13:13 +00:00
import {
2021-05-14 01:18:43 +00:00
Direction ,
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
2021-09-24 00:49:05 +00:00
import * as Bytes from './Bytes' ;
import { constantTimeEqual } from './Crypto' ;
2021-09-10 02:38:11 +00:00
import { assert , strictAssert } from './util/assert' ;
2021-02-26 23:42:45 +00:00
import { isNotNil } from './util/isNotNil' ;
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 ,
OuterSignedPrekeyType ,
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 ,
} from './textsecure/Types.d' ;
2021-08-30 21:39:57 +00:00
import type { RemoveAllConfiguration } from './types/RemoveAllConfiguration' ;
2021-10-26 19:15:33 +00:00
import type { UUIDStringType } from './types/UUID' ;
2022-04-20 19:35:53 +00:00
import { UUID , UUIDKind } from './types/UUID' ;
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 { singleProtoJobQueue } from './jobs/singleProtoJobQueue' ;
import * as Errors from './types/errors' ;
2022-06-13 21:39:35 +00:00
import MessageSender from './textsecure/SendMessage' ;
2021-02-26 23:42:45 +00:00
const TIMESTAMP_THRESHOLD = 5 * 1000 ; // 5 seconds
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 ;
}
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 =
| 'identityKeys'
| 'preKeys'
| 'senderKeys'
| 'sessions'
| 'signedPreKeys' ;
export type SessionTransactionOptions = {
2021-05-19 21:25:56 +00:00
readonly zone? : Zone ;
2021-05-17 18:03:42 +00:00
} ;
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
// We add a this parameter to avoid an 'implicit any' error on the next line
2021-11-11 22:43:05 +00:00
const EventsMixin = function EventsMixin ( this : unknown ) {
2022-05-31 21:42:18 +00:00
Object . assign ( this , window . Backbone . Events ) ;
2021-02-26 23:42:45 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2021-11-11 22:43:05 +00:00
} as any as typeof window . Backbone . EventsMixin ;
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 ;
} > ;
2021-02-26 23:42:45 +00:00
export class SignalProtocolStore extends EventsMixin {
// Enums used across the app
VerifiedStatus = VerifiedStatus ;
// Cached values
2021-09-10 02:38:11 +00:00
private ourIdentityKeys = new Map < UUIDStringType , KeyPairType > ( ) ;
2021-02-26 23:42:45 +00:00
2021-09-10 02:38:11 +00:00
private ourRegistrationIds = new Map < UUIDStringType , number > ( ) ;
2021-02-26 23:42:45 +00:00
2021-09-10 02:38:11 +00:00
identityKeys? : Map <
IdentityKeyIdType ,
CacheEntryType < IdentityKeyType , PublicKey >
> ;
2021-05-14 01:18:43 +00:00
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
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 ( ) ;
const map = await window . Signal . Data . getItemById ( 'identityKeyMap' ) ;
if ( ! map ) {
return ;
}
for ( const key of Object . keys ( map . value ) ) {
const { privKey , pubKey } = map . value [ key ] ;
this . ourIdentityKeys . set ( new UUID ( key ) . toString ( ) , {
2021-09-24 00:49:05 +00:00
privKey : Bytes.fromBase64 ( privKey ) ,
pubKey : Bytes.fromBase64 ( 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 ( ) ;
const map = await window . Signal . Data . getItemById ( 'registrationIdMap' ) ;
if ( ! map ) {
return ;
}
for ( const key of Object . keys ( map . value ) ) {
this . ourRegistrationIds . set ( new UUID ( key ) . toString ( ) , map . value [ key ] ) ;
}
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' ,
window . Signal . Data . getAllIdentityKeys ( )
) ,
2021-05-14 01:18:43 +00:00
_fillCaches < string , SessionType , SessionRecord > (
2021-02-26 23:42:45 +00:00
this ,
'sessions' ,
window . Signal . Data . getAllSessions ( )
) ,
2021-09-10 02:38:11 +00:00
_fillCaches < string , PreKeyType , PreKeyRecord > (
2021-02-26 23:42:45 +00:00
this ,
'preKeys' ,
window . Signal . Data . getAllPreKeys ( )
) ,
2021-05-14 01:18:43 +00:00
_fillCaches < string , SenderKeyType , SenderKeyRecord > (
this ,
'senderKeys' ,
window . Signal . Data . getAllSenderKeys ( )
) ,
2021-09-10 02:38:11 +00:00
_fillCaches < string , SignedPreKeyType , SignedPreKeyRecord > (
2021-02-26 23:42:45 +00:00
this ,
'signedPreKeys' ,
window . Signal . Data . getAllSignedPreKeys ( )
) ,
] ) ;
}
2021-09-10 02:38:11 +00:00
async getIdentityKeyPair ( ourUuid : UUID ) : Promise < KeyPairType | undefined > {
return this . ourIdentityKeys . get ( ourUuid . toString ( ) ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
async getLocalRegistrationId ( ourUuid : UUID ) : Promise < number | undefined > {
return this . ourRegistrationIds . get ( ourUuid . toString ( ) ) ;
2021-02-26 23:42:45 +00:00
}
// PreKeys
2021-09-10 02:38:11 +00:00
async loadPreKey (
ourUuid : UUID ,
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!' ) ;
}
2021-09-10 02:38:11 +00:00
const id : PreKeyIdType = ` ${ ourUuid . toString ( ) } : ${ keyId } ` ;
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
}
2021-09-10 02:38:11 +00:00
async storePreKey (
ourUuid : UUID ,
keyId : number ,
keyPair : KeyPairType
) : Promise < void > {
2021-02-26 23:42:45 +00:00
if ( ! this . preKeys ) {
throw new Error ( 'storePreKey: this.preKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
const id : PreKeyIdType = ` ${ ourUuid . toString ( ) } : ${ keyId } ` ;
if ( this . preKeys . has ( id ) ) {
throw new Error ( ` storePreKey: prekey ${ id } already exists! ` ) ;
2021-04-16 23:13:13 +00:00
}
2021-02-26 23:42:45 +00:00
2021-04-16 23:13:13 +00:00
const fromDB = {
2021-09-10 02:38:11 +00:00
id ,
keyId ,
ourUuid : ourUuid.toString ( ) ,
2021-02-26 23:42:45 +00:00
publicKey : keyPair.pubKey ,
privateKey : keyPair.privKey ,
} ;
2021-04-16 23:13:13 +00:00
await window . Signal . Data . createOrUpdatePreKey ( fromDB ) ;
2021-09-10 02:38:11 +00:00
this . preKeys . 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
}
2021-09-10 02:38:11 +00:00
async removePreKey ( ourUuid : UUID , keyId : number ) : Promise < void > {
2021-02-26 23:42:45 +00:00
if ( ! this . preKeys ) {
throw new Error ( 'removePreKey: this.preKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
const id : PreKeyIdType = ` ${ ourUuid . toString ( ) } : ${ keyId } ` ;
2021-02-26 23:42:45 +00:00
try {
2021-11-30 19:33:51 +00:00
this . trigger ( 'removePreKey' , ourUuid ) ;
2021-02-26 23:42:45 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2021-02-26 23:42:45 +00:00
'removePreKey error triggering removePreKey:' ,
error && error . stack ? error.stack : error
) ;
}
2021-09-10 02:38:11 +00:00
this . preKeys . delete ( id ) ;
await window . Signal . Data . removePreKeyById ( id ) ;
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 ( ) ;
}
2021-02-26 23:42:45 +00:00
await window . Signal . Data . removeAllPreKeys ( ) ;
}
// Signed PreKeys
async loadSignedPreKey (
2021-09-10 02:38:11 +00:00
ourUuid : UUID ,
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!' ) ;
}
2021-09-10 02:38:11 +00:00
const id : SignedPreKeyIdType = ` ${ ourUuid . toString ( ) } : ${ keyId } ` ;
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
}
2021-09-10 02:38:11 +00:00
async loadSignedPreKeys (
ourUuid : UUID
) : Promise < 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
. filter ( ( { fromDB } ) = > fromDB . ourUuid === ourUuid . toString ( ) )
. 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
}
2021-04-16 23:13:13 +00:00
// Note that this is also called in update scenarios, for confirming that signed prekeys
// have indeed been accepted by the server.
2021-02-26 23:42:45 +00:00
async storeSignedPreKey (
2021-09-10 02:38:11 +00:00
ourUuid : UUID ,
2021-02-26 23:42:45 +00:00
keyId : number ,
keyPair : KeyPairType ,
confirmed? : boolean
) : Promise < void > {
if ( ! this . signedPreKeys ) {
throw new Error ( 'storeSignedPreKey: this.signedPreKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
const id : SignedPreKeyIdType = ` ${ ourUuid . toString ( ) } : ${ keyId } ` ;
2021-04-16 23:13:13 +00:00
const fromDB = {
2021-09-10 02:38:11 +00:00
id ,
ourUuid : ourUuid.toString ( ) ,
keyId ,
2021-02-26 23:42:45 +00:00
publicKey : keyPair.pubKey ,
privateKey : keyPair.privKey ,
created_at : Date.now ( ) ,
confirmed : Boolean ( confirmed ) ,
} ;
2021-04-16 23:13:13 +00:00
await window . Signal . Data . 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
}
2021-09-10 02:38:11 +00:00
async removeSignedPreKey ( ourUuid : UUID , keyId : number ) : Promise < void > {
2021-02-26 23:42:45 +00:00
if ( ! this . signedPreKeys ) {
throw new Error ( 'removeSignedPreKey: this.signedPreKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
const id : SignedPreKeyIdType = ` ${ ourUuid . toString ( ) } : ${ keyId } ` ;
this . signedPreKeys . delete ( id ) ;
await window . Signal . Data . removeSignedPreKeyById ( 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 ( ) ;
}
2021-02-26 23:42:45 +00:00
await window . Signal . Data . removeAllSignedPreKeys ( ) ;
}
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 ,
timeout : 1000 * 60 * 2 ,
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 ) {
const errorString = error && error . stack ? error.stack : error ;
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 ) {
const errorString = error && error . stack ? error.stack : error ;
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
await window . Signal . Data . removeSenderKeyById ( id ) ;
this . senderKeys . delete ( id ) ;
} catch ( error ) {
const errorString = error && error . stack ? error.stack : 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 ( ) ;
}
await window . Signal . Data . removeAllSenderKeys ( ) ;
} ) ;
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 ,
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 > {
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
2021-05-20 23:49:08 +00:00
return queue . add < T > ( task ) ;
} ) ;
2021-04-16 23:13:13 +00:00
}
private _createSessionQueue ( ) : PQueue {
2021-11-23 22:01:03 +00:00
return new PQueue ( {
concurrency : 1 ,
timeout : 1000 * 60 * 2 ,
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 ;
}
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.
await window . Signal . Data . commitDecryptResult ( {
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 ;
assert ( 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 ;
assert (
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 } ` ,
2021-05-17 18:03:42 +00:00
error && error . stack
) ;
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 ) {
assert ( this . currentZone === undefined , 'Should not be in the zone' ) ;
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 {
assert ( this . currentZone === zone , 'Should be in the correct zone' ) ;
this . currentZoneDepth -= 1 ;
assert ( this . currentZoneDepth >= 0 , 'Unmatched number of leaveZone calls' ) ;
// 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 ( ) ;
assert ( elem , 'Zone element should be present' ) ;
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
2021-09-10 02:38:11 +00:00
if ( qualifiedAddress === null || qualifiedAddress === undefined ) {
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 ) {
const errorString = error && error . stack ? error.stack : 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!' ) ;
}
2021-09-10 02:38:11 +00:00
const ourUuid = new UUID ( session . ourUuid ) ;
const keyPair = await this . getIdentityKeyPair ( ourUuid ) ;
2021-04-16 23:13:13 +00:00
if ( ! keyPair ) {
throw new Error ( '_maybeMigrateSession: No identity key for ourself!' ) ;
}
2021-09-10 02:38:11 +00:00
const localRegistrationId = await this . getLocalRegistrationId ( ourUuid ) ;
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
2021-09-10 02:38:11 +00:00
if ( qualifiedAddress === null || qualifiedAddress === undefined ) {
throw new Error ( 'storeSession: qualifiedAddress was undefined/null' ) ;
2021-05-19 21:25:56 +00:00
}
2021-09-10 02:38:11 +00:00
const { uuid , deviceId } = qualifiedAddress ;
2021-09-10 17:17:32 +00:00
const conversationId = window . ConversationController . ensureContactIds ( {
uuid : uuid.toString ( ) ,
} ) ;
2021-09-10 02:38:11 +00:00
strictAssert (
2021-09-10 17:17:32 +00:00
conversationId !== undefined ,
'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 ,
2021-09-10 02:38:11 +00:00
ourUuid : qualifiedAddress.ourUuid.toString ( ) ,
2021-10-26 22:59:08 +00:00
conversationId ,
2021-09-10 02:38:11 +00:00
uuid : uuid.toString ( ) ,
2021-05-19 21:25:56 +00:00
deviceId ,
record : record.serialize ( ) . toString ( 'base64' ) ,
} ;
const newSession = {
hydrated : true ,
fromDB ,
item : record ,
} ;
assert ( this . currentZone , 'Must run in the zone' ) ;
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 ) {
const errorString = error && error . stack ? error.stack : 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
}
2021-05-25 22:40:04 +00:00
async getOpenDevices (
2021-09-10 02:38:11 +00:00
ourUuid : UUID ,
2021-08-03 22:42:23 +00:00
identifiers : Array < string > ,
{ zone = GLOBAL_ZONE } : SessionTransactionOptions = { }
2021-05-25 22:40:04 +00:00
) : Promise < {
devices : Array < DeviceType > ;
emptyIdentifiers : Array < string > ;
} > {
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
}
2021-05-25 22:40:04 +00:00
if ( identifiers . length === 0 ) {
2022-02-25 02:40:56 +00:00
return { devices : [ ] , emptyIdentifiers : [ ] } ;
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 {
2021-09-10 02:38:11 +00:00
const uuidsOrIdentifiers = new Set (
identifiers . map (
identifier = > UUID . lookup ( identifier ) ? . toString ( ) || identifier
)
) ;
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 } ) = >
fromDB . ourUuid === ourUuid . toString ( ) &&
uuidsOrIdentifiers . has ( fromDB . uuid )
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
2021-09-10 02:38:11 +00:00
const { uuid } = entry . fromDB ;
uuidsOrIdentifiers . delete ( uuid ) ;
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 {
2021-09-10 02:38:11 +00:00
identifier : uuid ,
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 ) ;
2021-09-10 02:38:11 +00:00
const emptyIdentifiers = Array . from ( uuidsOrIdentifiers . values ( ) ) ;
2021-05-25 22:40:04 +00:00
return {
devices ,
emptyIdentifiers ,
} ;
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' ,
2021-05-17 18:03:42 +00:00
error && error . stack ? error.stack : error
) ;
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 ( {
ourUuid ,
identifier ,
} : Readonly < {
ourUuid : UUID ;
identifier : string ;
} > ) : Promise < Array < number > > {
const { devices } = await this . getOpenDevices ( ourUuid , [ identifier ] ) ;
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 {
await window . Signal . Data . removeSessionById ( id ) ;
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
}
async removeAllSessions ( identifier : string ) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'removeAllSessions' , async ( ) = > {
2021-05-17 18:03:42 +00:00
if ( ! this . sessions ) {
throw new Error ( 'removeAllSessions: this.sessions not yet cached!' ) ;
}
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
if ( identifier === null || identifier === undefined ) {
throw new Error ( 'removeAllSessions: identifier was undefined/null' ) ;
}
2021-02-26 23:42:45 +00:00
2021-09-17 18:27:53 +00:00
log . info ( 'removeAllSessions: deleting sessions for' , identifier ) ;
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
const id = window . ConversationController . getConversationId ( identifier ) ;
2021-09-10 17:17:32 +00:00
strictAssert (
id ,
` removeAllSessions: Conversation not found: ${ identifier } `
) ;
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 ] ;
if ( entry . fromDB . conversationId === id ) {
this . sessions . delete ( entry . fromDB . id ) ;
this . pendingSessions . delete ( entry . fromDB . id ) ;
}
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
await window . Signal . Data . removeSessionsByConversation ( id ) ;
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 ,
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
2021-09-10 02:38:11 +00:00
const { uuid , 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 = >
2021-09-10 02:38:11 +00:00
entry . fromDB . uuid === uuid . toString ( ) &&
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
}
2021-09-10 02:38:11 +00:00
async archiveAllSessions ( uuid : UUID ) : 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
2021-09-17 18:27:53 +00:00
log . info (
2021-05-17 18:03:42 +00:00
'archiveAllSessions: archiving all sessions for' ,
2021-09-10 02:38:11 +00:00
uuid . toString ( )
2021-05-17 18:03:42 +00:00
) ;
2021-02-26 23:42:45 +00:00
2021-05-17 18:03:42 +00:00
const allEntries = this . _getAllSessions ( ) ;
const entries = allEntries . filter (
2021-09-10 02:38:11 +00:00
entry = > entry . fromDB . uuid === uuid . toString ( )
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 ( ) ;
await window . Signal . Data . removeAllSessions ( ) ;
} ) ;
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' ,
2021-06-15 00:09:37 +00:00
< SessionResetsType > { }
) ;
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 ( ) ;
window . storage . put ( 'sessionResets' , sessionResets ) ;
try {
2021-09-10 02:38:11 +00:00
const { uuid } = qualifiedAddress ;
2021-05-18 00:41:28 +00:00
// First, fetch this conversation
const conversationId = window . ConversationController . ensureContactIds ( {
2021-09-10 02:38:11 +00:00
uuid : uuid.toString ( ) ,
2021-05-18 00:41:28 +00:00
} ) ;
assert ( conversationId , ` lightSessionReset/ ${ id } : missing conversationId ` ) ;
const conversation = window . ConversationController . get ( conversationId ) ;
assert ( conversation , ` lightSessionReset/ ${ id } : missing conversation ` ) ;
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
await singleProtoJobQueue . add (
2022-06-13 21:39:35 +00:00
MessageSender . getNullMessage ( {
2022-01-14 21:34:52 +00:00
uuid : uuid.toString ( ) ,
} )
2021-07-15 23:48:09 +00:00
) ;
2021-05-18 00:41:28 +00:00
} catch ( error ) {
// If we failed to do the session reset, then we'll allow another attempt sooner
// than one hour from now.
delete sessionResets [ id ] ;
window . storage . put ( 'sessionResets' , sessionResets ) ;
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
2021-09-10 02:38:11 +00:00
getIdentityRecord ( uuid : UUID ) : 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
const id = uuid . toString ( ) ;
2021-02-26 23:42:45 +00:00
2021-09-10 02:38:11 +00:00
try {
2021-05-14 01:18:43 +00:00
const entry = this . identityKeys . get ( id ) ;
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 (
2021-09-10 02:38:11 +00:00
` getIdentityRecord: Failed to get identity record for identifier ${ id } `
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 (
uuid : UUID
) : Promise < IdentityKeyType | undefined > {
if ( ! this . identityKeys ) {
throw new Error (
'getOrMigrateIdentityRecord: this.identityKeys not yet cached!'
) ;
}
const result = this . getIdentityRecord ( uuid ) ;
if ( result ) {
return result ;
}
const newId = uuid . toString ( ) ;
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 ) ;
await window . Signal . Data . removeIdentityKeyById ( record . fromDB . id ) ;
return newRecord ;
}
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
}
if ( encodedAddress === null || encodedAddress === undefined ) {
2021-04-16 23:13:13 +00:00
throw new Error ( 'isTrustedIdentity: encodedAddress was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
const ourUuid = window . textsecure . storage . user . getCheckedUuid ( ) ;
const isOurIdentifier = encodedAddress . uuid . isEqual ( ourUuid ) ;
2021-02-26 23:42:45 +00:00
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord (
encodedAddress . uuid
) ;
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 :
2021-02-26 23:42:45 +00:00
return this . isTrustedForSending ( 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
}
}
isTrustedForSending (
2021-09-24 00:49:05 +00:00
publicKey : Uint8Array ,
2021-02-26 23:42:45 +00:00
identityRecord? : IdentityKeyType
) : boolean {
if ( ! identityRecord ) {
2021-09-17 18:27:53 +00:00
log . info ( 'isTrustedForSending: No previous record, 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 ) {
2021-09-17 18:27:53 +00:00
log . error ( 'isTrustedIdentity: 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 ;
}
2021-09-24 00:49:05 +00:00
async loadIdentityKey ( uuid : UUID ) : Promise < Uint8Array | undefined > {
2021-09-10 02:38:11 +00:00
if ( uuid === null || uuid === undefined ) {
throw new Error ( 'loadIdentityKey: uuid was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( uuid ) ;
2021-02-26 23:42:45 +00:00
if ( identityRecord ) {
return identityRecord . publicKey ;
}
return undefined ;
}
private async _saveIdentityKey ( data : IdentityKeyType ) : Promise < void > {
if ( ! this . identityKeys ) {
throw new Error ( '_saveIdentityKey: this.identityKeys not yet cached!' ) ;
}
const { id } = data ;
2021-04-16 23:13:13 +00:00
await window . Signal . Data . 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
}
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 ,
2021-05-19 21:25:56 +00:00
{ zone } : SessionTransactionOptions = { }
2021-02-26 23:42:45 +00:00
) : Promise < boolean > {
if ( ! this . identityKeys ) {
throw new Error ( 'saveIdentity: this.identityKeys not yet cached!' ) ;
}
if ( encodedAddress === null || encodedAddress === undefined ) {
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 ;
}
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord (
encodedAddress . uuid
) ;
const id = encodedAddress . uuid . toString ( ) ;
2021-02-26 23:42:45 +00:00
if ( ! identityRecord || ! identityRecord . publicKey ) {
// Lookup failed, or the current key was removed, so save this one.
2021-09-17 18:27:53 +00:00
log . info ( 'saveIdentity: Saving new identity...' ) ;
2021-02-26 23:42:45 +00:00
await this . _saveIdentityKey ( {
id ,
publicKey ,
firstUse : true ,
timestamp : Date.now ( ) ,
verified : VerifiedStatus.DEFAULT ,
nonblockingApproval ,
} ) ;
return false ;
}
const oldpublicKey = identityRecord . publicKey ;
if ( ! constantTimeEqual ( oldpublicKey , publicKey ) ) {
2021-09-17 18:27:53 +00:00
log . info ( 'saveIdentity: Replacing existing identity...' ) ;
2021-02-26 23:42:45 +00:00
const previousStatus = identityRecord . verified ;
let verifiedStatus ;
if (
previousStatus === VerifiedStatus . VERIFIED ||
previousStatus === VerifiedStatus . UNVERIFIED
) {
verifiedStatus = VerifiedStatus . UNVERIFIED ;
} else {
verifiedStatus = VerifiedStatus . DEFAULT ;
}
await this . _saveIdentityKey ( {
id ,
publicKey ,
firstUse : false ,
timestamp : Date.now ( ) ,
verified : verifiedStatus ,
nonblockingApproval ,
} ) ;
try {
2021-09-10 02:38:11 +00:00
this . trigger ( 'keychange' , encodedAddress . uuid ) ;
2021-02-26 23:42:45 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2021-04-16 23:13:13 +00:00
'saveIdentity: error triggering keychange:' ,
2021-02-26 23:42:45 +00:00
error && error . stack ? error.stack : error
) ;
}
2021-05-18 00:41:28 +00:00
2021-05-19 21:25:56 +00:00
// Pass the zone to facilitate transactional session use in
2021-05-18 00:41:28 +00:00
// MessageReceiver.ts
2021-09-10 02:38:11 +00:00
await this . archiveSiblingSessions ( encodedAddress , {
zone ,
} ) ;
2021-02-26 23:42:45 +00:00
return true ;
}
if ( this . isNonBlockingApprovalRequired ( identityRecord ) ) {
2021-09-17 18:27:53 +00:00
log . info ( 'saveIdentity: Setting approval status...' ) ;
2021-02-26 23:42:45 +00:00
identityRecord . nonblockingApproval = nonblockingApproval ;
await this . _saveIdentityKey ( identityRecord ) ;
return false ;
}
return false ;
}
isNonBlockingApprovalRequired ( identityRecord : IdentityKeyType ) : boolean {
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 (
2021-09-10 02:38:11 +00:00
uuid : UUID ,
2021-04-16 23:13:13 +00:00
attributes : Partial < IdentityKeyType >
2021-02-26 23:42:45 +00:00
) : Promise < void > {
2021-09-10 02:38:11 +00:00
if ( uuid === null || uuid === undefined ) {
throw new Error ( 'saveIdentityWithAttributes: uuid was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( uuid ) ;
const id = uuid . toString ( ) ;
2022-04-20 19:35:53 +00:00
// When saving a PNI identity - don't create a separate conversation
const uuidKind = window . textsecure . storage . user . getOurUuidKind ( uuid ) ;
if ( uuidKind !== UUIDKind . PNI ) {
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 ) ;
}
}
2021-09-10 02:38:11 +00:00
async setApproval ( uuid : UUID , nonblockingApproval : boolean ) : Promise < void > {
if ( uuid === null || uuid === undefined ) {
throw new Error ( 'setApproval: uuid 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
}
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( uuid ) ;
2021-02-26 23:42:45 +00:00
if ( ! identityRecord ) {
2021-09-10 02:38:11 +00:00
throw new Error ( ` setApproval: No identity record for ${ uuid } ` ) ;
2021-02-26 23:42:45 +00:00
}
identityRecord . nonblockingApproval = nonblockingApproval ;
await this . _saveIdentityKey ( identityRecord ) ;
}
async setVerified (
2021-09-10 02:38:11 +00:00
uuid : UUID ,
2021-02-26 23:42:45 +00:00
verifiedStatus : number ,
2021-09-24 00:49:05 +00:00
publicKey? : Uint8Array
2021-02-26 23:42:45 +00:00
) : Promise < void > {
2021-09-10 02:38:11 +00:00
if ( uuid === null || uuid === undefined ) {
throw new Error ( 'setVerified: uuid 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
}
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( uuid ) ;
2021-02-26 23:42:45 +00:00
if ( ! identityRecord ) {
2021-09-10 02:38:11 +00:00
throw new Error ( ` setVerified: No identity record for ${ uuid . toString ( ) } ` ) ;
2021-02-26 23:42:45 +00:00
}
if ( ! publicKey || constantTimeEqual ( identityRecord . publicKey , publicKey ) ) {
identityRecord . verified = verifiedStatus ;
2021-04-16 23:13:13 +00:00
if ( validateIdentityKey ( identityRecord ) ) {
2021-02-26 23:42:45 +00:00
await this . _saveIdentityKey ( identityRecord ) ;
}
} else {
2021-09-17 18:27:53 +00:00
log . info ( 'setVerified: No identity record for specified publicKey' ) ;
2021-02-26 23:42:45 +00:00
}
}
2021-09-10 02:38:11 +00:00
async getVerified ( uuid : UUID ) : Promise < number > {
if ( uuid === null || uuid === undefined ) {
throw new Error ( 'getVerified: uuid was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( uuid ) ;
2021-02-26 23:42:45 +00:00
if ( ! identityRecord ) {
2021-09-10 02:38:11 +00:00
throw new Error ( ` getVerified: No identity record for ${ uuid } ` ) ;
2021-02-26 23:42:45 +00:00
}
const verifiedStatus = identityRecord . verified ;
if ( validateVerifiedStatus ( verifiedStatus ) ) {
return verifiedStatus ;
}
return VerifiedStatus . DEFAULT ;
}
2022-03-21 22:06:34 +00:00
// See https://github.com/signalapp/Signal-iOS-Private/blob/e32c2dff0d03f67467b4df621d84b11412d50cdb/SignalServiceKit/src/Messages/OWSIdentityManager.m#L317
// for reference.
2021-02-26 23:42:45 +00:00
async processVerifiedMessage (
2021-09-10 02:38:11 +00:00
uuid : UUID ,
2021-02-26 23:42:45 +00:00
verifiedStatus : number ,
2021-09-24 00:49:05 +00:00
publicKey? : Uint8Array
2021-02-26 23:42:45 +00:00
) : Promise < boolean > {
2021-09-10 02:38:11 +00:00
if ( uuid === null || uuid === undefined ) {
throw new Error ( 'processVerifiedMessage: uuid 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 ( 'processVerifiedMessage: Invalid verified status' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-24 00:49:05 +00:00
if ( publicKey !== undefined && ! ( publicKey instanceof Uint8Array ) ) {
2021-04-16 23:13:13 +00:00
throw new Error ( 'processVerifiedMessage: Invalid public key' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
const identityRecord = await this . getOrMigrateIdentityRecord ( uuid ) ;
2021-02-26 23:42:45 +00:00
let isEqual = false ;
if ( identityRecord && publicKey ) {
isEqual = constantTimeEqual ( publicKey , identityRecord . publicKey ) ;
}
2022-03-21 22:06:34 +00:00
// Just update verified status if the key is the same or not present
if ( isEqual || ! publicKey ) {
2021-09-10 02:38:11 +00:00
await this . setVerified ( uuid , verifiedStatus , publicKey ) ;
2021-02-26 23:42:45 +00:00
return false ;
}
2022-03-21 22:06:34 +00:00
await this . saveIdentityWithAttributes ( uuid , {
publicKey ,
verified : verifiedStatus ,
firstUse : false ,
timestamp : Date.now ( ) ,
nonblockingApproval : verifiedStatus === VerifiedStatus . VERIFIED ,
} ) ;
2021-02-26 23:42:45 +00:00
2022-03-21 22:06:34 +00:00
if ( identityRecord ) {
try {
this . trigger ( 'keychange' , uuid ) ;
} catch ( error ) {
log . error (
'processVerifiedMessage error triggering keychange:' ,
Errors . toLogFormat ( error )
) ;
2021-02-26 23:42:45 +00:00
}
2022-03-21 22:06:34 +00:00
// true signifies that we overwrote a previous key with a new one
return true ;
2021-02-26 23:42:45 +00:00
}
return false ;
}
2021-09-10 02:38:11 +00:00
isUntrusted ( uuid : UUID ) : boolean {
if ( uuid === null || uuid === undefined ) {
throw new Error ( 'isUntrusted: uuid was undefined/null' ) ;
2021-02-26 23:42:45 +00:00
}
2021-09-10 02:38:11 +00:00
const identityRecord = this . getIdentityRecord ( uuid ) ;
2021-02-26 23:42:45 +00:00
if ( ! identityRecord ) {
2021-09-10 02:38:11 +00:00
throw new Error ( ` isUntrusted: No identity record for ${ uuid . toString ( ) } ` ) ;
2021-02-26 23:42:45 +00:00
}
if (
2021-03-22 21:08:52 +00:00
isMoreRecentThan ( identityRecord . timestamp , TIMESTAMP_THRESHOLD ) &&
2021-02-26 23:42:45 +00:00
! identityRecord . nonblockingApproval &&
! identityRecord . firstUse
) {
return true ;
}
return false ;
}
2021-09-10 02:38:11 +00:00
async removeIdentityKey ( uuid : UUID ) : Promise < void > {
2021-02-26 23:42:45 +00:00
if ( ! this . identityKeys ) {
throw new Error ( 'removeIdentityKey: this.identityKeys not yet cached!' ) ;
}
2021-09-10 02:38:11 +00:00
const id = uuid . toString ( ) ;
this . identityKeys . delete ( id ) ;
await window . Signal . Data . removeIdentityKeyById ( id ) ;
await this . removeAllSessions ( id ) ;
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 ( ) = > {
2021-05-17 18:03:42 +00:00
return window . Signal . Data . getUnprocessedCount ( ) ;
} ) ;
2021-02-26 23:42:45 +00:00
}
2022-04-28 22:28:30 +00:00
getAllUnprocessedAndIncrementAttempts ( ) : Promise < Array < UnprocessedType > > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'getAllUnprocessed' , async ( ) = > {
2022-04-28 22:28:30 +00:00
return window . Signal . Data . getAllUnprocessedAndIncrementAttempts ( ) ;
2021-05-17 18:03:42 +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 ( ) = > {
2021-05-17 18:03:42 +00:00
return window . Signal . Data . getUnprocessedById ( id ) ;
} ) ;
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 ( ) = > {
2021-05-17 18:03:42 +00:00
await window . Signal . Data . updateUnprocessedWithData ( id , data ) ;
} ) ;
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 ( ) = > {
await window . Signal . Data . updateUnprocessedsWithData ( items ) ;
}
) ;
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 ( ) = > {
2021-05-17 18:03:42 +00:00
await window . Signal . Data . removeUnprocessed ( idOrArray ) ;
} ) ;
2021-02-26 23:42:45 +00:00
}
removeAllUnprocessed ( ) : Promise < void > {
2021-05-19 21:25:56 +00:00
return this . withZone ( GLOBAL_ZONE , 'removeAllUnprocessed' , async ( ) = > {
2021-05-17 18:03:42 +00:00
await window . Signal . Data . removeAllUnprocessed ( ) ;
} ) ;
2021-02-26 23:42:45 +00:00
}
async removeAllData ( ) : Promise < void > {
await window . Signal . Data . removeAll ( ) ;
await this . hydrateCaches ( ) ;
window . storage . reset ( ) ;
await window . storage . fetch ( ) ;
window . ConversationController . reset ( ) ;
await window . ConversationController . load ( ) ;
}
2021-08-30 21:39:57 +00:00
async removeAllConfiguration ( mode : RemoveAllConfiguration ) : Promise < void > {
await window . Signal . Data . removeAllConfiguration ( mode ) ;
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
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 ( ) ) ;
}
2021-02-26 23:42:45 +00:00
}
window . SignalProtocolStore = SignalProtocolStore ;