426 lines
13 KiB
TypeScript
426 lines
13 KiB
TypeScript
// @ts-nocheck
|
|
import CryptoES from '../deps/node_modules/crypto-es/lib/index.js'
|
|
import JSEncrypt from '../deps/node_modules/jsencrypt/lib/index.js'
|
|
|
|
function ensureSupportedCodec(codec: string): void {
|
|
const value = codec.toLowerCase()
|
|
if (value == 'utf8' || value == 'hex' || value == 'base64' || value == 'base64url' || value == 'latin1' || value == 'utf16') {
|
|
return
|
|
}
|
|
throw new Error('不支持的编码类型')
|
|
}
|
|
|
|
function ensureDigestKind(kind: string): string {
|
|
const value = kind.toLowerCase()
|
|
if (value == 'md5' || value == 'sha1' || value == 'sha256' || value == 'sha512') {
|
|
return value
|
|
}
|
|
throw new Error('不支持的摘要算法')
|
|
}
|
|
|
|
function ensureMacKind(kind: string): string {
|
|
const value = kind.toLowerCase()
|
|
if (value == 'sha1' || value == 'sha256' || value == 'sha512') {
|
|
return value
|
|
}
|
|
throw new Error('不支持的HMAC算法')
|
|
}
|
|
|
|
function ensureAesMode(mode: string): any {
|
|
const value = mode.toUpperCase()
|
|
if (value == 'CBC') {
|
|
return CryptoES.mode.CBC
|
|
}
|
|
if (value == 'CFB') {
|
|
return CryptoES.mode.CFB
|
|
}
|
|
if (value == 'CTR') {
|
|
return CryptoES.mode.CTR
|
|
}
|
|
if (value == 'CTRGLADMAN') {
|
|
return CryptoES.mode.CTRGladman
|
|
}
|
|
if (value == 'ECB') {
|
|
return CryptoES.mode.ECB
|
|
}
|
|
if (value == 'OFB') {
|
|
return CryptoES.mode.OFB
|
|
}
|
|
throw new Error('不支持的AES模式')
|
|
}
|
|
|
|
function ensureAesPadding(padding: string): any {
|
|
const value = padding.toUpperCase()
|
|
if (value == 'PKCS7') {
|
|
return CryptoES.pad.Pkcs7
|
|
}
|
|
if (value == 'ANSI_X923') {
|
|
return CryptoES.pad.AnsiX923
|
|
}
|
|
if (value == 'ISO_10126') {
|
|
return CryptoES.pad.Iso10126
|
|
}
|
|
if (value == 'ISO_97971') {
|
|
return CryptoES.pad.Iso97971
|
|
}
|
|
if (value == 'NONE') {
|
|
return CryptoES.pad.NoPadding
|
|
}
|
|
if (value == 'ZERO') {
|
|
return CryptoES.pad.ZeroPadding
|
|
}
|
|
throw new Error('不支持的AES填充')
|
|
}
|
|
|
|
function ensureTripleDesMode(mode: string): any {
|
|
const value = mode.toUpperCase()
|
|
if (value == 'CBC') {
|
|
return CryptoES.mode.CBC
|
|
}
|
|
if (value == 'ECB') {
|
|
return CryptoES.mode.ECB
|
|
}
|
|
throw new Error('不支持的3DES模式')
|
|
}
|
|
|
|
function ensureDesMode(mode: string): any {
|
|
const value = mode.toUpperCase()
|
|
if (value == 'CBC') {
|
|
return CryptoES.mode.CBC
|
|
}
|
|
if (value == 'CFB') {
|
|
return CryptoES.mode.CFB
|
|
}
|
|
if (value == 'CTR') {
|
|
return CryptoES.mode.CTR
|
|
}
|
|
if (value == 'CTRGLADMAN') {
|
|
return CryptoES.mode.CTRGladman
|
|
}
|
|
if (value == 'ECB') {
|
|
return CryptoES.mode.ECB
|
|
}
|
|
if (value == 'OFB') {
|
|
return CryptoES.mode.OFB
|
|
}
|
|
throw new Error('不支持的DES模式')
|
|
}
|
|
|
|
function inferKeyLength(sourceLength: number, hint: number | null): number {
|
|
if (hint != null) {
|
|
if (hint == 16 || hint == 24 || hint == 32) {
|
|
return hint
|
|
}
|
|
if (hint == 128) {
|
|
return 16
|
|
}
|
|
if (hint == 192) {
|
|
return 24
|
|
}
|
|
if (hint == 256) {
|
|
return 32
|
|
}
|
|
throw new Error('AES密钥长度无效')
|
|
}
|
|
if (sourceLength <= 16) {
|
|
return 16
|
|
}
|
|
if (sourceLength <= 24) {
|
|
return 24
|
|
}
|
|
return 32
|
|
}
|
|
|
|
function clampBytes(source: Uint8Array, targetLength: number): Uint8Array {
|
|
const output = new Uint8Array(targetLength)
|
|
const actualLength = source.length < targetLength ? source.length : targetLength
|
|
let index = 0
|
|
while (index < actualLength) {
|
|
output[index] = source[index]
|
|
index += 1
|
|
}
|
|
return output
|
|
}
|
|
|
|
function textToUtf8Bytes(input: string): Uint8Array {
|
|
const wordArray = CryptoES.enc.Utf8.parse(input)
|
|
return wordArrayToBytes(wordArray)
|
|
}
|
|
|
|
function utf8BytesToText(input: Uint8Array): string {
|
|
return bytesToWordArray(input).toString(CryptoES.enc.Utf8)
|
|
}
|
|
|
|
function bytesToWordArray(input: Uint8Array): any {
|
|
const words: number[] = []
|
|
let index = 0
|
|
while (index < input.length) {
|
|
let word = 0
|
|
let offset = 0
|
|
while (offset < 4 && index + offset < input.length) {
|
|
word |= input[index + offset] << (24 - offset * 8)
|
|
offset += 1
|
|
}
|
|
words.push(word)
|
|
index += 4
|
|
}
|
|
return CryptoES.lib.WordArray.create(words, input.length)
|
|
}
|
|
|
|
function wordArrayToBytes(wordArray: any): Uint8Array {
|
|
const sigBytes = wordArray.sigBytes
|
|
const output = new Uint8Array(sigBytes)
|
|
const words = wordArray.words
|
|
let index = 0
|
|
while (index < sigBytes) {
|
|
const wordIndex = Math.floor(index / 4)
|
|
const byteOffset = index % 4
|
|
output[index] = (words[wordIndex] >>> (24 - byteOffset * 8)) & 0xff
|
|
index += 1
|
|
}
|
|
return output
|
|
}
|
|
|
|
function fitKeyText(secretText: string, keyLength: number | null): any {
|
|
const rawBytes = textToUtf8Bytes(secretText)
|
|
const actualLength = inferKeyLength(rawBytes.length, keyLength)
|
|
return bytesToWordArray(clampBytes(rawBytes, actualLength))
|
|
}
|
|
|
|
function fitFixedTextKey(secretText: string, targetLength: number): any {
|
|
return bytesToWordArray(clampBytes(textToUtf8Bytes(secretText), targetLength))
|
|
}
|
|
|
|
function fitKeyBytes(secretBytes: Uint8Array, keyLength: number | null): any {
|
|
const actualLength = inferKeyLength(secretBytes.length, keyLength)
|
|
return bytesToWordArray(clampBytes(secretBytes, actualLength))
|
|
}
|
|
|
|
function fitIvText(ivText: string | null, size: number): any {
|
|
const source = ivText == null ? new Uint8Array(size) : clampBytes(textToUtf8Bytes(ivText), size)
|
|
return bytesToWordArray(source)
|
|
}
|
|
|
|
function fitIvBytes(ivBytes: Uint8Array | null, size: number): any {
|
|
const source = ivBytes == null ? new Uint8Array(size) : clampBytes(ivBytes, size)
|
|
return bytesToWordArray(source)
|
|
}
|
|
|
|
function wrapCipherBytes(cipherBytes: Uint8Array): any {
|
|
return CryptoES.lib.CipherParams.create({
|
|
ciphertext: bytesToWordArray(cipherBytes)
|
|
})
|
|
}
|
|
|
|
function toBase64Url(base64Text: string): string {
|
|
return base64Text.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
}
|
|
|
|
function fromBase64Url(base64UrlText: string): string {
|
|
let output = base64UrlText.replace(/-/g, '+').replace(/_/g, '/')
|
|
while (output.length % 4 != 0) {
|
|
output += '='
|
|
}
|
|
return output
|
|
}
|
|
|
|
function base64ToHex(base64Text: string): string {
|
|
return CryptoES.enc.Base64.parse(base64Text).toString(CryptoES.enc.Hex)
|
|
}
|
|
|
|
function hexToBase64(hexText: string): string {
|
|
return CryptoES.enc.Hex.parse(hexText).toString(CryptoES.enc.Base64)
|
|
}
|
|
|
|
export function encodeTextPortable(codec: string, plainText: string): string {
|
|
ensureSupportedCodec(codec)
|
|
const value = codec.toLowerCase()
|
|
if (value == 'utf8') {
|
|
return plainText
|
|
}
|
|
const word = CryptoES.enc.Utf8.parse(plainText)
|
|
if (value == 'hex') {
|
|
return CryptoES.enc.Hex.stringify(word)
|
|
}
|
|
if (value == 'base64') {
|
|
return CryptoES.enc.Base64.stringify(word)
|
|
}
|
|
if (value == 'base64url') {
|
|
return toBase64Url(CryptoES.enc.Base64.stringify(word))
|
|
}
|
|
if (value == 'latin1') {
|
|
return CryptoES.enc.Latin1.stringify(word)
|
|
}
|
|
return CryptoES.enc.Utf16.stringify(word)
|
|
}
|
|
|
|
export function decodeTextPortable(codec: string, encodedText: string): string {
|
|
ensureSupportedCodec(codec)
|
|
const value = codec.toLowerCase()
|
|
if (value == 'utf8') {
|
|
return encodedText
|
|
}
|
|
if (value == 'hex') {
|
|
return CryptoES.enc.Hex.parse(encodedText).toString(CryptoES.enc.Utf8)
|
|
}
|
|
if (value == 'base64') {
|
|
return CryptoES.enc.Base64.parse(encodedText).toString(CryptoES.enc.Utf8)
|
|
}
|
|
if (value == 'base64url') {
|
|
return CryptoES.enc.Base64.parse(fromBase64Url(encodedText)).toString(CryptoES.enc.Utf8)
|
|
}
|
|
if (value == 'latin1') {
|
|
return CryptoES.enc.Latin1.parse(encodedText).toString(CryptoES.enc.Utf8)
|
|
}
|
|
return CryptoES.enc.Utf16.parse(encodedText).toString(CryptoES.enc.Utf8)
|
|
}
|
|
|
|
export function digestTextPortable(kind: string, plainText: string): string {
|
|
const value = ensureDigestKind(kind)
|
|
if (value == 'md5') {
|
|
return CryptoES.MD5(plainText).toString()
|
|
}
|
|
if (value == 'sha1') {
|
|
return CryptoES.SHA1(plainText).toString()
|
|
}
|
|
if (value == 'sha256') {
|
|
return CryptoES.SHA256(plainText).toString()
|
|
}
|
|
return CryptoES.SHA512(plainText).toString()
|
|
}
|
|
|
|
export function signTextPortable(kind: string, secretText: string, plainText: string): string {
|
|
const value = ensureMacKind(kind)
|
|
if (value == 'sha1') {
|
|
return CryptoES.HmacSHA1(plainText, secretText).toString()
|
|
}
|
|
if (value == 'sha256') {
|
|
return CryptoES.HmacSHA256(plainText, secretText).toString()
|
|
}
|
|
return CryptoES.HmacSHA512(plainText, secretText).toString()
|
|
}
|
|
|
|
export function aesEncryptTextPortable(secretText: string, plainText: string, mode: string, padding: string, ivText: string | null, keyLength: number | null): string {
|
|
const encrypted = CryptoES.AES.encrypt(plainText, fitKeyText(secretText, keyLength), {
|
|
iv: fitIvText(ivText, 16),
|
|
mode: ensureAesMode(mode),
|
|
padding: ensureAesPadding(padding)
|
|
})
|
|
return encrypted.toString()
|
|
}
|
|
|
|
export function aesDecryptTextPortable(secretText: string, cipherText: string, mode: string, padding: string, ivText: string | null, keyLength: number | null): string {
|
|
const decrypted = CryptoES.AES.decrypt(cipherText, fitKeyText(secretText, keyLength), {
|
|
iv: fitIvText(ivText, 16),
|
|
mode: ensureAesMode(mode),
|
|
padding: ensureAesPadding(padding)
|
|
})
|
|
return decrypted.toString(CryptoES.enc.Utf8)
|
|
}
|
|
|
|
export function aesEncryptBytesPortable(secretBytes: Uint8Array, plainBytes: Uint8Array, mode: string, padding: string, ivBytes: Uint8Array | null, keyLength: number | null): Uint8Array {
|
|
const encrypted = CryptoES.AES.encrypt(bytesToWordArray(plainBytes), fitKeyBytes(secretBytes, keyLength), {
|
|
iv: fitIvBytes(ivBytes, 16),
|
|
mode: ensureAesMode(mode),
|
|
padding: ensureAesPadding(padding)
|
|
})
|
|
return wordArrayToBytes(encrypted.ciphertext)
|
|
}
|
|
|
|
export function aesDecryptBytesPortable(secretBytes: Uint8Array, cipherBytes: Uint8Array, mode: string, padding: string, ivBytes: Uint8Array | null, keyLength: number | null): Uint8Array {
|
|
const decrypted = CryptoES.AES.decrypt(wrapCipherBytes(cipherBytes), fitKeyBytes(secretBytes, keyLength), {
|
|
iv: fitIvBytes(ivBytes, 16),
|
|
mode: ensureAesMode(mode),
|
|
padding: ensureAesPadding(padding)
|
|
})
|
|
return wordArrayToBytes(decrypted)
|
|
}
|
|
|
|
export function tripleDesEncryptTextPortable(secretText: string, plainText: string, mode: string, ivText: string | null): string {
|
|
const encrypted = CryptoES.TripleDES.encrypt(plainText, fitKeyText(secretText, 24), {
|
|
iv: fitIvText(ivText, 8),
|
|
mode: ensureTripleDesMode(mode),
|
|
padding: CryptoES.pad.Pkcs7
|
|
})
|
|
return encrypted.toString()
|
|
}
|
|
|
|
export function tripleDesDecryptTextPortable(secretText: string, cipherText: string, mode: string, ivText: string | null): string {
|
|
const decrypted = CryptoES.TripleDES.decrypt(cipherText, fitKeyText(secretText, 24), {
|
|
iv: fitIvText(ivText, 8),
|
|
mode: ensureTripleDesMode(mode),
|
|
padding: CryptoES.pad.Pkcs7
|
|
})
|
|
return decrypted.toString(CryptoES.enc.Utf8)
|
|
}
|
|
|
|
export function desEncryptTextPortable(secretText: string, plainText: string, mode: string, padding: string, ivText: string | null): string {
|
|
const encrypted = CryptoES.DES.encrypt(plainText, fitFixedTextKey(secretText, 8), {
|
|
iv: fitIvText(ivText, 8),
|
|
mode: ensureDesMode(mode),
|
|
padding: ensureAesPadding(padding)
|
|
})
|
|
return encrypted.toString()
|
|
}
|
|
|
|
export function desDecryptTextPortable(secretText: string, cipherText: string, mode: string, padding: string, ivText: string | null): string {
|
|
const decrypted = CryptoES.DES.decrypt(cipherText, fitFixedTextKey(secretText, 8), {
|
|
iv: fitIvText(ivText, 8),
|
|
mode: ensureDesMode(mode),
|
|
padding: ensureAesPadding(padding)
|
|
})
|
|
return decrypted.toString(CryptoES.enc.Utf8)
|
|
}
|
|
|
|
export function rc4EncryptTextPortable(secretText: string, plainText: string): string {
|
|
const encrypted = CryptoES.RC4.encrypt(plainText, CryptoES.enc.Utf8.parse(secretText))
|
|
return encrypted.ciphertext.toString(CryptoES.enc.Hex)
|
|
}
|
|
|
|
export function rc4DecryptTextPortable(secretText: string, cipherHexText: string): string {
|
|
const decrypted = CryptoES.RC4.decrypt(wrapCipherBytes(wordArrayToBytes(CryptoES.enc.Hex.parse(cipherHexText))), CryptoES.enc.Utf8.parse(secretText))
|
|
return decrypted.toString(CryptoES.enc.Utf8)
|
|
}
|
|
|
|
export function createRsaPairPortable(keySize: number): { publicKey: string, privateKey: string } {
|
|
const engine = new JSEncrypt({ default_key_size: keySize.toString() })
|
|
engine.getKey()
|
|
return {
|
|
publicKey: engine.getPublicKey(),
|
|
privateKey: engine.getPrivateKey()
|
|
}
|
|
}
|
|
|
|
export function rsaEncryptTextPortable(publicKey: string, plainText: string, outputKind: string | null): string {
|
|
const engine = new JSEncrypt()
|
|
engine.setPublicKey(publicKey)
|
|
const encrypted = engine.encrypt(plainText)
|
|
if (encrypted == null || encrypted == false) {
|
|
throw new Error('RSA加密失败')
|
|
}
|
|
return outputKind == 'hex' ? base64ToHex(encrypted as string) : encrypted as string
|
|
}
|
|
|
|
export function rsaDecryptTextPortable(privateKey: string, cipherText: string, outputKind: string | null): string {
|
|
const engine = new JSEncrypt()
|
|
engine.setPrivateKey(privateKey)
|
|
const source = outputKind == 'hex' ? hexToBase64(cipherText) : cipherText
|
|
const decrypted = engine.decrypt(source)
|
|
if (decrypted == null || decrypted == false) {
|
|
throw new Error('RSA解密失败')
|
|
}
|
|
return decrypted as string
|
|
}
|
|
|
|
export function bytesToHexPortable(input: Uint8Array): string {
|
|
return bytesToWordArray(input).toString(CryptoES.enc.Hex)
|
|
}
|
|
|
|
export function utf8ToBytesPortable(input: string): Uint8Array {
|
|
return textToUtf8Bytes(input)
|
|
}
|
|
|
|
export function bytesToUtf8Portable(input: Uint8Array): string {
|
|
return utf8BytesToText(input)
|
|
}
|