feat: add sncrypto client side packages
This commit is contained in:
252
packages/sncrypto-web/src/utils.ts
Normal file
252
packages/sncrypto-web/src/utils.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/* 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
|
||||
}
|
||||
Reference in New Issue
Block a user