session-desktop/ts/session/apis/snode_api/onsResolve.ts

146 lines
4.8 KiB
TypeScript

import _, { range } from 'lodash';
import { getSodiumRenderer } from '../../crypto';
import {
fromHexToArray,
fromUInt8ArrayToBase64,
stringToUint8Array,
toHex,
} from '../../utils/String';
import { doSnodeBatchRequest } from './batchRequest';
import { GetNetworkTime } from './getNetworkTime';
import { getRandomSnode } from './snodePool';
import { OnsResolveSubRequest } from './SnodeRequestTypes';
// ONS name can have [a-zA-Z0-9_-] except that - is not allowed as start or end
// do not define a regex but rather create it on the fly to avoid https://stackoverflow.com/questions/3891641/regex-test-only-works-every-other-time
const onsNameRegex = '^\\w([\\w-]*[\\w])?$';
function buildOnsResolveRequests(base64EncodedNameHash: string): Array<OnsResolveSubRequest> {
const request: OnsResolveSubRequest = {
method: 'oxend_request',
params: {
endpoint: 'ons_resolve',
params: { type: 0, name_hash: base64EncodedNameHash },
},
};
return [request];
}
async function getSessionIDForOnsName(onsNameCase: string) {
const validationCount = 3;
const onsNameLowerCase = onsNameCase.toLowerCase();
const sodium = await getSodiumRenderer();
const nameAsData = stringToUint8Array(onsNameLowerCase);
const nameHash = sodium.crypto_generichash(sodium.crypto_generichash_BYTES, nameAsData);
const base64EncodedNameHash = fromUInt8ArrayToBase64(nameHash);
const onsResolveRequests = buildOnsResolveRequests(base64EncodedNameHash);
// we do this request with validationCount snodes
const promises = range(0, validationCount).map(async () => {
const targetNode = await getRandomSnode();
const results = await doSnodeBatchRequest(onsResolveRequests, targetNode, 4000, null);
const firstResult = results[0];
if (!firstResult || firstResult.code !== 200 || !firstResult.body) {
throw new Error('ONSresolve:Failed to resolve ONS');
}
const parsedBody = firstResult.body;
GetNetworkTime.handleTimestampOffsetFromNetwork('ons_resolve', parsedBody.t);
const intermediate = parsedBody?.result;
if (!intermediate || !intermediate?.encrypted_value) {
throw new Error('ONSresolve: no encrypted_value');
}
const hexEncodedCipherText = intermediate?.encrypted_value;
const isArgon2Based = !Boolean(intermediate?.nonce);
const ciphertext = fromHexToArray(hexEncodedCipherText);
let sessionIDAsData: Uint8Array;
let nonce: Uint8Array;
let key: Uint8Array;
if (isArgon2Based) {
// Handle old Argon2-based encryption used before HF16
const salt = new Uint8Array(sodium.crypto_pwhash_SALTBYTES);
nonce = new Uint8Array(sodium.crypto_secretbox_NONCEBYTES);
try {
const keyHex = sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
onsNameLowerCase,
salt,
sodium.crypto_pwhash_OPSLIMIT_MODERATE,
sodium.crypto_pwhash_MEMLIMIT_MODERATE,
sodium.crypto_pwhash_ALG_ARGON2ID13,
'hex'
);
if (!keyHex) {
throw new Error('ONSresolve: key invalid argon2');
}
key = fromHexToArray(keyHex);
} catch (e) {
throw new Error('ONSresolve: Hashing failed');
}
sessionIDAsData = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
if (!sessionIDAsData) {
throw new Error('ONSresolve: Decryption failed');
}
return toHex(sessionIDAsData);
}
// not argon2Based
const hexEncodedNonce = intermediate.nonce as string;
if (!hexEncodedNonce) {
throw new Error('ONSresolve: No hexEncodedNonce');
}
nonce = fromHexToArray(hexEncodedNonce);
try {
key = sodium.crypto_generichash(sodium.crypto_generichash_BYTES, nameAsData, nameHash);
if (!key) {
throw new Error('ONSresolve: Hashing failed');
}
} catch (e) {
window?.log?.warn('ONSresolve: hashing failed', e);
throw new Error('ONSresolve: Hashing failed');
}
sessionIDAsData = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
null,
ciphertext,
null,
nonce,
key
);
if (!sessionIDAsData) {
throw new Error('ONSresolve: Decryption failed');
}
return toHex(sessionIDAsData);
});
try {
// if one promise throws, we end un the catch case
const allResolvedSessionIds = await Promise.all(promises);
if (allResolvedSessionIds?.length !== validationCount) {
throw new Error('ONSresolve: Validation failed');
}
// assert all the returned session ids are the same
if (_.uniq(allResolvedSessionIds).length !== 1) {
throw new Error('ONSresolve: Validation failed');
}
return allResolvedSessionIds[0];
} catch (e) {
window.log.warn('ONSresolve: error', e);
throw e;
}
}
export const ONSResolve = { onsNameRegex, getSessionIDForOnsName };