331 lines
9.1 KiB
Go
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]
|
|
}
|