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.
		
		
		
		
		
			
		
			
				
	
	
		
			146 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			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 = !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 };
 |