253 lines
6.1 KiB
TypeScript
253 lines
6.1 KiB
TypeScript
/* eslint-disable camelcase */
|
|
import { base64_variants, from_base64, from_hex, from_string, to_base64, to_hex, to_string } from './libsodium'
|
|
import { Buffer } from 'buffer'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
const SN_BASE64_VARIANT = base64_variants.ORIGINAL
|
|
|
|
/**
|
|
* Libsodium's to_* functions take either a Buffer or String, but do not take raw buffers,
|
|
* as may be returned by WebCrypto API.
|
|
*/
|
|
|
|
declare global {
|
|
interface Document {
|
|
documentMode?: string
|
|
}
|
|
interface Window {
|
|
msCrypto?: Crypto
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns `window` if available, or `global` if supported in environment.
|
|
*/
|
|
export function getGlobalScope(): Window & typeof globalThis {
|
|
return window
|
|
}
|
|
|
|
/**
|
|
* Determines whether we are in an Internet Explorer or Edge environment
|
|
* @access public
|
|
*/
|
|
export function ieOrEdge(): boolean {
|
|
return (typeof document !== 'undefined' && !!document.documentMode) || /Edge/.test(navigator.userAgent)
|
|
}
|
|
|
|
/**
|
|
* Returns true if WebCrypto is available
|
|
* @access public
|
|
*/
|
|
export function isWebCryptoAvailable(): boolean {
|
|
return !ieOrEdge() && getGlobalScope().crypto && !!getGlobalScope().crypto.subtle
|
|
}
|
|
|
|
/**
|
|
* Returns the WebCrypto instance
|
|
* @access public
|
|
*/
|
|
export function getSubtleCrypto(): SubtleCrypto {
|
|
if (!getGlobalScope().crypto) {
|
|
throw Error('Could not obtain SubtleCrypto instance')
|
|
}
|
|
|
|
return getGlobalScope().crypto.subtle
|
|
}
|
|
|
|
/**
|
|
* Generates a UUID syncronously
|
|
* @access public
|
|
*/
|
|
export function generateUUID(): string {
|
|
return uuidv4()
|
|
}
|
|
|
|
/**
|
|
* Converts a plain string into an ArrayBuffer
|
|
* @param {string} string - A plain string
|
|
*/
|
|
export function stringToArrayBuffer(string: string): Uint8Array {
|
|
return from_string(string)
|
|
}
|
|
|
|
/**
|
|
* Converts an ArrayBuffer into a plain string
|
|
* @param {ArrayBuffer} arrayBuffer
|
|
*/
|
|
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
|
|
return to_string(arrayBuffer as Uint8Array)
|
|
}
|
|
|
|
/**
|
|
* Converts an ArrayBuffer into a hex string
|
|
* @param arrayBuffer
|
|
*/
|
|
export function arrayBufferToHexString(arrayBuffer: ArrayBuffer): string {
|
|
return to_hex(Buffer.from(arrayBuffer))
|
|
}
|
|
|
|
/**
|
|
* Converts a hex string into an ArrayBuffer
|
|
* @access public
|
|
* @param hex - A hex string
|
|
*/
|
|
export function hexStringToArrayBuffer(hex: string): Uint8Array {
|
|
return from_hex(hex)
|
|
}
|
|
|
|
/**
|
|
* Converts a base64 string into an ArrayBuffer
|
|
* @param base64 - A base64 string
|
|
*/
|
|
export function base64ToArrayBuffer(base64: string): Uint8Array {
|
|
return from_base64(base64, SN_BASE64_VARIANT)
|
|
}
|
|
|
|
/**
|
|
* Converts an ArrayBuffer into a base64 string
|
|
* @param buffer
|
|
*/
|
|
export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {
|
|
return to_base64(Buffer.from(arrayBuffer), SN_BASE64_VARIANT)
|
|
}
|
|
|
|
/**
|
|
* Converts a hex string into a base64 string
|
|
* @param hex - A hex string
|
|
*/
|
|
export function hexToBase64(hex: string): string {
|
|
return to_base64(from_hex(hex), SN_BASE64_VARIANT)
|
|
}
|
|
|
|
/**
|
|
* Converts a base64 string into a hex string
|
|
* @param base64 - A base64 string
|
|
*/
|
|
export function base64ToHex(base64: string): string {
|
|
return to_hex(from_base64(base64, SN_BASE64_VARIANT))
|
|
}
|
|
|
|
/**
|
|
* Converts a plain string into base64
|
|
* @param text - A plain string
|
|
* @returns A base64 encoded string
|
|
*/
|
|
export function base64Encode(text: string): string {
|
|
return to_base64(text, SN_BASE64_VARIANT)
|
|
}
|
|
|
|
/**
|
|
* Converts a plain string into base64 url safe
|
|
* @param text - A plain string
|
|
* @returns A base64 url safe encoded string
|
|
*/
|
|
export function base64URLEncode(text: string): string {
|
|
return to_base64(text, base64_variants.URLSAFE_NO_PADDING)
|
|
}
|
|
|
|
/**
|
|
* Converts a base64 string into a plain string
|
|
* @param base64String - A base64 encoded string
|
|
* @returns A plain string
|
|
*/
|
|
export function base64Decode(base64String: string): string {
|
|
return to_string(from_base64(base64String, SN_BASE64_VARIANT))
|
|
}
|
|
|
|
const RFC4648 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
|
|
export function base32Encode(input: ArrayBuffer): string {
|
|
const length = input.byteLength
|
|
const buffer = new Uint8Array(input)
|
|
|
|
let bitIdx = 0
|
|
let currentVal = 0
|
|
let output = ''
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
currentVal = (currentVal << 8) | buffer[i]
|
|
bitIdx += 8
|
|
|
|
while (bitIdx >= 5) {
|
|
output += RFC4648[(currentVal >>> (bitIdx - 5)) & 31]
|
|
bitIdx -= 5
|
|
}
|
|
}
|
|
|
|
if (bitIdx > 0) {
|
|
output += RFC4648[(currentVal << (5 - bitIdx)) & 31]
|
|
}
|
|
|
|
while (output.length % 8 > 0) {
|
|
output += '='
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
export function base32Decode(b32Input: string): ArrayBuffer {
|
|
const input = b32Input.toUpperCase().replace(/=+$/, '')
|
|
|
|
for (let i = 0; i < input.length; i++) {
|
|
if (!RFC4648.includes(input[i])) {
|
|
throw new Error(`Invalid RFC4648 char ${input[i]} at index ${i}`)
|
|
}
|
|
}
|
|
|
|
const output = new Uint8Array(((input.length * 5) / 8) | 0)
|
|
|
|
let outIdx = 0
|
|
let bitIdx = 0
|
|
let currentVal = 0
|
|
|
|
for (let i = 0; i < input.length; i++) {
|
|
currentVal = (currentVal << 5) | RFC4648.indexOf(input[i])
|
|
bitIdx += 5
|
|
|
|
if (bitIdx >= 8) {
|
|
output[outIdx++] = (currentVal >>> (bitIdx - 8)) & 255
|
|
bitIdx -= 8
|
|
}
|
|
}
|
|
|
|
return output.buffer
|
|
}
|
|
|
|
/**
|
|
* Truncate HMAC-SHA1 calculated value for HOTP code generation
|
|
*/
|
|
export function truncateOTP(hsBuffer: ArrayBuffer): number {
|
|
const hs = new Uint8Array(hsBuffer)
|
|
// First we take the last byte of our generated HS and extract last 4 bits out of it.
|
|
// This will be our _offset_, a number between 0 and 15.
|
|
const offset = hs[19] & 0b1111
|
|
|
|
// Next we take 4 bytes out of the HS, starting at the offset
|
|
const P = ((hs[offset] & 0x7f) << 24) | (hs[offset + 1] << 16) | (hs[offset + 2] << 8) | hs[offset + 3]
|
|
|
|
// Finally, convert it into a binary string representation
|
|
const pString = P.toString(2)
|
|
|
|
const Snum = parseInt(pString, 2)
|
|
|
|
return Snum
|
|
}
|
|
|
|
/**
|
|
* Pad HOTP counter with leading zeros producing an 8 byte array
|
|
*/
|
|
export function padStart(counter: number): ArrayBuffer {
|
|
const buffer = new ArrayBuffer(8)
|
|
const bView = new DataView(buffer)
|
|
|
|
const byteString = '0'.repeat(64)
|
|
const bCounter = (byteString + counter.toString(2)).slice(-64)
|
|
|
|
for (let byte = 0; byte < 64; byte += 8) {
|
|
const byteValue = parseInt(bCounter.slice(byte, byte + 8), 2)
|
|
bView.setUint8(byte / 8, byteValue)
|
|
}
|
|
|
|
return buffer
|
|
}
|