diff --git a/indexedDb/init.go b/indexedDb/init.go index 95ae98a0ae3ce85da85e559dd59a4a8775f1d284..271300b4bafcec519a1b15ea05518fb0cfae5d6a 100644 --- a/indexedDb/init.go +++ b/indexedDb/init.go @@ -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 } diff --git a/main.go b/main.go index 4a2f6f6617c02a84750ffbb7a4bef5bb035426ac..7c5f570cf47b27cad75d3e65ba1465bd0590667e 100644 --- a/main.go +++ b/main.go @@ -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)) diff --git a/storage/indexedDbEncryptionTrack_test.go b/storage/indexedDbEncryptionTrack_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ec754f87a940e14f6802e347f86f44fa437f4ff9 --- /dev/null +++ b/storage/indexedDbEncryptionTrack_test.go @@ -0,0 +1,40 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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) + } +} diff --git a/storage/indexedDbList.go b/storage/indexedDbList.go new file mode 100644 index 0000000000000000000000000000000000000000..a736984f0ef742d96e7a63238ea6ebfbdc25d9f9 --- /dev/null +++ b/storage/indexedDbList.go @@ -0,0 +1,53 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 +} diff --git a/storage/indexedDbList_test.go b/storage/indexedDbList_test.go new file mode 100644 index 0000000000000000000000000000000000000000..76270fce099c03b1774c49ce17736b8baeefea31 --- /dev/null +++ b/storage/indexedDbList_test.go @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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) + } +} diff --git a/storage/localStorage.go b/storage/localStorage.go index 2f2329209ca951c5f1dcc4e063dfde9ba9edc7f4..b0fcf33e96d095832bfafec2ec92ae7d4b63a64f 100644 --- a/storage/localStorage.go +++ b/storage/localStorage.go @@ -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 diff --git a/storage/localStorage_test.go b/storage/localStorage_test.go index 155bd7c4c32a558f05ea14989f18c1cb83a38051..6e96814e7594a8cb5a25b536fdfdb61692ec5a2e 100644 --- a/storage/localStorage_test.go +++ b/storage/localStorage_test.go @@ -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) { diff --git a/storage/purge.go b/storage/purge.go new file mode 100644 index 0000000000000000000000000000000000000000..ed0b12e40ab0ff4884da8dff8dacf9e5a59279dd --- /dev/null +++ b/storage/purge.go @@ -0,0 +1,81 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 +} diff --git a/wasm/follow.go b/wasm/follow.go index 4b343314206a28f5139b9d464fd4e965c11ea8a0..ed98bfbf4e54b92105f92cde0618e31cb674be77 100644 --- a/wasm/follow.go +++ b/wasm/follow.go @@ -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 }