UNPKG

cripta

Version:

A layer of encryption a little too judicious

233 lines (187 loc) 6.13 kB
import crypto from 'node:crypto' import { Config } from './config' // import { INDEX_TYPE, TYPE_INDEX, TYPE, AcceptedValue } from './types' import { TYPE, AcceptedValue } from './types' import { UrlSafeBase64 } from '@/utils/base64' import { sha256 } from '@/hash' export class Factory { private separator: string = '\x1f' private encoding: BufferEncoding = 'utf8' public config: Config public lastConfig: Config | null = null constructor(config?: Record<string, any> | Config) { this.config = new Config() this.setConfig(config || new Config()) } public same(): this { this.config.entropy = 0 // $this->config->seed = hexdec(md5($this->config->key ?? '')) return this.setConfig(this.config) } public unique(): this { this.config.entropy = 4 return this.setConfig(this.config) } // TODO: review it.. public setConfig(config: Record<string, any> | Config): this { if (!(config instanceof Config)) config = new Config(config) this.config = Object.assign(new Config(), config) return this } public onceConfig(config: Record<string, any> | Config): this { if (!(config instanceof Config)) config = new Config(config) this.lastConfig = this.config this.config = Object.assign(new Config(), config) return this } public onceKey(secret: string): this { this.lastConfig = Object.assign(new Config(), this.config) this.config.key = secret return this } private maybeRestoreConfig(): this { if (this.lastConfig !== null) { this.config = Object.assign(new Config(), this.lastConfig) this.lastConfig = null } return this } private validateConfig(): void { const key = this.config.key if (typeof key !== 'string' || !key.trim()) throw new Error('Secret key is required for use cripta.') } public encode(data: AcceptedValue): string { this.validateConfig() const entropy = this.config.getInt('entropy') const entropyBytes = entropy ? crypto.randomBytes(entropy) : Buffer.from('') const encoded = this.baseEncode( Buffer.concat([ Buffer.from(this.cipher( this.toText(data), this.forgeKey(this.config.key as string, entropyBytes.toString('binary')) ), 'binary'), entropyBytes ]) ) this.maybeRestoreConfig() return encoded } public decode(data: string): AcceptedValue { this.validateConfig() const binary = this.baseDecode(data) const eLength = this.config.getInt('entropy') const entropyBytes = binary.slice(-eLength) const cipherBytes = binary.slice(0, -eLength || undefined) const decoded = this.convert(this.cipher( cipherBytes.toString(), this.forgeKey(this.config.key as string, entropyBytes.toString()) )) this.maybeRestoreConfig() return decoded } private convert(value: string): AcceptedValue { const separatorIndex = value.indexOf(this.separator) if (separatorIndex !== -1) { const typeStr = value.substring(0, separatorIndex) const typeNum = parseInt(typeStr, 10) const data = value.substring(separatorIndex + 1) switch (typeNum) { case TYPE.null: return null case TYPE.boolean: return data === 'true' || data === '1' case TYPE.string: return data case TYPE.int: return Number(data) case TYPE.float: return parseFloat(data) case TYPE.bigint: return '' // ??? case TYPE.array: return '' // ??? case TYPE.object: return '' // ??? case TYPE.symbol: return '' // ??? case TYPE.undefined: return '' // ??? } } try { return JSON.parse(value) } catch (e) {} return value } private toText(value: AcceptedValue): string { const type = typeof value if (value === null) { return `${TYPE.null}${this.separator}` } else if (type === 'boolean') { return `${TYPE.boolean}${this.separator}${value ? 'true' : 'false'}` } else if (type === 'string') { return `${TYPE.string}${this.separator}${value}` } else if (type === 'number') { if (Number.isInteger(value)) { return `${TYPE.int}${this.separator}${value}` } else { return `${TYPE.float}${this.separator}${value}` } } else if (Array.isArray(value) || type === 'object') { return JSON.stringify(value) } return '' } private cipher(str: string, key: string = ''): string { if (!str || !key) return '' const strBuffer = Buffer.from(str, this.encoding) const keyBuffer = Buffer.from(key, this.encoding) const result = Buffer.alloc(strBuffer.length) const dataLength = strBuffer.length - 1 const keyLength = keyBuffer.length - 1 for (let i = dataLength; i >= 0; i--) result[dataLength - i] = strBuffer[i] ^ keyBuffer[i % (keyLength + 1)] return result.toString(this.encoding) } private maybeUseAlphabet(data: string, from?: string, to?: string): string { if (!data || typeof data !== 'string' || !from || !to || from === to) return data return this.strtr(data, from, to) } private strtr(str: string, from: string, to: string): string { let result = '' for (let i = 0; i < str.length; i++) { const char = str[i] const index = from.indexOf(char) if (index !== -1 && index < to.length) { result += to[index] } else { result += char } } return result } private forgeKey(secret: string, entropy: string = ''): string { if (!entropy.trim()) return secret return sha256(secret + entropy, 'binary').toString() } public baseEncode(data: string | Buffer): string { return this.maybeUseAlphabet( UrlSafeBase64.encode(data), this.config.baseAlphabet, this.config.alphabet ) } public baseDecode(data: string): string { return UrlSafeBase64.decode(this.maybeUseAlphabet( data, this.config.alphabet, this.config.baseAlphabet )) } }