Files
seanime-docker/seanime-2.9.10/internal/goja/goja_bindings/crypto.go
2025-09-20 14:08:38 +01:00

331 lines
9.1 KiB
Go

package goja_bindings
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"github.com/dop251/goja"
)
type wordArray struct {
vm *goja.Runtime
iv []byte
data []byte
}
func BindCrypto(vm *goja.Runtime) error {
err := vm.Set("CryptoJS", map[string]interface{}{
"AES": map[string]interface{}{
"encrypt": cryptoAESEncryptFunc(vm),
"decrypt": cryptoAESDecryptFunc(vm),
},
"enc": map[string]interface{}{
"Utf8": map[string]interface{}{
"parse": cryptoEncUtf8ParseFunc(vm),
"stringify": cryptoEncUtf8StringifyFunc(vm),
},
"Base64": map[string]interface{}{
"parse": cryptoEncBase64ParseFunc(vm),
"stringify": cryptoEncBase64StringifyFunc(vm),
},
"Hex": map[string]interface{}{
"parse": cryptoEncHexParseFunc(vm),
"stringify": cryptoEncHexStringifyFunc(vm),
},
"Latin1": map[string]interface{}{
"parse": cryptoEncLatin1ParseFunc(vm),
"stringify": cryptoEncLatin1StringifyFunc(vm),
},
"Utf16": map[string]interface{}{
"parse": cryptoEncUtf16ParseFunc(vm),
"stringify": cryptoEncUtf16StringifyFunc(vm),
},
"Utf16LE": map[string]interface{}{
"parse": cryptoEncUtf16LEParseFunc(vm),
"stringify": cryptoEncUtf16LEStringifyFunc(vm),
},
},
})
if err != nil {
return err
}
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func newWordArrayGojaValue(vm *goja.Runtime, data []byte, iv []byte) goja.Value {
wa := &wordArray{
vm: vm,
iv: iv,
data: data,
}
// WordArray // Utf8
// WordArray.toString(): string // Uses Base64.stringify
// WordArray.toString(encoder: Encoder): string
obj := vm.NewObject()
obj.Prototype().Set("toString", wa.toStringFunc)
obj.Set("toString", wa.toStringFunc)
obj.Set("iv", iv)
return obj
}
func (wa *wordArray) toStringFunc(call goja.FunctionCall) goja.Value {
if len(call.Arguments) == 0 {
return wa.vm.ToValue(base64Stringify(wa.data))
}
encoder, ok := call.Argument(0).Export().(map[string]interface{})
if !ok {
panic(wa.vm.ToValue("TypeError: encoder parameter must be a CryptoJS.enc object"))
}
var ret string
if f, ok := encoder["stringify"]; ok {
if stringify, ok := f.(func(functionCall goja.FunctionCall) goja.Value); ok {
ret = stringify(goja.FunctionCall{Arguments: []goja.Value{wa.vm.ToValue(wa.data)}}).String()
} else {
panic(wa.vm.ToValue("TypeError: encoder.stringify must be a function"))
}
} else {
ret = string(wa.data)
}
return wa.vm.ToValue(ret)
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CryptoJS.AES.encrypt(message: string, key: string, cfg?: { iv: ArrayBuffer }): WordArray
func cryptoAESEncryptFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) (ret goja.Value) {
if len(call.Arguments) < 2 {
panic(vm.ToValue("TypeError: AES.encrypt requires at least 2 arguments"))
}
message := call.Argument(0).String()
var keyBytes []byte
switch call.Argument(1).Export().(type) {
case string:
key := call.Argument(1).String()
keyBytes = adjustKeyLength([]byte(key))
case []byte:
keyBytes = call.Argument(1).Export().([]byte)
keyBytes = adjustKeyLength(keyBytes)
default:
panic(vm.ToValue("TypeError: key parameter must be a string or an ArrayBuffer"))
}
usedRandomIV := false
// Check if IV is provided
var ivBytes []byte
if len(call.Arguments) > 2 {
cfg := call.Argument(2).Export().(map[string]interface{})
var ok bool
iv, ok := cfg["iv"].([]byte)
if !ok {
panic(vm.ToValue("TypeError: iv parameter must be an ArrayBuffer"))
}
ivBytes = iv
if len(ivBytes) != aes.BlockSize {
panic(vm.ToValue("TypeError: IV length must be equal to block size (16 bytes for AES)"))
}
} else {
// Generate a random IV
ivBytes = make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, ivBytes); err != nil {
panic(vm.ToValue(fmt.Sprintf("Failed to generate IV: %v", err)))
}
usedRandomIV = true
}
defer func() {
if r := recover(); r != nil {
ret = vm.ToValue(fmt.Sprintf("Encryption failed: %v", r))
}
}()
// Encrypt the message
encryptedMessage := encryptAES(vm, message, keyBytes, ivBytes)
if usedRandomIV {
// Prepend the IV to the encrypted message
encryptedMessage = append(ivBytes, encryptedMessage...)
}
return newWordArrayGojaValue(vm, encryptedMessage, ivBytes)
}
}
// CryptoJS.AES.decrypt(encryptedMessage: string | WordArray, key: string, cfg?: { iv: ArrayBuffer }): WordArray
func cryptoAESDecryptFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) (ret goja.Value) {
if len(call.Arguments) < 2 {
panic(vm.ToValue("TypeError: AES.decrypt requires at least 2 arguments"))
}
// Can be string or WordArray
// If WordArray, String() will call WordArray.toString() which will return the base64 encoded string
encryptedMessage := call.Argument(0).String()
var keyBytes []byte
var originalPassword []byte
switch call.Argument(1).Export().(type) {
case string:
key := call.Argument(1).String()
originalPassword = []byte(key)
keyBytes = adjustKeyLength([]byte(key))
case []byte:
keyBytes = call.Argument(1).Export().([]byte)
originalPassword = keyBytes
keyBytes = adjustKeyLength(keyBytes)
default:
panic(vm.ToValue("TypeError: key parameter must be a string or an ArrayBuffer"))
}
var ivBytes []byte
var cipherText []byte
// If IV is provided in the third argument
if len(call.Arguments) > 2 {
cfg := call.Argument(2).Export().(map[string]interface{})
var ok bool
iv, ok := cfg["iv"].([]byte)
if !ok {
panic(vm.ToValue("TypeError: iv parameter must be an ArrayBuffer"))
}
ivBytes = iv
if len(ivBytes) != aes.BlockSize {
panic(vm.ToValue("TypeError: IV length must be equal to block size (16 bytes for AES)"))
}
var err error
decodedMessage, err := base64.StdEncoding.DecodeString(encryptedMessage)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("Failed to decode ciphertext: %v", err)))
}
cipherText = decodedMessage
} else {
// Decode the base64 encoded string
decodedMessage, err := base64.StdEncoding.DecodeString(encryptedMessage)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("Failed to decode ciphertext: %v", err)))
}
// Check if openssl
if len(decodedMessage) >= 16 && string(decodedMessage[:8]) == "Salted__" {
salt := decodedMessage[8:16]
cipherText = decodedMessage[16:]
derivedKey, derivedIV := evpBytesToKey(originalPassword, salt, 32, aes.BlockSize)
keyBytes = derivedKey
ivBytes = derivedIV
} else {
// Extract the IV from the beginning of the message
ivBytes = decodedMessage[:aes.BlockSize]
cipherText = decodedMessage[aes.BlockSize:]
}
}
// Decrypt the message
decrypted := decryptAES(vm, cipherText, keyBytes, ivBytes)
return newWordArrayGojaValue(vm, decrypted, ivBytes)
}
}
// Adjusts the key length to match AES key length requirements (16, 24, or 32 bytes).
// If the key length is not 16, 24, or 32 bytes, it is hashed using SHA-256 and truncated to 32 bytes (AES-256).
func adjustKeyLength(keyBytes []byte) []byte {
switch len(keyBytes) {
case 16, 24, 32:
// Valid AES key lengths: 16 bytes (AES-128), 24 bytes (AES-192), 32 bytes (AES-256)
return keyBytes
default:
// Hash the key to 32 bytes (AES-256)
hash := sha256.Sum256(keyBytes)
return hash[:]
}
}
func encryptAES(vm *goja.Runtime, message string, key []byte, iv []byte) (ret []byte) {
defer func() {
if r := recover(); r != nil {
ret = nil
}
}()
block, err := aes.NewCipher(key)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("%v", err)))
}
messageBytes := []byte(message)
messageBytes = pkcs7Padding(messageBytes, aes.BlockSize)
cipherText := make([]byte, len(messageBytes))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText, messageBytes)
return cipherText
}
func decryptAES(vm *goja.Runtime, cipherText []byte, key []byte, iv []byte) (ret []byte) {
defer func() {
if r := recover(); r != nil {
ret = nil
}
}()
block, err := aes.NewCipher(key)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("%v", err)))
}
plainText := make([]byte, len(cipherText))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(plainText, cipherText)
plainText = pkcs7Trimming(plainText)
return plainText
}
func pkcs7Padding(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padText...)
}
func pkcs7Trimming(data []byte) []byte {
length := len(data)
up := int(data[length-1])
return data[:(length - up)]
}
func evpBytesToKey(password []byte, salt []byte, keyLen, ivLen int) ([]byte, []byte) {
d := make([]byte, 0)
dI := make([]byte, 0)
for len(d) < (keyLen + ivLen) {
h := md5.New()
h.Write(dI)
h.Write(password)
h.Write(salt)
dI = h.Sum(nil)
d = append(d, dI...)
}
return d[:keyLen], d[keyLen : keyLen+ivLen]
}