feat: add sncrypto client side packages
This commit is contained in:
400
packages/sncrypto-web/src/crypto.ts
Normal file
400
packages/sncrypto-web/src/crypto.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import {
|
||||
StreamEncryptor,
|
||||
StreamDecryptor,
|
||||
SodiumConstant,
|
||||
StreamDecryptorResult,
|
||||
Base64String,
|
||||
Base64URLSafeString,
|
||||
HexString,
|
||||
PureCryptoInterface,
|
||||
Utf8String,
|
||||
timingSafeEqual,
|
||||
} from '@standardnotes/sncrypto-common'
|
||||
import * as Utils from './utils'
|
||||
import * as sodium from './libsodium'
|
||||
|
||||
enum WebCryptoAlgs {
|
||||
AesCbc = 'AES-CBC',
|
||||
Sha512 = 'SHA-512',
|
||||
Sha256 = 'SHA-256',
|
||||
Pbkdf2 = 'PBKDF2',
|
||||
Sha1 = 'SHA-1',
|
||||
Hmac = 'HMAC',
|
||||
}
|
||||
|
||||
enum WebCryptoActions {
|
||||
DeriveBits = 'deriveBits',
|
||||
Encrypt = 'encrypt',
|
||||
Decrypt = 'decrypt',
|
||||
Sign = 'sign',
|
||||
}
|
||||
|
||||
type WebCryptoParams = {
|
||||
name: string
|
||||
hash?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The web crypto class allows access to a set of cryptographic primitives available
|
||||
* in a web environment, consisting of two main sources:
|
||||
* — Built-in browser WebCrypto
|
||||
* — Libsodium.js library integration
|
||||
*/
|
||||
export class SNWebCrypto implements PureCryptoInterface {
|
||||
private ready: Promise<void> | null
|
||||
|
||||
constructor() {
|
||||
/** Functions using Libsodium must await this
|
||||
* promise before performing any library functions */
|
||||
this.ready = sodium.ready
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.ready
|
||||
}
|
||||
|
||||
deinit(): void {
|
||||
this.ready = null
|
||||
}
|
||||
|
||||
public generateUUID(): string {
|
||||
return Utils.generateUUID()
|
||||
}
|
||||
|
||||
public timingSafeEqual(a: string, b: string): boolean {
|
||||
return timingSafeEqual(a, b)
|
||||
}
|
||||
|
||||
public base64Encode(text: Utf8String): string {
|
||||
return Utils.base64Encode(text)
|
||||
}
|
||||
|
||||
public base64URLEncode(text: Utf8String): Base64URLSafeString {
|
||||
return Utils.base64URLEncode(text)
|
||||
}
|
||||
|
||||
public base64Decode(base64String: Base64String): string {
|
||||
return Utils.base64Decode(base64String)
|
||||
}
|
||||
|
||||
public async pbkdf2(
|
||||
password: Utf8String,
|
||||
salt: Utf8String,
|
||||
iterations: number,
|
||||
length: number,
|
||||
): Promise<HexString | null> {
|
||||
const keyData = await Utils.stringToArrayBuffer(password)
|
||||
const key = await this.webCryptoImportKey(keyData, WebCryptoAlgs.Pbkdf2, [WebCryptoActions.DeriveBits])
|
||||
if (!key) {
|
||||
console.error('Key is null, unable to continue')
|
||||
return null
|
||||
}
|
||||
return this.webCryptoDeriveBits(key, salt, iterations, length)
|
||||
}
|
||||
|
||||
public generateRandomKey(bits: number): string {
|
||||
const bytes = bits / 8
|
||||
const arrayBuffer = Utils.getGlobalScope().crypto.getRandomValues(new Uint8Array(bytes))
|
||||
return Utils.arrayBufferToHexString(arrayBuffer)
|
||||
}
|
||||
|
||||
public async aes256CbcEncrypt(plaintext: Utf8String, iv: HexString, key: HexString): Promise<Base64String> {
|
||||
const keyData = await Utils.hexStringToArrayBuffer(key)
|
||||
const ivData = await Utils.hexStringToArrayBuffer(iv)
|
||||
const alg = { name: WebCryptoAlgs.AesCbc, iv: ivData }
|
||||
const importedKeyData = await this.webCryptoImportKey(keyData, alg.name, [WebCryptoActions.Encrypt])
|
||||
const textData = await Utils.stringToArrayBuffer(plaintext)
|
||||
const result = await crypto.subtle.encrypt(alg, importedKeyData, textData)
|
||||
return Utils.arrayBufferToBase64(result)
|
||||
}
|
||||
|
||||
public async aes256CbcDecrypt(ciphertext: Base64String, iv: HexString, key: HexString): Promise<Utf8String | null> {
|
||||
const keyData = await Utils.hexStringToArrayBuffer(key)
|
||||
const ivData = await Utils.hexStringToArrayBuffer(iv)
|
||||
const alg = { name: WebCryptoAlgs.AesCbc, iv: ivData }
|
||||
const importedKeyData = await this.webCryptoImportKey(keyData, alg.name, [WebCryptoActions.Decrypt])
|
||||
const textData = await Utils.base64ToArrayBuffer(ciphertext)
|
||||
|
||||
try {
|
||||
const result = await crypto.subtle.decrypt(alg, importedKeyData, textData)
|
||||
|
||||
return Utils.arrayBufferToString(result)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public async hmac256(message: Utf8String, key: HexString): Promise<HexString | null> {
|
||||
const keyHexData = Utils.hexStringToArrayBuffer(key)
|
||||
const keyData = await this.webCryptoImportKey(keyHexData, WebCryptoAlgs.Hmac, [WebCryptoActions.Sign], {
|
||||
name: WebCryptoAlgs.Sha256,
|
||||
})
|
||||
const messageData = Utils.stringToArrayBuffer(message)
|
||||
const funcParams = { name: WebCryptoAlgs.Hmac }
|
||||
|
||||
try {
|
||||
const signature = await crypto.subtle.sign(funcParams, keyData, messageData)
|
||||
|
||||
return Utils.arrayBufferToHexString(signature)
|
||||
} catch (error) {
|
||||
console.error('Error computing HMAC:', error)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public async sha256(text: string): Promise<string> {
|
||||
const textData = Utils.stringToArrayBuffer(text)
|
||||
const digest = await crypto.subtle.digest(WebCryptoAlgs.Sha256, textData)
|
||||
return Utils.arrayBufferToHexString(digest)
|
||||
}
|
||||
|
||||
public async hmac1(message: Utf8String, key: HexString): Promise<HexString | null> {
|
||||
const keyHexData = await Utils.hexStringToArrayBuffer(key)
|
||||
const keyData = await this.webCryptoImportKey(keyHexData, WebCryptoAlgs.Hmac, [WebCryptoActions.Sign], {
|
||||
name: WebCryptoAlgs.Sha1,
|
||||
})
|
||||
const messageData = await Utils.stringToArrayBuffer(message)
|
||||
const funcParams = { name: WebCryptoAlgs.Hmac }
|
||||
|
||||
try {
|
||||
const signature = await crypto.subtle.sign(funcParams, keyData, messageData)
|
||||
|
||||
return Utils.arrayBufferToHexString(signature)
|
||||
} catch (error) {
|
||||
console.error('Error computing HMAC:', error)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public async unsafeSha1(text: string): Promise<string> {
|
||||
const textData = await Utils.stringToArrayBuffer(text)
|
||||
const digest = await crypto.subtle.digest(WebCryptoAlgs.Sha1, textData)
|
||||
return Utils.arrayBufferToHexString(digest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a raw string key to a WebCrypto CryptoKey object.
|
||||
* @param rawKey
|
||||
* A plain utf8 string or an array buffer
|
||||
* @param alg
|
||||
* The name of the algorithm this key will be used for (i.e 'AES-CBC' or 'HMAC')
|
||||
* @param actions
|
||||
* The actions this key will be used for (i.e 'deriveBits' or 'encrypt')
|
||||
* @param hash
|
||||
* An optional object representing the hashing function this key is intended to be
|
||||
* used for. This option is only supplied when the `alg` is HMAC.
|
||||
* @param hash.name
|
||||
* The name of the hashing function to use with HMAC.
|
||||
* @returns A WebCrypto CryptoKey object
|
||||
*/
|
||||
private async webCryptoImportKey(
|
||||
keyData: Uint8Array,
|
||||
alg: WebCryptoAlgs,
|
||||
actions: Array<WebCryptoActions>,
|
||||
hash?: WebCryptoParams,
|
||||
): Promise<CryptoKey> {
|
||||
return Utils.getSubtleCrypto().importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{
|
||||
name: alg,
|
||||
hash: hash,
|
||||
},
|
||||
false,
|
||||
actions,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs WebCrypto PBKDF2 derivation.
|
||||
* @param key - A WebCrypto CryptoKey object
|
||||
* @param length - In bits
|
||||
*/
|
||||
private async webCryptoDeriveBits(
|
||||
key: CryptoKey,
|
||||
salt: Utf8String,
|
||||
iterations: number,
|
||||
length: number,
|
||||
): Promise<HexString> {
|
||||
const params = {
|
||||
name: WebCryptoAlgs.Pbkdf2,
|
||||
salt: await Utils.stringToArrayBuffer(salt),
|
||||
iterations: iterations,
|
||||
hash: { name: WebCryptoAlgs.Sha512 },
|
||||
}
|
||||
|
||||
return Utils.getSubtleCrypto()
|
||||
.deriveBits(params, key, length)
|
||||
.then((bits) => {
|
||||
return Utils.arrayBufferToHexString(new Uint8Array(bits))
|
||||
})
|
||||
}
|
||||
|
||||
public argon2(password: Utf8String, salt: HexString, iterations: number, bytes: number, length: number): HexString {
|
||||
const result = sodium.crypto_pwhash(
|
||||
length,
|
||||
Utils.stringToArrayBuffer(password),
|
||||
Utils.hexStringToArrayBuffer(salt),
|
||||
iterations,
|
||||
bytes,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT,
|
||||
'hex',
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
public xchacha20Encrypt(
|
||||
plaintext: Utf8String,
|
||||
nonce: HexString,
|
||||
key: HexString,
|
||||
assocData: Utf8String,
|
||||
): Base64String {
|
||||
if (nonce.length !== 48) {
|
||||
throw Error('Nonce must be 24 bytes')
|
||||
}
|
||||
const arrayBuffer = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||
plaintext,
|
||||
assocData,
|
||||
null,
|
||||
Utils.hexStringToArrayBuffer(nonce),
|
||||
Utils.hexStringToArrayBuffer(key),
|
||||
)
|
||||
return Utils.arrayBufferToBase64(arrayBuffer)
|
||||
}
|
||||
|
||||
public xchacha20Decrypt(
|
||||
ciphertext: Base64String,
|
||||
nonce: HexString,
|
||||
key: HexString,
|
||||
assocData: Utf8String | Uint8Array,
|
||||
): Utf8String | null {
|
||||
if (nonce.length !== 48) {
|
||||
throw Error('Nonce must be 24 bytes')
|
||||
}
|
||||
try {
|
||||
return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
null,
|
||||
Utils.base64ToArrayBuffer(ciphertext),
|
||||
assocData,
|
||||
Utils.hexStringToArrayBuffer(nonce),
|
||||
Utils.hexStringToArrayBuffer(key),
|
||||
'text',
|
||||
)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public xchacha20StreamInitEncryptor(key: HexString): StreamEncryptor {
|
||||
const res = sodium.crypto_secretstream_xchacha20poly1305_init_push(Utils.hexStringToArrayBuffer(key))
|
||||
return {
|
||||
state: res.state,
|
||||
header: Utils.arrayBufferToBase64(res.header),
|
||||
}
|
||||
}
|
||||
|
||||
public xchacha20StreamEncryptorPush(
|
||||
encryptor: StreamEncryptor,
|
||||
plainBuffer: Uint8Array,
|
||||
assocData: Utf8String,
|
||||
tag: SodiumConstant = SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_PUSH,
|
||||
): Uint8Array {
|
||||
const encryptedBuffer = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
encryptor.state as sodium.StateAddress,
|
||||
plainBuffer,
|
||||
assocData.length > 0 ? Utils.stringToArrayBuffer(assocData) : null,
|
||||
tag,
|
||||
)
|
||||
return encryptedBuffer
|
||||
}
|
||||
|
||||
public xchacha20StreamInitDecryptor(header: Base64String, key: HexString): StreamDecryptor {
|
||||
const rawHeader = Utils.base64ToArrayBuffer(header)
|
||||
|
||||
if (rawHeader.length !== SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES) {
|
||||
throw new Error(`Header must be ${SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES} bytes long`)
|
||||
}
|
||||
|
||||
const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(rawHeader, Utils.hexStringToArrayBuffer(key))
|
||||
|
||||
return { state }
|
||||
}
|
||||
|
||||
public xchacha20StreamDecryptorPush(
|
||||
decryptor: StreamDecryptor,
|
||||
encryptedBuffer: Uint8Array,
|
||||
assocData: Utf8String,
|
||||
): StreamDecryptorResult | false {
|
||||
if (encryptedBuffer.length < SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES) {
|
||||
throw new Error('Invalid ciphertext size')
|
||||
}
|
||||
|
||||
const result = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
decryptor.state as sodium.StateAddress,
|
||||
encryptedBuffer,
|
||||
assocData.length > 0 ? Utils.stringToArrayBuffer(assocData) : null,
|
||||
)
|
||||
|
||||
if ((result as unknown) === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random secret for TOTP authentication
|
||||
*
|
||||
* RFC4226 reccomends a length of at least 160 bits = 32 b32 chars
|
||||
* https://datatracker.ietf.org/doc/html/rfc4226#section-4
|
||||
*/
|
||||
public async generateOtpSecret(): Promise<string> {
|
||||
const bits = 160
|
||||
const bytes = bits / 8
|
||||
const secretBytes = Utils.getGlobalScope().crypto.getRandomValues(new Uint8Array(bytes))
|
||||
const secret = Utils.base32Encode(secretBytes)
|
||||
return secret
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a HOTP code as per RFC4226 specification
|
||||
* using HMAC-SHA1
|
||||
* https://datatracker.ietf.org/doc/html/rfc4226
|
||||
*
|
||||
* @param secret OTP shared secret
|
||||
* @param counter HOTP counter
|
||||
* @returns HOTP auth code
|
||||
*/
|
||||
public async hotpToken(secret: string, counter: number, tokenLength = 6): Promise<string> {
|
||||
const bytes = new Uint8Array(Utils.base32Decode(secret))
|
||||
|
||||
const key = await this.webCryptoImportKey(bytes, WebCryptoAlgs.Hmac, [WebCryptoActions.Sign], {
|
||||
name: WebCryptoAlgs.Sha1,
|
||||
})
|
||||
|
||||
const counterArray = Utils.padStart(counter)
|
||||
const hs = await Utils.getSubtleCrypto().sign('HMAC', key, counterArray)
|
||||
const sNum = Utils.truncateOTP(hs)
|
||||
const padded = ('0'.repeat(tokenLength) + (sNum % 10 ** tokenLength)).slice(-tokenLength)
|
||||
|
||||
return padded
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a TOTP code as per RFC6238 specification
|
||||
* using HMAC-SHA1
|
||||
* https://datatracker.ietf.org/doc/html/rfc6238
|
||||
*
|
||||
* @param secret OTP shared secret
|
||||
* @param timestamp time specified in milliseconds since UNIX epoch
|
||||
* @param step time step specified in seconds
|
||||
* @returns TOTP auth code
|
||||
*/
|
||||
public async totpToken(secret: string, timestamp: number, tokenLength = 6, step = 30): Promise<string> {
|
||||
const time = Math.floor(timestamp / step / 1000.0)
|
||||
const token = await this.hotpToken(secret, time, tokenLength)
|
||||
return token
|
||||
}
|
||||
}
|
||||
16
packages/sncrypto-web/src/index.ts
Normal file
16
packages/sncrypto-web/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { SNWebCrypto } from './crypto'
|
||||
export {
|
||||
arrayBufferToBase64,
|
||||
arrayBufferToHexString,
|
||||
arrayBufferToString,
|
||||
base64Decode,
|
||||
base64Encode,
|
||||
base64ToArrayBuffer,
|
||||
base64ToHex,
|
||||
hexStringToArrayBuffer,
|
||||
hexToBase64,
|
||||
isWebCryptoAvailable,
|
||||
stringToArrayBuffer,
|
||||
base32Decode,
|
||||
base32Encode,
|
||||
} from './utils'
|
||||
21
packages/sncrypto-web/src/libsodium.ts
Normal file
21
packages/sncrypto-web/src/libsodium.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable camelcase */
|
||||
export {
|
||||
base64_variants,
|
||||
crypto_aead_xchacha20poly1305_ietf_decrypt,
|
||||
crypto_aead_xchacha20poly1305_ietf_encrypt,
|
||||
crypto_secretstream_xchacha20poly1305_push,
|
||||
crypto_secretstream_xchacha20poly1305_pull,
|
||||
crypto_secretstream_xchacha20poly1305_init_push,
|
||||
crypto_secretstream_xchacha20poly1305_init_pull,
|
||||
crypto_pwhash_ALG_DEFAULT,
|
||||
crypto_pwhash,
|
||||
from_base64,
|
||||
from_hex,
|
||||
from_string,
|
||||
ready,
|
||||
to_base64,
|
||||
to_hex,
|
||||
to_string,
|
||||
} from 'libsodium-wrappers'
|
||||
|
||||
export type { StateAddress } from 'libsodium-wrappers'
|
||||
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