SharedKey.js

// this file contains implementation details and is not intended to be used directly

import {
	/* eslint-disable no-unused-vars */
	PublicKey,
	/* eslint-enable no-unused-vars */
	SharedKey256
} from './CryptoTypes.js';
import tweetnacl from './impl/external/tweetnacl-nacl-fast-symbol.js';
import { hkdf } from '@noble/hashes/hkdf';
import { sha256 } from '@noble/hashes/sha256';

// order matches order of exported methods
const tweetnacl_lowlevel = (/** @type {any} */ (tweetnacl)).lowlevel;
const {
	gf, neq25519, pack, unpackneg
} = tweetnacl_lowlevel;

// publicKey is canonical if the y coordinate is smaller than 2^255 - 19
// note: this version is based on server version and should be constant-time
// note 2: don't touch it, you'll break it
const isCanonicalKey = publicKey => {
	const buffer = publicKey.bytes;
	let a = (buffer[31] & 0x7F) ^ 0x7F;
	for (let i = 30; 0 < i; --i)
		a |= buffer[i] ^ 0xFF;

	a = (a - 1) >>> 8;

	const b = (0xED - 1 - buffer[0]) >>> 8;
	return 0 !== 1 - (a & b & 1);
};

const isInMainSubgroup = point => {
	const { scalarmult, L } = tweetnacl_lowlevel;
	const result = [gf(), gf(), gf(), gf()];
	// multiply by group order
	scalarmult(result, point, L);

	// check if result is neutral element
	const gf0 = gf();
	const areEqual = neq25519(result[1], result[2]);
	const isZero = neq25519(gf0, result[0]);

	// yes, this is supposed to be  bit OR
	return 0 === (areEqual | isZero);
};

/**
 * Creates a shared secret factory given a hash function.
 * @param {object} hasher Hash object to use.
 * @returns {function(Uint8Array, PublicKey): Uint8Array} Creates a shared secret from a raw private key and public key.
 */
const deriveSharedSecretFactory = hasher => (privateKeyBytes, otherPublicKey) => {
	const { scalarmult, Z } = tweetnacl_lowlevel;
	const point = [gf(), gf(), gf(), gf()];

	if (!isCanonicalKey(otherPublicKey) || 0 !== unpackneg(point, otherPublicKey.bytes) || !isInMainSubgroup(point))
		throw Error('invalid point');

	// negate point == negate X coordinate and 't'
	Z(point[0], gf(), point[0]);
	Z(point[3], gf(), point[3]);

	const scalar = new Uint8Array(64);

	tweetnacl.lowlevel.crypto_hash(scalar, privateKeyBytes, 32, hasher);
	scalar[0] &= 248;
	scalar[31] &= 127;
	scalar[31] |= 64;

	const result = [gf(), gf(), gf(), gf()];
	scalarmult(result, point, scalar);

	const sharedSecret = new Uint8Array(32);
	pack(sharedSecret, result);
	return sharedSecret;
};

/**
 * Creates a shared key factory given a tag and a hash function.
 * @param {string} info Tag used in HKDF algorithm.
 * @param {object} hasher Hash object to use.
 * @returns {function(Uint8Array, PublicKey): SharedKey256} Creates a shared key from a raw private key and public key.
 */
const deriveSharedKeyFactory = (info, hasher) => {
	const deriveSharedSecret = deriveSharedSecretFactory(hasher);
	return (privateKeyBytes, otherPublicKey) => {
		const sharedSecret = deriveSharedSecret(privateKeyBytes, otherPublicKey);
		return new SharedKey256(hkdf(sha256, sharedSecret, undefined, info, 32));
	};
};

export {
	deriveSharedSecretFactory,
	deriveSharedKeyFactory
};