mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-05 04:08:02 +02:00
b9c3f2f0dd
Remove manual GeneratedPluginRegistrant.registerWith() call since super.configureFlutterEngine() already handles this automatically.
350 lines
7.7 KiB
Go
350 lines
7.7 KiB
Go
// Package gobackend provides Storage and Credentials API for extension runtime
|
|
package gobackend
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
// ==================== Storage API ====================
|
|
|
|
func (r *ExtensionRuntime) getStoragePath() string {
|
|
return filepath.Join(r.dataDir, "storage.json")
|
|
}
|
|
|
|
func (r *ExtensionRuntime) loadStorage() (map[string]interface{}, error) {
|
|
storagePath := r.getStoragePath()
|
|
data, err := os.ReadFile(storagePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return make(map[string]interface{}), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var storage map[string]interface{}
|
|
if err := json.Unmarshal(data, &storage); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return storage, nil
|
|
}
|
|
|
|
func (r *ExtensionRuntime) saveStorage(storage map[string]interface{}) error {
|
|
storagePath := r.getStoragePath()
|
|
data, err := json.MarshalIndent(storage, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(storagePath, data, 0644)
|
|
}
|
|
|
|
func (r *ExtensionRuntime) storageGet(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 1 {
|
|
return goja.Undefined()
|
|
}
|
|
|
|
key := call.Arguments[0].String()
|
|
|
|
storage, err := r.loadStorage()
|
|
if err != nil {
|
|
GoLog("[Extension:%s] Storage load error: %v\n", r.extensionID, err)
|
|
return goja.Undefined()
|
|
}
|
|
|
|
value, exists := storage[key]
|
|
if !exists {
|
|
if len(call.Arguments) > 1 {
|
|
return call.Arguments[1]
|
|
}
|
|
return goja.Undefined()
|
|
}
|
|
|
|
return r.vm.ToValue(value)
|
|
}
|
|
|
|
func (r *ExtensionRuntime) storageSet(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 2 {
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
key := call.Arguments[0].String()
|
|
value := call.Arguments[1].Export()
|
|
|
|
storage, err := r.loadStorage()
|
|
if err != nil {
|
|
GoLog("[Extension:%s] Storage load error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
storage[key] = value
|
|
|
|
if err := r.saveStorage(storage); err != nil {
|
|
GoLog("[Extension:%s] Storage save error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
return r.vm.ToValue(true)
|
|
}
|
|
|
|
func (r *ExtensionRuntime) storageRemove(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 1 {
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
key := call.Arguments[0].String()
|
|
|
|
storage, err := r.loadStorage()
|
|
if err != nil {
|
|
GoLog("[Extension:%s] Storage load error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
delete(storage, key)
|
|
|
|
if err := r.saveStorage(storage); err != nil {
|
|
GoLog("[Extension:%s] Storage save error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
return r.vm.ToValue(true)
|
|
}
|
|
|
|
func (r *ExtensionRuntime) getCredentialsPath() string {
|
|
return filepath.Join(r.dataDir, ".credentials.enc")
|
|
}
|
|
|
|
func (r *ExtensionRuntime) getSaltPath() string {
|
|
return filepath.Join(r.dataDir, ".cred_salt")
|
|
}
|
|
|
|
func (r *ExtensionRuntime) getOrCreateSalt() ([]byte, error) {
|
|
saltPath := r.getSaltPath()
|
|
|
|
salt, err := os.ReadFile(saltPath)
|
|
if err == nil && len(salt) == 32 {
|
|
return salt, nil
|
|
}
|
|
|
|
salt = make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
|
return nil, fmt.Errorf("failed to generate salt: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(saltPath, salt, 0600); err != nil {
|
|
return nil, fmt.Errorf("failed to save salt: %w", err)
|
|
}
|
|
|
|
return salt, nil
|
|
}
|
|
|
|
func (r *ExtensionRuntime) getEncryptionKey() ([]byte, error) {
|
|
salt, err := r.getOrCreateSalt()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
combined := append([]byte(r.extensionID), salt...)
|
|
hash := sha256.Sum256(combined)
|
|
return hash[:], nil
|
|
}
|
|
|
|
func (r *ExtensionRuntime) loadCredentials() (map[string]interface{}, error) {
|
|
credPath := r.getCredentialsPath()
|
|
data, err := os.ReadFile(credPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return make(map[string]interface{}), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
key, err := r.getEncryptionKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get encryption key: %w", err)
|
|
}
|
|
decrypted, err := decryptAES(data, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt credentials: %w", err)
|
|
}
|
|
|
|
var creds map[string]interface{}
|
|
if err := json.Unmarshal(decrypted, &creds); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return creds, nil
|
|
}
|
|
|
|
func (r *ExtensionRuntime) saveCredentials(creds map[string]interface{}) error {
|
|
data, err := json.Marshal(creds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key, err := r.getEncryptionKey()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get encryption key: %w", err)
|
|
}
|
|
encrypted, err := encryptAES(data, key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to encrypt credentials: %w", err)
|
|
}
|
|
|
|
credPath := r.getCredentialsPath()
|
|
return os.WriteFile(credPath, encrypted, 0600)
|
|
}
|
|
|
|
func (r *ExtensionRuntime) credentialsStore(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 2 {
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": "key and value are required",
|
|
})
|
|
}
|
|
|
|
key := call.Arguments[0].String()
|
|
value := call.Arguments[1].Export()
|
|
|
|
creds, err := r.loadCredentials()
|
|
if err != nil {
|
|
GoLog("[Extension:%s] Credentials load error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
creds[key] = value
|
|
|
|
if err := r.saveCredentials(creds); err != nil {
|
|
GoLog("[Extension:%s] Credentials save error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
return r.vm.ToValue(map[string]interface{}{
|
|
"success": true,
|
|
})
|
|
}
|
|
|
|
func (r *ExtensionRuntime) credentialsGet(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 1 {
|
|
return goja.Undefined()
|
|
}
|
|
|
|
key := call.Arguments[0].String()
|
|
|
|
creds, err := r.loadCredentials()
|
|
if err != nil {
|
|
GoLog("[Extension:%s] Credentials load error: %v\n", r.extensionID, err)
|
|
return goja.Undefined()
|
|
}
|
|
|
|
value, exists := creds[key]
|
|
if !exists {
|
|
if len(call.Arguments) > 1 {
|
|
return call.Arguments[1]
|
|
}
|
|
return goja.Undefined()
|
|
}
|
|
|
|
return r.vm.ToValue(value)
|
|
}
|
|
|
|
func (r *ExtensionRuntime) credentialsRemove(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 1 {
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
key := call.Arguments[0].String()
|
|
|
|
creds, err := r.loadCredentials()
|
|
if err != nil {
|
|
GoLog("[Extension:%s] Credentials load error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
delete(creds, key)
|
|
|
|
if err := r.saveCredentials(creds); err != nil {
|
|
GoLog("[Extension:%s] Credentials save error: %v\n", r.extensionID, err)
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
return r.vm.ToValue(true)
|
|
}
|
|
|
|
func (r *ExtensionRuntime) credentialsHas(call goja.FunctionCall) goja.Value {
|
|
if len(call.Arguments) < 1 {
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
key := call.Arguments[0].String()
|
|
|
|
creds, err := r.loadCredentials()
|
|
if err != nil {
|
|
return r.vm.ToValue(false)
|
|
}
|
|
|
|
_, exists := creds[key]
|
|
return r.vm.ToValue(exists)
|
|
}
|
|
|
|
func encryptAES(plaintext []byte, key []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
|
return ciphertext, nil
|
|
}
|
|
|
|
func decryptAES(ciphertext []byte, key []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonceSize := gcm.NonceSize()
|
|
if len(ciphertext) < nonceSize {
|
|
return nil, fmt.Errorf("ciphertext too short")
|
|
}
|
|
|
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|