@foxt/js-srp
Version:
js-srp modified to add support for the SRP implementation used by Apple's iCloud.com
308 lines (281 loc) • 7.31 kB
text/typescript
/**
* Returns the length of the bigint in bits.
*
* @param {bigint} n bigint to check
* @return {number} count of bits needed to store bigint
*/
export function bitLength(n: bigint): number {
return n.toString(2).length;
}
/**
* Returns the length of the bigint in bytes. Rounds up to the nearest byte.
*
* @param {bigint} n bigint to check
* @return {number} count of bytes needed to store bigint
*/
export function byteLength(n: bigint): number {
return ((bitLength(n) + 7) / 8) | 0;
}
/**
* Deserializes a buffer of bytes into a bigint.
*
* @param {Uint8Array} buf buffer containing a serialized bigint
* @return {bigint} deserialized value parsed from buf
*/
export function bigintFromBytes(buf: Uint8Array): bigint {
let ret = 0n;
for (const i of buf.values()) {
ret = (ret << 8n) + BigInt(i);
}
return ret;
}
/**
* Serializes a bigint into a buffer of bytes.
*
* @param {bigint} v value to serialize
* @return {Uint8Array} serialized form of v
*/
export function bytesFromBigint(v: bigint): Uint8Array {
const bytes = new Uint8Array(byteLength(v));
for (let i = bytes.length - 1; v > 0; i--, v >>= 8n) {
bytes[i] = Number(v & 0xffn);
}
return bytes;
}
/**
* Returns cryptographically-safe random bytes into a buffer.
*
* @param {number} numBytes number of bytes
* @return {Uint8Array} buffer containing random bytes
*/
export function randomBytes(numBytes: number): Uint8Array {
if (numBytes < 1) {
throw new RangeError("numBytes must be >= 1");
}
const bytes = new Uint8Array(numBytes);
crypto.getRandomValues(bytes);
return bytes;
}
/**
* Returns the smallest positive value in the multiplicative group of integers
* modulo n that is congruent to a.
*
* @param {bigint} a value to find congruent value of
* @param {bigint} n modulo of multiplicative group
* @return {bigint} smallest positive congruent value of a in integers modulo n
*/
function toZn(a: bigint, n: bigint): bigint {
if (n < 1n) {
throw new RangeError("n must be > 0");
}
const aZn = a % n;
return aZn < 0n ? aZn + n : aZn;
}
/**
* Solves for values g, x, y, such that g = gcd(a, b) and g = ax + by.
*
* @param {bigint} a
* @param {bigint} b
* @return {{g: bigint, x: bigint, y: bigint }}
*/
function eGcd(
a: bigint,
b: bigint
): {
g: bigint;
x: bigint;
y: bigint;
} {
if (a < 1n || b < 1n) {
throw new RangeError("a and b must be > 0");
}
let x = 0n;
let y = 1n;
let u = 1n;
let v = 0n;
while (a !== 0n) {
const q = b / a;
const r = b % a;
const m = x - u * q;
const n = y - v * q;
b = a;
a = r;
x = u;
y = v;
u = m;
v = n;
}
return { g: b, x, y };
}
/**
* Calculates the modular inverse of a in the multiplicative group of integers
* modulo n.
*
* @param {bigint} a
* @param {bigint} n
* @return {bigint}
*/
function modInv(a: bigint, n: bigint): bigint {
const egcd = eGcd(toZn(a, n), n);
if (egcd.g !== 1n) {
throw new RangeError();
} else {
return toZn(egcd.x, n);
}
}
/**
* Calculates the value of x ^ y % m efficiently.
*
* @param {bigint} x
* @param {bigint} y
* @param {bigint} m
* @return {bigint}
*/
export function modPow(x: bigint, y: bigint, m: bigint): bigint {
if (m < 1n) {
throw new RangeError("n must be > 0");
} else if (m === 1n) {
return 0n;
}
x = toZn(x, m);
if (y < 0n) {
return modInv(modPow(x, y >= 0 ? y : -y, m), m);
}
let r = 1n;
while (y > 0) {
if (y % 2n === 1n) {
r = (r * x) % m;
}
y = y / 2n;
x = x ** 2n % m;
}
return r;
}
/**
* Concatenates multiple buffers into one new buffer.
*
* @param {Uint8Array[]} a buffers to concatenate
* @return {Uint8Array} a new buffer containing the concatenated contents
*/
export function concatBytes(...a: Uint8Array[]): Uint8Array {
let length = 0;
for (const b of a) {
length += b.byteLength;
}
const buf = new Uint8Array(length);
let offset = 0;
for (const b of a) {
buf.set(b, offset);
offset += b.byteLength;
}
return buf;
}
/**
* XORs two equal-size byte arrays together.
*
* @param {Uint8Array[]} a buffers to concatenate
* @return {Uint8Array} a new buffer containing the concatenated contents
*/
export function xorBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
if (a.length !== b.length) {
throw new Error('xorBytes: buffers must be same length');
}
const length = a.length;
const result = new Uint8Array(length);
for (let i = 0; i < length; i++) {
result[i] = a[i]! ^ b[i]!;
}
return result;
}
/**
* Encodes a buffer into a hexadecimal string.
*
* @param {Uint8Array} buffer buffer to encode
* @return {string} hex-encoded form of buffer
*/
export function toHex(buffer: Uint8Array): string {
return [...buffer].map((x) => x.toString(16).padStart(2, "0")).join("");
}
/**
* Decodes a hexadecimal string into a new buffer.
*
* @param {string} str hexadecimal string to decode
* @return {Uint8Array} buffer of bytes decoded from str
*/
export function fromHex(str: string): Uint8Array {
return Uint8Array.from(
str.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? []
);
}
/**
* Compares two buffers with constant-time execution.
*
* @param {Uint8Array} a first buffer to compare
* @param {Uint8Array} b second buffer to compare
* @return {boolean} true if a == b, otherwise false
*/
export function constantTimeCompare(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) {
return false;
}
const len = a.length;
let out = 0;
for (let i = 0; i < len; i++) {
out |= a[i]! ^ b[i]!;
}
return out === 0;
}
/**
* Enumeration of hash types. This is a subset of the Go hash enumeration, with
* only algorithms supported by WebCrypto.
*/
export enum Hash {
SHA1 = 3,
SHA256 = 5,
SHA384 = 6,
SHA512 = 7,
}
/**
* Returns the result of applying a hash to the given buffer.
*
* @param {Hash} hash algorithm to use
* @param {BufferSource} data data to hash
* @return {Promise<ArrayBuffer>} digest
*/
export function hash(hash: Hash, data: ArrayBuffer): Promise<ArrayBuffer> {
switch (hash) {
case Hash.SHA1:
return crypto.subtle.digest("SHA-1", data);
case Hash.SHA256:
return crypto.subtle.digest("SHA-256", data);
case Hash.SHA384:
return crypto.subtle.digest("SHA-384", data);
case Hash.SHA512:
return crypto.subtle.digest("SHA-512", data);
}
}
export async function hashInterleave(h: Hash, data: ArrayBuffer): Promise<ArrayBuffer> {
let copy = new Uint8Array(data, 0, data.byteLength);
for (var i = 0; i < copy.length; i++) {
if (copy[i] !== 0) {
if ((data.byteLength - i) % 2 === 1) i++;
copy = new Uint8Array(data, i, data.byteLength - i);
break;
}
}
const halfl = copy.length / 2;
const even = new Uint8Array(halfl);
const odd = new Uint8Array(halfl);
for (let i = 0; i < copy.length; i++) {
even[i] = copy[i * 2]!;
odd[i] = copy[i * 2 + 1]!;
}
const hash1 = new Uint8Array(await hash(h, even));
const hash2 = new Uint8Array(await hash(h, odd));
const result = new Uint8Array(hash1.byteLength * 2);
for (let i = 0; i < hash1.byteLength; i++) {
result[i * 2] = hash1[i]!;
result[i * 2 + 1] = hash2[i]!;
}
return result;
}