mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-04-21 19:16:01 +02:00
360 lines
9.1 KiB
Go
360 lines
9.1 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/dop251/goja"
|
|
"golang.org/x/crypto/blowfish"
|
|
)
|
|
|
|
type runtimeBlockCipherOptions struct {
|
|
Algorithm string
|
|
Mode string
|
|
Key []byte
|
|
IV []byte
|
|
InputEncoding string
|
|
OutputEncoding string
|
|
Padding string
|
|
}
|
|
|
|
func parseRuntimeOptionsArgument(call goja.FunctionCall, index int) map[string]interface{} {
|
|
if len(call.Arguments) <= index {
|
|
return nil
|
|
}
|
|
|
|
value := call.Arguments[index]
|
|
if goja.IsUndefined(value) || goja.IsNull(value) {
|
|
return nil
|
|
}
|
|
|
|
exported := value.Export()
|
|
if options, ok := exported.(map[string]interface{}); ok {
|
|
return options
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runtimeOptionString(options map[string]interface{}, key, defaultValue string) string {
|
|
if options == nil {
|
|
return defaultValue
|
|
}
|
|
raw, ok := options[key]
|
|
if !ok || raw == nil {
|
|
return defaultValue
|
|
}
|
|
switch value := raw.(type) {
|
|
case string:
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
case []byte:
|
|
if len(value) > 0 {
|
|
return string(value)
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func runtimeOptionBool(options map[string]interface{}, key string, defaultValue bool) bool {
|
|
if options == nil {
|
|
return defaultValue
|
|
}
|
|
raw, ok := options[key]
|
|
if !ok || raw == nil {
|
|
return defaultValue
|
|
}
|
|
switch value := raw.(type) {
|
|
case bool:
|
|
return value
|
|
case int:
|
|
return value != 0
|
|
case int64:
|
|
return value != 0
|
|
case float64:
|
|
return value != 0
|
|
case string:
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
case "0", "false", "no", "off":
|
|
return false
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func runtimeOptionInt64(options map[string]interface{}, key string, defaultValue int64) int64 {
|
|
if options == nil {
|
|
return defaultValue
|
|
}
|
|
raw, ok := options[key]
|
|
if !ok || raw == nil {
|
|
return defaultValue
|
|
}
|
|
switch value := raw.(type) {
|
|
case int:
|
|
return int64(value)
|
|
case int32:
|
|
return int64(value)
|
|
case int64:
|
|
return value
|
|
case float32:
|
|
return int64(value)
|
|
case float64:
|
|
return int64(value)
|
|
case string:
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return defaultValue
|
|
}
|
|
var parsed int64
|
|
if _, err := fmt.Sscanf(value, "%d", &parsed); err == nil {
|
|
return parsed
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func runtimeOptionHasKey(options map[string]interface{}, key string) bool {
|
|
if options == nil {
|
|
return false
|
|
}
|
|
_, exists := options[key]
|
|
return exists
|
|
}
|
|
|
|
func decodeRuntimeBytesString(input, encoding string) ([]byte, error) {
|
|
switch strings.ToLower(strings.TrimSpace(encoding)) {
|
|
case "", "utf8", "utf-8", "text":
|
|
return []byte(input), nil
|
|
case "base64":
|
|
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(input))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid base64 data: %w", err)
|
|
}
|
|
return decoded, nil
|
|
case "hex":
|
|
decoded, err := hex.DecodeString(strings.TrimSpace(input))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid hex data: %w", err)
|
|
}
|
|
return decoded, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported byte encoding: %s", encoding)
|
|
}
|
|
}
|
|
|
|
func decodeRuntimeBytesValue(raw interface{}, encoding string) ([]byte, error) {
|
|
switch value := raw.(type) {
|
|
case string:
|
|
return decodeRuntimeBytesString(value, encoding)
|
|
case []byte:
|
|
cloned := make([]byte, len(value))
|
|
copy(cloned, value)
|
|
return cloned, nil
|
|
case []interface{}:
|
|
decoded := make([]byte, len(value))
|
|
for i, item := range value {
|
|
switch num := item.(type) {
|
|
case int:
|
|
decoded[i] = byte(num)
|
|
case int64:
|
|
decoded[i] = byte(num)
|
|
case float64:
|
|
decoded[i] = byte(int(num))
|
|
default:
|
|
return nil, fmt.Errorf("unsupported byte array item at index %d", i)
|
|
}
|
|
}
|
|
return decoded, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported byte payload type")
|
|
}
|
|
}
|
|
|
|
func encodeRuntimeBytes(data []byte, encoding string) (string, error) {
|
|
switch strings.ToLower(strings.TrimSpace(encoding)) {
|
|
case "", "base64":
|
|
return base64.StdEncoding.EncodeToString(data), nil
|
|
case "hex":
|
|
return hex.EncodeToString(data), nil
|
|
case "utf8", "utf-8", "text":
|
|
return string(data), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported byte encoding: %s", encoding)
|
|
}
|
|
}
|
|
|
|
func parseRuntimeBlockCipherOptions(options map[string]interface{}) (*runtimeBlockCipherOptions, error) {
|
|
parsed := &runtimeBlockCipherOptions{
|
|
Algorithm: strings.ToLower(runtimeOptionString(options, "algorithm", "")),
|
|
Mode: strings.ToLower(runtimeOptionString(options, "mode", "cbc")),
|
|
InputEncoding: strings.ToLower(runtimeOptionString(options, "inputEncoding", "base64")),
|
|
OutputEncoding: strings.ToLower(runtimeOptionString(options, "outputEncoding", "base64")),
|
|
Padding: strings.ToLower(runtimeOptionString(options, "padding", "none")),
|
|
}
|
|
if parsed.Algorithm == "" {
|
|
return nil, fmt.Errorf("algorithm is required")
|
|
}
|
|
if parsed.Mode == "" {
|
|
return nil, fmt.Errorf("mode is required")
|
|
}
|
|
|
|
key, err := decodeRuntimeBytesString(runtimeOptionString(options, "key", ""), runtimeOptionString(options, "keyEncoding", "utf8"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid key: %w", err)
|
|
}
|
|
if len(key) == 0 {
|
|
return nil, fmt.Errorf("key is required")
|
|
}
|
|
parsed.Key = key
|
|
|
|
iv, err := decodeRuntimeBytesString(runtimeOptionString(options, "iv", ""), runtimeOptionString(options, "ivEncoding", "utf8"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid iv: %w", err)
|
|
}
|
|
parsed.IV = iv
|
|
return parsed, nil
|
|
}
|
|
|
|
func newRuntimeBlockCipher(options *runtimeBlockCipherOptions) (cipher.Block, error) {
|
|
switch options.Algorithm {
|
|
case "blowfish":
|
|
return blowfish.NewCipher(options.Key)
|
|
case "aes":
|
|
return aes.NewCipher(options.Key)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported block cipher algorithm: %s", options.Algorithm)
|
|
}
|
|
}
|
|
|
|
func applyPKCS7Padding(data []byte, blockSize int) []byte {
|
|
padding := blockSize - (len(data) % blockSize)
|
|
if padding == 0 {
|
|
padding = blockSize
|
|
}
|
|
out := make([]byte, len(data)+padding)
|
|
copy(out, data)
|
|
for i := len(data); i < len(out); i++ {
|
|
out[i] = byte(padding)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func removePKCS7Padding(data []byte, blockSize int) ([]byte, error) {
|
|
if len(data) == 0 || len(data)%blockSize != 0 {
|
|
return nil, fmt.Errorf("invalid padded payload length")
|
|
}
|
|
padding := int(data[len(data)-1])
|
|
if padding <= 0 || padding > blockSize || padding > len(data) {
|
|
return nil, fmt.Errorf("invalid PKCS7 padding")
|
|
}
|
|
for i := len(data) - padding; i < len(data); i++ {
|
|
if int(data[i]) != padding {
|
|
return nil, fmt.Errorf("invalid PKCS7 padding")
|
|
}
|
|
}
|
|
return data[:len(data)-padding], nil
|
|
}
|
|
|
|
func (r *extensionRuntime) transformBlockCipher(call goja.FunctionCall, decrypt bool) goja.Value {
|
|
if len(call.Arguments) < 2 {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": "data and options are required",
|
|
})
|
|
}
|
|
|
|
options := parseRuntimeOptionsArgument(call, 1)
|
|
parsedOptions, err := parseRuntimeBlockCipherOptions(options)
|
|
if err != nil {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
if parsedOptions.Mode != "cbc" {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": fmt.Sprintf("unsupported block cipher mode: %s", parsedOptions.Mode),
|
|
})
|
|
}
|
|
|
|
inputData, err := decodeRuntimeBytesValue(call.Arguments[0].Export(), parsedOptions.InputEncoding)
|
|
if err != nil {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
block, err := newRuntimeBlockCipher(parsedOptions)
|
|
if err != nil {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
if len(parsedOptions.IV) != block.BlockSize() {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": fmt.Sprintf("iv must be %d bytes for %s", block.BlockSize(), parsedOptions.Algorithm),
|
|
})
|
|
}
|
|
|
|
data := inputData
|
|
if !decrypt && parsedOptions.Padding == "pkcs7" {
|
|
data = applyPKCS7Padding(data, block.BlockSize())
|
|
}
|
|
if len(data)%block.BlockSize() != 0 {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": fmt.Sprintf("input length must be a multiple of %d bytes", block.BlockSize()),
|
|
})
|
|
}
|
|
|
|
output := make([]byte, len(data))
|
|
if decrypt {
|
|
cipher.NewCBCDecrypter(block, parsedOptions.IV).CryptBlocks(output, data)
|
|
if parsedOptions.Padding == "pkcs7" {
|
|
output, err = removePKCS7Padding(output, block.BlockSize())
|
|
if err != nil {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
cipher.NewCBCEncrypter(block, parsedOptions.IV).CryptBlocks(output, data)
|
|
}
|
|
|
|
encoded, err := encodeRuntimeBytes(output, parsedOptions.OutputEncoding)
|
|
if err != nil {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": true,
|
|
"data": encoded,
|
|
"block_size": block.BlockSize(),
|
|
})
|
|
}
|
|
|
|
func (r *extensionRuntime) encryptBlockCipher(call goja.FunctionCall) goja.Value {
|
|
return r.transformBlockCipher(call, false)
|
|
}
|
|
|
|
func (r *extensionRuntime) decryptBlockCipher(call goja.FunctionCall) goja.Value {
|
|
return r.transformBlockCipher(call, true)
|
|
}
|