You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/session/apis/snode_api/onsResolve.ts

118 lines
3.9 KiB
TypeScript

import _, { range } from 'lodash';
import { isTestNet } from '../../../shared/env_vars';
import { getSodiumRenderer } from '../../crypto';
import {
fromHexToArray,
fromUInt8ArrayToBase64,
stringToUint8Array,
toHex,
} from '../../utils/String';
import { OnsResolveSubRequest } from './SnodeRequestTypes';
import { doSnodeBatchRequest } from './batchRequest';
import { GetNetworkTime } from './getNetworkTime';
import { getRandomSnode } from './snodePool';
// 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);
if (isTestNet()) {
window.log.info('OnsResolve response are not registered to anything on testnet');
}
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 ciphertext = fromHexToArray(hexEncodedCipherText);
let key: Uint8Array;
// we dropped support for argon2 based ons
const hexEncodedNonce = intermediate.nonce as string;
if (!hexEncodedNonce) {
throw new Error('ONSresolve: No hexEncodedNonce');
}
const 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');
}
const 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 };