@socketsupply/socket
Version:
A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.
238 lines (197 loc) • 6.04 kB
JavaScript
/* global console */
/* eslint-disable no-fallthrough */
/**
* @module crypto
*
* Some high-level methods around the `crypto.subtle` API for getting
* random bytes and hashing.
*
* Example usage:
* ```js
* import { randomBytes } from 'socket:crypto'
* ```
*/
import { toBuffer } from './util.js'
import { Buffer } from './buffer.js'
import * as exports from './crypto.js'
const textDecoder = new TextDecoder()
/**
* @typedef {Uint8Array|Int8Array} TypedArray
*/
/**
* WebCrypto API
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto}
*/
export let webcrypto = globalThis.crypto?.webcrypto ?? globalThis.crypto
const pending = []
if (globalThis?.process?.versions?.node) {
pending.push(
import('node:crypto')
.then((module) => {
webcrypto = module.webcrypto
})
)
}
const sodium = {
ready: new Promise((resolve, reject) => {
import('./crypto/sodium.js')
.then((module) => module.default.libsodium)
.then((libsodium) => libsodium.ready.then(() => libsodium))
.then((libsodium) => Object.assign(sodium, libsodium))
.then(resolve, reject)
})
}
pending.push(sodium.ready)
/**
* A promise that resolves when all internals to be loaded/ready.
* @type {Promise}
*/
export const ready = Promise.all(pending)
/**
* libsodium API
* @see {@link https://doc.libsodium.org/}
* @see {@link https://github.com/jedisct1/libsodium.js}
*/
export { sodium }
/**
* Generate cryptographically strong random values into the `buffer`
* @param {TypedArray} buffer
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues}
* @return {TypedArray}
*/
export function getRandomValues (buffer, ...args) {
if (typeof webcrypto?.getRandomValues === 'function') {
return webcrypto?.getRandomValues(buffer, ...args)
}
if (sodium.randombytes_buf) {
const input = toBuffer(sodium.randombytes_buf(buffer.byteLength))
const output = toBuffer(buffer)
input.copy(output)
return buffer
}
console.warn('Missing implementation for globalThis.crypto.getRandomValues()')
return null
}
// so this is re-used instead of creating new one each rand64() call
const tmp = new Uint32Array(2)
/**
* Generate a random 64-bit number.
* @returns {BigInt} - A random 64-bit number.
*/
export function rand64 () {
getRandomValues(tmp)
return (BigInt(tmp[0]) << 32n) | BigInt(tmp[1])
}
/**
* Maximum total size of random bytes per page
*/
export const RANDOM_BYTES_QUOTA = 64 * 1024
/**
* Maximum total size for random bytes.
*/
export const MAX_RANDOM_BYTES = 0xFFFF_FFFF_FFFF
/**
* Maximum total amount of allocated per page of bytes (max/quota)
*/
export const MAX_RANDOM_BYTES_PAGES = MAX_RANDOM_BYTES / RANDOM_BYTES_QUOTA
// note: should it do Math.ceil() / Math.round()?
/**
* Generate `size` random bytes.
* @param {number} size - The number of bytes to generate. The size must not be larger than 2**31 - 1.
* @returns {Buffer} - A promise that resolves with an instance of socket.Buffer with random bytes.
*/
export function randomBytes (size) {
const buffers = []
if (size < 0 || size >= MAX_RANDOM_BYTES || !Number.isInteger(size)) {
throw Object.assign(new RangeError(
`The value of "size" is out of range. It must be >= 0 && <= ${MAX_RANDOM_BYTES}. ` +
`Received ${size}`
), {
code: 'ERR_OUT_OF_RANGE'
})
}
do {
const length = size > RANDOM_BYTES_QUOTA ? RANDOM_BYTES_QUOTA : size
const bytes = getRandomValues(new Int8Array(length))
buffers.push(toBuffer(bytes))
size = Math.max(0, size - RANDOM_BYTES_QUOTA)
} while (size > 0)
return Buffer.concat(buffers)
}
/**
* @param {string} algorithm - `SHA-1` | `SHA-256` | `SHA-384` | `SHA-512`
* @param {Buffer | TypedArray | DataView} message - An instance of socket.Buffer, TypedArray or Dataview.
* @returns {Promise<Buffer>} - A promise that resolves with an instance of socket.Buffer with the hash.
*/
export async function createDigest (algorithm, buf) {
return Buffer.from(await webcrypto.subtle.digest(algorithm, buf))
}
/**
* A murmur3 hash implementation based on https://github.com/jwerle/murmurhash.c
* that works on strings and `ArrayBuffer` views (typed arrays)
* @param {string|Uint8Array|ArrayBuffer} value
* @param {number=} [seed = 0]
* @return {number}
*/
export function murmur3 (value, seed = 0) {
let string = value
if (typeof string !== 'string') {
if (string instanceof ArrayBuffer) {
string = new Uint8Array(string)
}
if (ArrayBuffer.isView(string)) {
string = textDecoder.decode(string)
}
}
if (typeof string !== 'string') {
throw new TypeError(
'Expecting input to be a string, ArrayBuffer, or TypedArray'
)
}
let hash = seed
let len = string.length
const c1 = 0xcc9e2d51
const c2 = 0x1b873593
const r1 = 15
const r2 = 13
const m = 5
const n = 0xe6546b64
const remainder = len & 3
len = len - remainder
for (let i = 0; i < len; i += 4) {
let k = (
(string.charCodeAt(i) & 0xff) |
((string.charCodeAt(i + 1) & 0xff) << 8) |
((string.charCodeAt(i + 2) & 0xff) << 16) |
((string.charCodeAt(i + 3) & 0xff) << 24)
)
k = (k * c1) & 0xffffffff
k = (k << r1) | (k >>> (32 - r1))
k = (k * c2) & 0xffffffff
hash ^= k
hash = ((hash << r2) | (hash >>> (32 - r2))) * m + n
hash = hash & 0xffffffff
}
let k = 0
switch (remainder) {
case 3:
k ^= (string.charCodeAt(len + 2) & 0xff) << 16
case 2:
k ^= (string.charCodeAt(len + 1) & 0xff) << 8
case 1:
k ^= (string.charCodeAt(len) & 0xff)
k = (k * c1) & 0xffffffff
k = (k << r1) | (k >>> (32 - r1))
k = (k * c2) & 0xffffffff
hash ^= k
}
hash ^= len
hash ^= (hash >>> 16)
hash = (hash * 0x85ebca6b) & 0xffffffff
hash ^= (hash >>> 13)
hash = (hash * 0xc2b2ae35) & 0xffffffff
hash ^= (hash >>> 16)
// convert to unsigned 32-bit integer
return hash >>> 0
}
export default exports