Send support for Sender Key
This commit is contained in:
parent
d8417e562b
commit
e6f1ec2b6b
30 changed files with 2290 additions and 911 deletions
|
@ -10,19 +10,16 @@
|
|||
|
||||
import { reject } from 'lodash';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
CiphertextMessageType,
|
||||
PreKeyBundle,
|
||||
processPreKeyBundle,
|
||||
ProtocolAddress,
|
||||
PublicKey,
|
||||
sealedSenderEncryptMessage,
|
||||
SenderCertificate,
|
||||
signalEncrypt,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { ServerKeysType, WebAPIType } from './WebAPI';
|
||||
import { WebAPIType } from './WebAPI';
|
||||
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
||||
import {
|
||||
CallbackResultType,
|
||||
|
@ -40,6 +37,7 @@ import {
|
|||
import { isValidNumber } from '../types/PhoneNumber';
|
||||
import { Sessions, IdentityKeys } from '../LibSignalStores';
|
||||
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
||||
import { getKeysForIdentifier } from './getKeysForIdentifier';
|
||||
|
||||
export const enum SenderCertificateMode {
|
||||
WithE164,
|
||||
|
@ -80,6 +78,27 @@ function ciphertextMessageTypeToEnvelopeType(type: number) {
|
|||
);
|
||||
}
|
||||
|
||||
function getPaddedMessageLength(messageLength: number): number {
|
||||
const messageLengthWithTerminator = messageLength + 1;
|
||||
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
||||
|
||||
if (messageLengthWithTerminator % 160 !== 0) {
|
||||
messagePartCount += 1;
|
||||
}
|
||||
|
||||
return messagePartCount * 160;
|
||||
}
|
||||
|
||||
export function padMessage(messageBuffer: ArrayBuffer): Uint8Array {
|
||||
const plaintext = new Uint8Array(
|
||||
getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
||||
);
|
||||
plaintext.set(new Uint8Array(messageBuffer));
|
||||
plaintext[messageBuffer.byteLength] = 0x80;
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
export default class OutgoingMessage {
|
||||
server: WebAPIType;
|
||||
|
||||
|
@ -187,95 +206,26 @@ export default class OutgoingMessage {
|
|||
identifier: string,
|
||||
recurse?: boolean
|
||||
): () => Promise<void> {
|
||||
return async () =>
|
||||
window.textsecure.storage.protocol
|
||||
.getDeviceIds(identifier)
|
||||
.then(async deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
this.registerError(
|
||||
identifier,
|
||||
'reloadDevicesAndSend: Got empty device list when loading device keys',
|
||||
undefined
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||
});
|
||||
return async () => {
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
|
||||
identifier
|
||||
);
|
||||
if (deviceIds.length === 0) {
|
||||
this.registerError(
|
||||
identifier,
|
||||
'reloadDevicesAndSend: Got empty device list when loading device keys',
|
||||
undefined
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||
};
|
||||
}
|
||||
|
||||
async getKeysForIdentifier(
|
||||
identifier: string,
|
||||
updateDevices: Array<number> | undefined
|
||||
updateDevices?: Array<number>
|
||||
): Promise<void | Array<void | null>> {
|
||||
const handleResult = async (response: ServerKeysType) => {
|
||||
const sessionStore = new Sessions();
|
||||
const identityKeyStore = new IdentityKeys();
|
||||
|
||||
return Promise.all(
|
||||
response.devices.map(async device => {
|
||||
const { deviceId, registrationId, preKey, signedPreKey } = device;
|
||||
if (
|
||||
updateDevices === undefined ||
|
||||
updateDevices.indexOf(deviceId) > -1
|
||||
) {
|
||||
if (device.registrationId === 0) {
|
||||
window.log.info('device registrationId 0!');
|
||||
}
|
||||
if (!signedPreKey) {
|
||||
throw new Error(
|
||||
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
|
||||
);
|
||||
}
|
||||
const protocolAddress = ProtocolAddress.new(identifier, deviceId);
|
||||
const preKeyId = preKey?.keyId || null;
|
||||
const preKeyObject = preKey
|
||||
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
|
||||
: null;
|
||||
const signedPreKeyObject = PublicKey.deserialize(
|
||||
Buffer.from(signedPreKey.publicKey)
|
||||
);
|
||||
const identityKey = PublicKey.deserialize(
|
||||
Buffer.from(response.identityKey)
|
||||
);
|
||||
|
||||
const preKeyBundle = PreKeyBundle.new(
|
||||
registrationId,
|
||||
deviceId,
|
||||
preKeyId,
|
||||
preKeyObject,
|
||||
signedPreKey.keyId,
|
||||
signedPreKeyObject,
|
||||
Buffer.from(signedPreKey.signature),
|
||||
identityKey
|
||||
);
|
||||
|
||||
const address = `${identifier}.${deviceId}`;
|
||||
await window.textsecure.storage.protocol
|
||||
.enqueueSessionJob(address, () =>
|
||||
processPreKeyBundle(
|
||||
preKeyBundle,
|
||||
protocolAddress,
|
||||
sessionStore,
|
||||
identityKeyStore
|
||||
)
|
||||
)
|
||||
.catch(error => {
|
||||
if (
|
||||
error?.message?.includes('untrusted identity for address')
|
||||
) {
|
||||
error.timestamp = this.timestamp;
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
error.identityKey = response.identityKey;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const { sendMetadata } = this;
|
||||
const info =
|
||||
sendMetadata && sendMetadata[identifier]
|
||||
|
@ -283,65 +233,23 @@ export default class OutgoingMessage {
|
|||
: { accessKey: undefined };
|
||||
const { accessKey } = info;
|
||||
|
||||
if (updateDevices === undefined) {
|
||||
if (accessKey) {
|
||||
return this.server
|
||||
.getKeysForIdentifierUnauth(identifier, undefined, { accessKey })
|
||||
.catch(async (error: Error) => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
return this.server.getKeysForIdentifier(identifier);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.then(handleResult);
|
||||
try {
|
||||
const { accessKeyFailed } = await getKeysForIdentifier(
|
||||
identifier,
|
||||
this.server,
|
||||
updateDevices,
|
||||
accessKey
|
||||
);
|
||||
if (accessKeyFailed && !this.failoverIdentifiers.includes(identifier)) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
|
||||
return this.server.getKeysForIdentifier(identifier).then(handleResult);
|
||||
} catch (error) {
|
||||
if (error?.message?.includes('untrusted identity for address')) {
|
||||
error.timestamp = this.timestamp;
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let promise: Promise<void | Array<void | null>> = Promise.resolve();
|
||||
updateDevices.forEach(deviceId => {
|
||||
promise = promise.then(async () => {
|
||||
let innerPromise;
|
||||
|
||||
if (accessKey) {
|
||||
innerPromise = this.server
|
||||
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
|
||||
.then(handleResult)
|
||||
.catch(async error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
return this.server
|
||||
.getKeysForIdentifier(identifier, deviceId)
|
||||
.then(handleResult);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
innerPromise = this.server
|
||||
.getKeysForIdentifier(identifier, deviceId)
|
||||
.then(handleResult);
|
||||
}
|
||||
|
||||
return innerPromise.catch(async e => {
|
||||
if (e.name === 'HTTPError' && e.code === 404) {
|
||||
if (deviceId !== 1) {
|
||||
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
|
||||
}
|
||||
throw new UnregisteredUserError(identifier, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async transmitMessage(
|
||||
|
@ -389,25 +297,9 @@ export default class OutgoingMessage {
|
|||
});
|
||||
}
|
||||
|
||||
getPaddedMessageLength(messageLength: number): number {
|
||||
const messageLengthWithTerminator = messageLength + 1;
|
||||
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
||||
|
||||
if (messageLengthWithTerminator % 160 !== 0) {
|
||||
messagePartCount += 1;
|
||||
}
|
||||
|
||||
return messagePartCount * 160;
|
||||
}
|
||||
|
||||
getPlaintext(): ArrayBuffer {
|
||||
if (!this.plaintext) {
|
||||
const messageBuffer = this.message.toArrayBuffer();
|
||||
this.plaintext = new Uint8Array(
|
||||
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
||||
);
|
||||
this.plaintext.set(new Uint8Array(messageBuffer));
|
||||
this.plaintext[messageBuffer.byteLength] = 0x80;
|
||||
this.plaintext = padMessage(this.message.toArrayBuffer());
|
||||
}
|
||||
return this.plaintext;
|
||||
}
|
||||
|
@ -629,34 +521,6 @@ export default class OutgoingMessage {
|
|||
});
|
||||
}
|
||||
|
||||
async getStaleDeviceIdsForIdentifier(
|
||||
identifier: string
|
||||
): Promise<Array<number> | undefined> {
|
||||
const sessionStore = new Sessions();
|
||||
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
|
||||
identifier
|
||||
);
|
||||
if (deviceIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateDevices: Array<number> = [];
|
||||
await Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const record = await sessionStore.getSession(
|
||||
ProtocolAddress.new(identifier, deviceId)
|
||||
);
|
||||
|
||||
if (!record || !record.hasCurrentState()) {
|
||||
updateDevices.push(deviceId);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return updateDevices;
|
||||
}
|
||||
|
||||
async removeDeviceIdsForIdentifier(
|
||||
identifier: string,
|
||||
deviceIdsToRemove: Array<number>
|
||||
|
@ -713,10 +577,12 @@ export default class OutgoingMessage {
|
|||
);
|
||||
}
|
||||
|
||||
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
|
||||
identifier
|
||||
);
|
||||
await this.getKeysForIdentifier(identifier, updateDevices);
|
||||
if (deviceIds.length === 0) {
|
||||
await this.getKeysForIdentifier(identifier);
|
||||
}
|
||||
await this.reloadDevicesAndSend(identifier, true)();
|
||||
} catch (error) {
|
||||
if (error?.message?.includes('untrusted identity for address')) {
|
||||
|
|
File diff suppressed because it is too large
Load diff
5
ts/textsecure/Types.d.ts
vendored
5
ts/textsecure/Types.d.ts
vendored
|
@ -11,6 +11,11 @@ export {
|
|||
UnprocessedUpdateType,
|
||||
} from '../sql/Interface';
|
||||
|
||||
export type DeviceType = {
|
||||
id: number;
|
||||
identifier: string;
|
||||
};
|
||||
|
||||
// How the legacy APIs generate these types
|
||||
|
||||
export type CompatSignedPreKeyType = {
|
||||
|
|
|
@ -26,6 +26,7 @@ import { pki } from 'node-forge';
|
|||
import is from '@sindresorhus/is';
|
||||
import PQueue from 'p-queue';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Long } from '../window.d';
|
||||
import { getUserAgent } from '../util/getUserAgent';
|
||||
|
@ -351,6 +352,49 @@ type ArrayBufferWithDetailsType = {
|
|||
response: Response;
|
||||
};
|
||||
|
||||
export const multiRecipient200ResponseSchema = z
|
||||
.object({
|
||||
uuids404: z.array(z.string()).optional(),
|
||||
needsSync: z.boolean().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
export type MultiRecipient200ResponseType = z.infer<
|
||||
typeof multiRecipient200ResponseSchema
|
||||
>;
|
||||
|
||||
export const multiRecipient409ResponseSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
uuid: z.string(),
|
||||
devices: z
|
||||
.object({
|
||||
missingDevices: z.array(z.number()).optional(),
|
||||
extraDevices: z.array(z.number()).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough()
|
||||
);
|
||||
export type MultiRecipient409ResponseType = z.infer<
|
||||
typeof multiRecipient409ResponseSchema
|
||||
>;
|
||||
|
||||
export const multiRecipient410ResponseSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
uuid: z.string(),
|
||||
devices: z
|
||||
.object({
|
||||
staleDevices: z.array(z.number()).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough()
|
||||
);
|
||||
export type MultiRecipient410ResponseType = z.infer<
|
||||
typeof multiRecipient410ResponseSchema
|
||||
>;
|
||||
|
||||
function isSuccess(status: number): boolean {
|
||||
return status >= 0 && status < 400;
|
||||
}
|
||||
|
@ -685,6 +729,7 @@ const URL_CALLS = {
|
|||
groupToken: 'v1/groups/token',
|
||||
keys: 'v2/keys',
|
||||
messages: 'v1/messages',
|
||||
multiRecipient: 'v1/messages/multi_recipient',
|
||||
profile: 'v1/profile',
|
||||
registerCapabilities: 'v1/devices/capabilities',
|
||||
removeSignalingKey: 'v1/accounts/signaling_key',
|
||||
|
@ -728,6 +773,7 @@ type AjaxOptionsType = {
|
|||
call: keyof typeof URL_CALLS;
|
||||
contentType?: string;
|
||||
data?: ArrayBuffer | Buffer | string;
|
||||
headers?: HeaderListType;
|
||||
host?: string;
|
||||
httpType: HTTPCodeType;
|
||||
jsonData?: any;
|
||||
|
@ -749,10 +795,12 @@ export type WebAPIConnectType = {
|
|||
export type CapabilitiesType = {
|
||||
gv2: boolean;
|
||||
'gv1-migration': boolean;
|
||||
senderKey: boolean;
|
||||
};
|
||||
export type CapabilitiesUploadType = {
|
||||
'gv2-3': boolean;
|
||||
'gv1-migration': boolean;
|
||||
senderKey: boolean;
|
||||
};
|
||||
|
||||
type StickerPackManifestType = any;
|
||||
|
@ -895,6 +943,12 @@ export type WebAPIType = {
|
|||
online?: boolean,
|
||||
options?: { accessKey?: string }
|
||||
) => Promise<void>;
|
||||
sendWithSenderKey: (
|
||||
payload: ArrayBuffer,
|
||||
accessKeys: ArrayBuffer,
|
||||
timestamp: number,
|
||||
online?: boolean
|
||||
) => Promise<MultiRecipient200ResponseType>;
|
||||
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
|
||||
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||
uploadGroupAvatar: (
|
||||
|
@ -1065,6 +1119,7 @@ export function initialize({
|
|||
requestVerificationVoice,
|
||||
sendMessages,
|
||||
sendMessagesUnauth,
|
||||
sendWithSenderKey,
|
||||
setSignedPreKey,
|
||||
updateDeviceName,
|
||||
uploadGroupAvatar,
|
||||
|
@ -1082,6 +1137,7 @@ export function initialize({
|
|||
certificateAuthority,
|
||||
contentType: param.contentType || 'application/json; charset=utf-8',
|
||||
data: param.data || (param.jsonData && _jsonThing(param.jsonData)),
|
||||
headers: param.headers,
|
||||
host: param.host || url,
|
||||
password: param.password || password,
|
||||
path: URL_CALLS[param.call] + param.urlParameters,
|
||||
|
@ -1375,6 +1431,7 @@ export function initialize({
|
|||
const capabilities: CapabilitiesUploadType = {
|
||||
'gv2-3': true,
|
||||
'gv1-migration': true,
|
||||
senderKey: false,
|
||||
};
|
||||
|
||||
const { accessKey } = options;
|
||||
|
@ -1661,6 +1718,25 @@ export function initialize({
|
|||
});
|
||||
}
|
||||
|
||||
async function sendWithSenderKey(
|
||||
data: ArrayBuffer,
|
||||
accessKeys: ArrayBuffer,
|
||||
timestamp: number,
|
||||
online?: boolean
|
||||
): Promise<MultiRecipient200ResponseType> {
|
||||
return _ajax({
|
||||
call: 'multiRecipient',
|
||||
httpType: 'PUT',
|
||||
contentType: 'application/vnd.signal-messenger.mrm',
|
||||
data,
|
||||
urlParameters: `?ts=${timestamp}&online=${online ? 'true' : 'false'}`,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'Unidentified-Access-Key': arrayBufferToBase64(accessKeys),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function redactStickerUrl(stickerUrl: string) {
|
||||
return stickerUrl.replace(
|
||||
/(\/stickers\/)([^/]+)(\/)/,
|
||||
|
|
140
ts/textsecure/getKeysForIdentifier.ts
Normal file
140
ts/textsecure/getKeysForIdentifier.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
PreKeyBundle,
|
||||
processPreKeyBundle,
|
||||
ProtocolAddress,
|
||||
PublicKey,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { UnregisteredUserError } from './Errors';
|
||||
import { Sessions, IdentityKeys } from '../LibSignalStores';
|
||||
import { ServerKeysType, WebAPIType } from './WebAPI';
|
||||
|
||||
export async function getKeysForIdentifier(
|
||||
identifier: string,
|
||||
server: WebAPIType,
|
||||
devicesToUpdate?: Array<number>,
|
||||
accessKey?: string
|
||||
): Promise<{ accessKeyFailed?: boolean }> {
|
||||
try {
|
||||
const { keys, accessKeyFailed } = await getServerKeys(
|
||||
identifier,
|
||||
server,
|
||||
accessKey
|
||||
);
|
||||
|
||||
await handleServerKeys(identifier, keys, devicesToUpdate);
|
||||
|
||||
return {
|
||||
accessKeyFailed,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.name === 'HTTPError' && error.code === 404) {
|
||||
await window.textsecure.storage.protocol.archiveAllSessions(identifier);
|
||||
}
|
||||
throw new UnregisteredUserError(identifier, error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getServerKeys(
|
||||
identifier: string,
|
||||
server: WebAPIType,
|
||||
accessKey?: string
|
||||
): Promise<{ accessKeyFailed?: boolean; keys: ServerKeysType }> {
|
||||
if (!accessKey) {
|
||||
return {
|
||||
keys: await server.getKeysForIdentifier(identifier),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
keys: await server.getKeysForIdentifierUnauth(identifier, undefined, {
|
||||
accessKey,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
return {
|
||||
accessKeyFailed: true,
|
||||
keys: await server.getKeysForIdentifier(identifier),
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleServerKeys(
|
||||
identifier: string,
|
||||
response: ServerKeysType,
|
||||
devicesToUpdate?: Array<number>
|
||||
): Promise<void> {
|
||||
const sessionStore = new Sessions();
|
||||
const identityKeyStore = new IdentityKeys();
|
||||
|
||||
await Promise.all(
|
||||
response.devices.map(async device => {
|
||||
const { deviceId, registrationId, preKey, signedPreKey } = device;
|
||||
if (
|
||||
devicesToUpdate !== undefined &&
|
||||
!devicesToUpdate.includes(deviceId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.registrationId === 0) {
|
||||
window.log.info(
|
||||
`handleServerKeys/${identifier}: Got device registrationId zero!`
|
||||
);
|
||||
}
|
||||
if (!signedPreKey) {
|
||||
throw new Error(
|
||||
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
|
||||
);
|
||||
}
|
||||
const protocolAddress = ProtocolAddress.new(identifier, deviceId);
|
||||
const preKeyId = preKey?.keyId || null;
|
||||
const preKeyObject = preKey
|
||||
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
|
||||
: null;
|
||||
const signedPreKeyObject = PublicKey.deserialize(
|
||||
Buffer.from(signedPreKey.publicKey)
|
||||
);
|
||||
const identityKey = PublicKey.deserialize(
|
||||
Buffer.from(response.identityKey)
|
||||
);
|
||||
|
||||
const preKeyBundle = PreKeyBundle.new(
|
||||
registrationId,
|
||||
deviceId,
|
||||
preKeyId,
|
||||
preKeyObject,
|
||||
signedPreKey.keyId,
|
||||
signedPreKeyObject,
|
||||
Buffer.from(signedPreKey.signature),
|
||||
identityKey
|
||||
);
|
||||
|
||||
const address = `${identifier}.${deviceId}`;
|
||||
await window.textsecure.storage.protocol
|
||||
.enqueueSessionJob(address, () =>
|
||||
processPreKeyBundle(
|
||||
preKeyBundle,
|
||||
protocolAddress,
|
||||
sessionStore,
|
||||
identityKeyStore
|
||||
)
|
||||
)
|
||||
.catch(error => {
|
||||
if (error?.message?.includes('untrusted identity for address')) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.identityKey = response.identityKey;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue