Skip to content
Snippets Groups Projects
Commit 7ae90588 authored by Jono Wenger's avatar Jono Wenger
Browse files

XX-4272 / Purge

parent 46812ef5
No related branches found
No related tags found
2 merge requests!60Revert "Fail a test to be sure it works",!18XX-4272 / Purge
......@@ -186,5 +186,12 @@ func v1Upgrade(db *idb.Database) error {
return err
}
// Get the database name and save it to storage
if databaseName, err := db.Name(); err != nil {
return err
} else if err = storage.StoreIndexedDb(databaseName); err != nil {
return err
}
return nil
}
......@@ -44,6 +44,9 @@ func main() {
js.FuncOf(storage.ChangeExternalPassword))
js.Global().Set("VerifyPassword", js.FuncOf(storage.VerifyPassword))
// storage/purge.go
js.Global().Set("Purge", js.FuncOf(storage.Purge))
// utils/array.go
js.Global().Set("Uint8ArrayToBase64", js.FuncOf(utils.Uint8ArrayToBase64))
js.Global().Set("Base64ToUint8Array", js.FuncOf(utils.Base64ToUint8Array))
......
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation //
// //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file. //
////////////////////////////////////////////////////////////////////////////////
//go:build js && wasm
package storage
import (
"testing"
)
// Tests that StoreIndexedDbEncryptionStatus stores the initial encryption value
// and return that value on subsequent checks.
func TestStoreIndexedDbEncryptionStatus(t *testing.T) {
databaseName := "databaseA"
encrypted, err := StoreIndexedDbEncryptionStatus(databaseName, true)
if err != nil {
t.Errorf("Failed to store/get encryption status: %+v", err)
}
if encrypted != true {
t.Errorf("Incorrect encryption values.\nexpected: %t\nreceived: %t",
true, encrypted)
}
encrypted, err = StoreIndexedDbEncryptionStatus(databaseName, false)
if err != nil {
t.Errorf("Failed to store/get encryption status: %+v", err)
}
if encrypted != true {
t.Errorf("Incorrect encryption values.\nexpected: %t\nreceived: %t",
true, encrypted)
}
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation //
// //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file. //
////////////////////////////////////////////////////////////////////////////////
//go:build js && wasm
package storage
import (
"encoding/json"
"github.com/pkg/errors"
"os"
)
const indexedDbListKey = "xxDkWasmIndexedDbList"
// GetIndexedDbList returns the list of stored indexedDb databases.
func GetIndexedDbList() (map[string]struct{}, error) {
list := make(map[string]struct{})
listBytes, err := GetLocalStorage().GetItem(indexedDbListKey)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
} else if err == nil {
err = json.Unmarshal(listBytes, &list)
if err != nil {
return nil, err
}
}
return list, nil
}
// StoreIndexedDb saved the indexedDb database name to storage.
func StoreIndexedDb(databaseName string) error {
list, err := GetIndexedDbList()
if err != nil {
return err
}
list[databaseName] = struct{}{}
listBytes, err := json.Marshal(list)
if err != nil {
return err
}
GetLocalStorage().SetItem(indexedDbListKey, listBytes)
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation //
// //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file. //
////////////////////////////////////////////////////////////////////////////////
//go:build js && wasm
package storage
import (
"reflect"
"testing"
)
// Tests that three indexedDb database names stored with StoreIndexedDb are
// retrieved with GetIndexedDbList.
func TestStoreIndexedDb_GetIndexedDbList(t *testing.T) {
expected := map[string]struct{}{"db1": {}, "db2": {}, "db3": {}}
for name := range expected {
err := StoreIndexedDb(name)
if err != nil {
t.Errorf("Failed to store database name %q: %+v", name, err)
}
}
list, err := GetIndexedDbList()
if err != nil {
t.Errorf("Failed to get database list: %+v", err)
}
if !reflect.DeepEqual(expected, list) {
t.Errorf("Did not get expected list.\nexpected: %s\nreceived: %s",
expected, list)
}
}
......@@ -12,9 +12,16 @@ package storage
import (
"encoding/base64"
"os"
"strings"
"syscall/js"
)
// localStorageWasmPrefix is prefixed to every keyName saved to local storage by
// LocalStorage. It allows the identifications and deletion of keys only created
// by this WASM binary while ignoring keys made by other scripts on the same
// page.
const localStorageWasmPrefix = "xxdkWasmStorage/"
// LocalStorage contains the js.Value representation of localStorage.
type LocalStorage struct {
// The Javascript value containing the localStorage object
......@@ -43,7 +50,7 @@ func GetLocalStorage() *LocalStorage {
// - Documentation:
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem
func (ls *LocalStorage) GetItem(keyName string) ([]byte, error) {
keyValue := ls.getItem(keyName)
keyValue := ls.getItem(localStorageWasmPrefix + keyName)
if keyValue.IsNull() {
return nil, os.ErrNotExist
}
......@@ -65,7 +72,7 @@ func (ls *LocalStorage) GetItem(keyName string) ([]byte, error) {
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem
func (ls *LocalStorage) SetItem(keyName string, keyValue []byte) {
encodedKeyValue := base64.StdEncoding.EncodeToString(keyValue)
ls.setItem(keyName, encodedKeyValue)
ls.setItem(localStorageWasmPrefix+keyName, encodedKeyValue)
}
// RemoveItem removes a key's value from local storage given its name. If there
......@@ -77,7 +84,7 @@ func (ls *LocalStorage) SetItem(keyName string, keyValue []byte) {
// - Documentation:
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
func (ls *LocalStorage) RemoveItem(keyName string) {
ls.removeItem(keyName)
ls.removeItem(localStorageWasmPrefix + keyName)
}
// Clear clears all the keys in storage. Underneath, it calls
......@@ -91,6 +98,38 @@ func (ls *LocalStorage) Clear() {
ls.clear()
}
// ClearPrefix clears all keys with the given prefix.
func (ls *LocalStorage) ClearPrefix(prefix string) {
// Get a copy of all key names at once
keys := js.Global().Get("Object").Call("keys", ls.v)
// Loop through each key
for i := 0; i < keys.Length(); i++ {
if v := keys.Index(i); !v.IsNull() {
keyName := strings.TrimPrefix(v.String(), localStorageWasmPrefix)
if strings.HasPrefix(keyName, prefix) {
ls.RemoveItem(keyName)
}
}
}
}
// ClearWASM clears all the keys in storage created by WASM.
func (ls *LocalStorage) ClearWASM() {
// Get a copy of all key names at once
keys := js.Global().Get("Object").Call("keys", ls.v)
// Loop through each key
for i := 0; i < keys.Length(); i++ {
if v := keys.Index(i); !v.IsNull() {
keyName := v.String()
if strings.HasPrefix(keyName, localStorageWasmPrefix) {
ls.RemoveItem(strings.TrimPrefix(keyName, localStorageWasmPrefix))
}
}
}
}
// Key returns the name of the nth key in localStorage. Return os.ErrNotExist if
// the key does not exist. The order of keys is not defined. If there is no item
// with the given key, this function does nothing. Underneath, it calls
......@@ -106,7 +145,7 @@ func (ls *LocalStorage) Key(n int) (string, error) {
return "", os.ErrNotExist
}
return keyName.String(), nil
return strings.TrimPrefix(keyName.String(), localStorageWasmPrefix), nil
}
// Length returns the number of keys in localStorage. Underneath, it accesses
......
......@@ -12,6 +12,7 @@ package storage
import (
"bytes"
"github.com/pkg/errors"
"math/rand"
"os"
"strconv"
"testing"
......@@ -84,6 +85,73 @@ func TestLocalStorage_Clear(t *testing.T) {
}
}
// Tests that LocalStorage.ClearPrefix deletes only the keys with the given
// prefix.
func TestLocalStorage_ClearPrefix(t *testing.T) {
jsStorage.clear()
prng := rand.New(rand.NewSource(11))
var yesPrefix, noPrefix []string
prefix := "keyNamePrefix/"
for i := 0; i < 10; i++ {
keyName := "keyNum" + strconv.Itoa(i)
if prng.Intn(2) == 0 {
keyName = prefix + keyName
yesPrefix = append(yesPrefix, keyName)
} else {
noPrefix = append(noPrefix, keyName)
}
jsStorage.SetItem(keyName, []byte(strconv.Itoa(i)))
}
jsStorage.ClearPrefix(prefix)
for _, keyName := range noPrefix {
if _, err := jsStorage.GetItem(keyName); err != nil {
t.Errorf("Could not get keyName %q: %+v", keyName, err)
}
}
for _, keyName := range yesPrefix {
keyValue, err := jsStorage.GetItem(keyName)
if err == nil || !errors.Is(err, os.ErrNotExist) {
t.Errorf("Found keyName %q: %q", keyName, keyValue)
}
}
}
// Tests that LocalStorage.ClearWASM deletes all the WASM keys from storage and
// does not remove any others
func TestLocalStorage_ClearWASM(t *testing.T) {
jsStorage.clear()
prng := rand.New(rand.NewSource(11))
var yesPrefix, noPrefix []string
for i := 0; i < 10; i++ {
keyName := "keyNum" + strconv.Itoa(i)
if prng.Intn(2) == 0 {
yesPrefix = append(yesPrefix, keyName)
jsStorage.SetItem(keyName, []byte(strconv.Itoa(i)))
} else {
noPrefix = append(noPrefix, keyName)
jsStorage.setItem(keyName, strconv.Itoa(i))
}
}
jsStorage.ClearWASM()
for _, keyName := range noPrefix {
if v := jsStorage.getItem(keyName); v.IsNull() {
t.Errorf("Could not get keyName %q.", keyName)
}
}
for _, keyName := range yesPrefix {
keyValue, err := jsStorage.GetItem(keyName)
if err == nil || !errors.Is(err, os.ErrNotExist) {
t.Errorf("Found keyName %q: %q", keyName, keyValue)
}
}
}
// Tests that LocalStorage.Key return all added keys when looping through all
// indexes.
func TestLocalStorage_Key(t *testing.T) {
......
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation //
// //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file. //
////////////////////////////////////////////////////////////////////////////////
//go:build js && wasm
package storage
import (
"github.com/hack-pad/go-indexeddb/idb"
"github.com/pkg/errors"
"gitlab.com/elixxir/xxdk-wasm/utils"
"sync/atomic"
"syscall/js"
)
// NumClientsRunning is an atomic that tracks the current number of Cmix
// followers that have been started. Every time one is started, this counter
// must be incremented and every time one is stopped, it must be decremented.
//
// This variable is an atomic. Only access it with atomic functions
var NumClientsRunning uint64
// Purge clears all local storage and indexedDb databases saved by this WASM
// binary. All Cmix followers must be closed and the user's password is
// required.
//
// Warning: This deletes all storage local to the webpage running this WASM.
// Only use if you want to destroy everything.
//
// Parameters:
// - args[0] - Storage directory path (string).
// - args[1] - Password used for storage (Uint8Array).
//
// Returns:
// - Throws a TypeError if the password is incorrect or if not all Cmix
// followers have been stopped.
func Purge(_ js.Value, args []js.Value) interface{} {
// Check the password
if !verifyPassword(args[1].String()) {
utils.Throw(utils.TypeError, errors.New("invalid password"))
return nil
}
// Verify all Cmix followers are stopped
if n := atomic.LoadUint64(&NumClientsRunning); n != 0 {
utils.Throw(
utils.TypeError, errors.Errorf("%d Cmix followers running", n))
return nil
}
// Get all indexedDb database names
databaseList, err := GetIndexedDbList()
if err != nil {
utils.Throw(
utils.TypeError, errors.Errorf(
"failed to get list of indexedDb database names: %+v", err))
return nil
}
// Delete each database
for dbName := range databaseList {
_, err = idb.Global().DeleteDatabase(dbName)
if err != nil {
utils.Throw(
utils.TypeError, errors.Errorf(
"failed to delete indexedDb database %q: %+v", dbName, err))
return nil
}
}
// Clear WASM local storage and EKV
ls := GetLocalStorage()
ls.ClearWASM()
ls.ClearPrefix(args[0].String())
return nil
}
......@@ -10,7 +10,9 @@
package wasm
import (
"gitlab.com/elixxir/xxdk-wasm/storage"
"gitlab.com/elixxir/xxdk-wasm/utils"
"sync/atomic"
"syscall/js"
)
......@@ -60,6 +62,8 @@ func (c *Cmix) StartNetworkFollower(_ js.Value, args []js.Value) interface{} {
return nil
}
atomic.AddUint64(&storage.NumClientsRunning, 1)
return nil
}
......@@ -77,6 +81,7 @@ func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) interface{} {
utils.Throw(utils.TypeError, err)
return nil
}
atomic.AddUint64(&storage.NumClientsRunning, ^uint64(0))
return nil
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment