cripta
Version:
A layer of encryption a little too judicious
233 lines (187 loc) • 6.13 kB
text/typescript
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
))
}
}