diff --git a/Makefile b/Makefile
index c899add28240bd6c3c47544c55a47d4e9c47c8aa..1a6a142603268ae7ae7a425bd58bcb6584cdf0c1 100644
--- a/Makefile
+++ b/Makefile
@@ -15,12 +15,14 @@ update_release:
 	GOFLAGS="" go get -d gitlab.com/elixxir/client@release
 	GOFLAGS="" go get gitlab.com/elixxir/crypto@release
 	GOFLAGS="" go get gitlab.com/elixxir/primitives@release
+	GOFLAGS="" go get gitlab.com/xx_network/crypto@release
 	GOFLAGS="" go get gitlab.com/xx_network/primitives@release
 
 update_master:
 	GOFLAGS="" go get -d gitlab.com/elixxir/client@master
 	GOFLAGS="" go get gitlab.com/elixxir/crypto@master
 	GOFLAGS="" go get gitlab.com/elixxir/primitives@master
+	GOFLAGS="" go get gitlab.com/xx_network/crypto@master
 	GOFLAGS="" go get gitlab.com/xx_network/primitives@master
 
 binary:
diff --git a/creds/password.go b/creds/password.go
new file mode 100644
index 0000000000000000000000000000000000000000..c980ef33031535da61cf47fcd115e420d0d6704b
--- /dev/null
+++ b/creds/password.go
@@ -0,0 +1,314 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 creds
+
+import (
+	"crypto/cipher"
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/xx_network/crypto/csprng"
+	"golang.org/x/crypto/argon2"
+	"golang.org/x/crypto/blake2b"
+	"golang.org/x/crypto/chacha20poly1305"
+	"io"
+	"os"
+	"syscall/js"
+)
+
+// Data lengths.
+const (
+	// Length of the internal password (256-bit)
+	internalPasswordLen = 32
+
+	// keyLen is the length of the key generated
+	keyLen = chacha20poly1305.KeySize
+
+	// saltLen is the length of the salt. Recommended to be 16 bytes here:
+	// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-argon2-04#section-3.1
+	saltLen = 16
+)
+
+// Storage keys.
+const (
+	// Key used to store the encrypted internal password salt in local storage.
+	saltKey = "xxInternalPasswordSalt"
+
+	// Key used to store the encrypted internal password in local storage.
+	passwordKey = "xxEncryptedInternalPassword"
+
+	// Key used to store the argon2 parameters used to encrypted/decrypt the
+	// password.
+	argonParamsKey = "xxEncryptedInternalPasswordParams"
+)
+
+// Error messages.
+const (
+	// getInternalPassword
+	getPasswordStorageErr = "could not retrieve encrypted internal password from storage: %+v"
+	getSaltStorageErr     = "could not retrieve salt from storage: %+v"
+	getParamsStorageErr   = "could not retrieve encryption parameters from storage: %+v"
+	paramsUnmarshalErr    = "failed to unmarshal encryption parameters loaded from storage: %+v"
+	decryptPasswordErr    = "could not decrypt internal password: %+v"
+
+	// initInternalPassword
+	readInternalPasswordErr     = "could not generate internal password: %+v"
+	internalPasswordNumBytesErr = "expected %d bytes for internal password, found %d bytes"
+
+	// decryptPassword
+	readNonceLenErr        = "read %d bytes, too short to decrypt"
+	decryptWithPasswordErr = "cannot decrypt with password: %+v"
+
+	// makeSalt
+	readSaltErr     = "could not generate salt: %+v"
+	saltNumBytesErr = "expected %d bytes for salt, found %d bytes"
+)
+
+// GetOrInitJS takes a user-provided password and returns its associated 256-bit
+// internal password.
+//
+// If the internal password has not previously been created, then it is
+// generated, saved to local storage, and returned. If the internal password has
+// been previously generated, it is retrieved from local storage and returned.
+//
+// Any password saved to local storage is encrypted using the user-provided
+// password.
+//
+// Parameters:
+//  - args[0] - The user supplied password (string).
+//
+// Returns:
+//  - Internal password (Uint8Array).
+//  - Throws TypeError on failure.
+func GetOrInitJS(_ js.Value, args []js.Value) interface{} {
+	internalPassword, err := GetOrInit(args[0].String())
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(internalPassword)
+}
+
+// ChangeExternalPasswordJS allows a user to change their external password.
+//
+// Parameters:
+//  - args[0] - The user's old password (string).
+//  - args[1] - The user's new password (string).
+//
+// Returns:
+//  - Throws TypeError on failure.
+func ChangeExternalPasswordJS(_ js.Value, args []js.Value) interface{} {
+	err := ChangeExternalPassword(args[0].String(), args[1].String())
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
+
+// GetOrInit takes a user-provided password and returns its associated 256-bit
+// internal password.
+//
+// If the internal password has not previously been created, then it is
+// generated, saved to local storage, and returned. If the internal password has
+// been previously generated, it is retrieved from local storage and returned.
+//
+// Any password saved to local storage is encrypted using the user-provided
+// password.
+func GetOrInit(externalPassword string) ([]byte, error) {
+	localStorage := utils.GetLocalStorage()
+	internalPassword, err := getInternalPassword(externalPassword, localStorage)
+	if err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			rng := csprng.NewSystemRNG()
+			return initInternalPassword(
+				externalPassword, localStorage, rng, defaultParams())
+		}
+
+		return nil, err
+	}
+
+	return internalPassword, nil
+}
+
+// ChangeExternalPassword allows a user to change their external password.
+func ChangeExternalPassword(oldExternalPassword, newExternalPassword string) error {
+	localStorage := utils.GetLocalStorage()
+	internalPassword, err := getInternalPassword(oldExternalPassword, localStorage)
+	if err != nil {
+		return err
+	}
+
+	salt, err := makeSalt(csprng.NewSystemRNG())
+	if err != nil {
+		return err
+	}
+	localStorage.SetItem(saltKey, salt)
+
+	key := deriveKey(newExternalPassword, salt, defaultParams())
+
+	encryptedInternalPassword := encryptPassword(
+		internalPassword, key, csprng.NewSystemRNG())
+	localStorage.SetItem(passwordKey, encryptedInternalPassword)
+
+	return nil
+}
+
+// initInternalPassword generates a new internal password, stores an encrypted
+// version in local storage, and returns it.
+func initInternalPassword(externalPassword string,
+	localStorage *utils.LocalStorage, csprng io.Reader,
+	params argonParams) ([]byte, error) {
+	internalPassword := make([]byte, internalPasswordLen)
+
+	// Generate internal password
+	n, err := csprng.Read(internalPassword)
+	if err != nil {
+		return nil, errors.Errorf(readInternalPasswordErr, err)
+	} else if n != internalPasswordLen {
+		return nil, errors.Errorf(
+			internalPasswordNumBytesErr, internalPasswordLen, n)
+	}
+
+	// Generate and store salt
+	salt, err := makeSalt(csprng)
+	if err != nil {
+		return nil, err
+	}
+	localStorage.SetItem(saltKey, salt)
+
+	// Store argon2 parameters
+	paramsData, err := json.Marshal(params)
+	if err != nil {
+		return nil, err
+	}
+	localStorage.SetItem(argonParamsKey, paramsData)
+
+	key := deriveKey(externalPassword, salt, params)
+
+	encryptedInternalPassword := encryptPassword(internalPassword, key, csprng)
+	localStorage.SetItem(passwordKey, encryptedInternalPassword)
+
+	return internalPassword, nil
+}
+
+// getInternalPassword retrieves the internal password from local storage,
+// decrypts it, and returns it.
+func getInternalPassword(
+	externalPassword string, localStorage *utils.LocalStorage) ([]byte, error) {
+	encryptedInternalPassword, err := localStorage.GetItem(passwordKey)
+	if err != nil {
+		return nil, errors.WithMessage(err, getPasswordStorageErr)
+	}
+
+	salt, err := localStorage.GetItem(saltKey)
+	if err != nil {
+		return nil, errors.WithMessage(err, getSaltStorageErr)
+	}
+
+	paramsData, err := localStorage.GetItem(argonParamsKey)
+	if err != nil {
+		return nil, errors.WithMessage(err, getParamsStorageErr)
+	}
+
+	var params argonParams
+	err = json.Unmarshal(paramsData, &params)
+	if err != nil {
+		return nil, errors.Errorf(paramsUnmarshalErr, err)
+	}
+
+	key := deriveKey(externalPassword, salt, params)
+
+	decryptedInternalPassword, err :=
+		decryptPassword(encryptedInternalPassword, key)
+	if err != nil {
+		return nil, errors.Errorf(decryptPasswordErr, err)
+	}
+
+	return decryptedInternalPassword, nil
+}
+
+// encryptPassword encrypts the data for a shared URL using XChaCha20-Poly1305.
+func encryptPassword(data, password []byte, csprng io.Reader) []byte {
+	chaCipher := initChaCha20Poly1305(password)
+	nonce := make([]byte, chaCipher.NonceSize())
+	if _, err := io.ReadFull(csprng, nonce); err != nil {
+		jww.FATAL.Panicf("Could not generate nonce %+v", err)
+	}
+	ciphertext := chaCipher.Seal(nonce, nonce, data, nil)
+	return ciphertext
+}
+
+// decryptPassword decrypts the encrypted data from a shared URL using
+// XChaCha20-Poly1305.
+func decryptPassword(data, password []byte) ([]byte, error) {
+	chaCipher := initChaCha20Poly1305(password)
+	nonceLen := chaCipher.NonceSize()
+	if (len(data) - nonceLen) <= 0 {
+		return nil, errors.Errorf(readNonceLenErr, len(data))
+	}
+	nonce, ciphertext := data[:nonceLen], data[nonceLen:]
+	plaintext, err := chaCipher.Open(nil, nonce, ciphertext, nil)
+	if err != nil {
+		return nil, errors.Errorf(decryptWithPasswordErr, err)
+	}
+	return plaintext, nil
+}
+
+// initChaCha20Poly1305 returns a XChaCha20-Poly1305 cipher.AEAD that uses the
+// given password hashed into a 256-bit key.
+func initChaCha20Poly1305(password []byte) cipher.AEAD {
+	pwHash := blake2b.Sum256(password)
+	chaCipher, err := chacha20poly1305.NewX(pwHash[:])
+	if err != nil {
+		jww.FATAL.Panicf("Could not init XChaCha20Poly1305 mode: %+v", err)
+	}
+
+	return chaCipher
+}
+
+// argonParams contains the cost parameters used by Argon2.
+type argonParams struct {
+	Time    uint32 // Number of passes over the memory
+	Memory  uint32 // Amount of memory used in KiB
+	Threads uint8  // Number of threads used
+}
+
+// defaultParams returns the recommended general purposes parameters.
+func defaultParams() argonParams {
+	return argonParams{
+		Time:    1,
+		Memory:  64 * 1024, // ~64 MB
+		Threads: 4,
+	}
+}
+
+// deriveKey derives a key from a user supplied password and a salt via the
+// Argon2 algorithm.
+func deriveKey(password string, salt []byte, params argonParams) []byte {
+	return argon2.IDKey([]byte(password), salt,
+		params.Time, params.Memory, params.Threads, keyLen)
+}
+
+// makeSalt generates a salt of the correct length of key generation.
+func makeSalt(csprng io.Reader) ([]byte, error) {
+	b := make([]byte, saltLen)
+	size, err := csprng.Read(b)
+	if err != nil {
+		return nil, errors.Errorf(readSaltErr, err)
+	} else if size != saltLen {
+		return nil, errors.Errorf(saltNumBytesErr, saltLen, size)
+	}
+
+	return b, nil
+}
diff --git a/creds/password_test.go b/creds/password_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7bba31c24400b86a9a93dc2f693f20b443b01993
--- /dev/null
+++ b/creds/password_test.go
@@ -0,0 +1,366 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 creds
+
+import (
+	"bytes"
+	"crypto/rand"
+	"encoding/base64"
+	"fmt"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/xx_network/crypto/csprng"
+	"strings"
+	"testing"
+)
+
+// Tests that running GetOrInit twice returns the same internal password both
+// times.
+func TestGetOrInit(t *testing.T) {
+	externalPassword := "myPassword"
+	internalPassword, err := GetOrInit(externalPassword)
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	loadedInternalPassword, err := GetOrInit(externalPassword)
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	if !bytes.Equal(internalPassword, loadedInternalPassword) {
+		t.Errorf("Internal password from storage does not match original."+
+			"\nexpected: %+v\nreceived: %+v",
+			internalPassword, loadedInternalPassword)
+	}
+}
+
+func TestChangeExternalPassword(t *testing.T) {
+	oldExternalPassword := "myPassword"
+	newExternalPassword := "hunter2"
+	oldInternalPassword, err := GetOrInit(oldExternalPassword)
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	err = ChangeExternalPassword(oldExternalPassword, newExternalPassword)
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	newInternalPassword, err := GetOrInit(newExternalPassword)
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	if !bytes.Equal(oldInternalPassword, newInternalPassword) {
+		t.Errorf("Internal password was not changed in storage. Old and new "+
+			"should be different.\nold: %+v\nnew: %+v",
+			oldInternalPassword, newInternalPassword)
+	}
+
+	_, err = GetOrInit(oldExternalPassword)
+	expectedErr := strings.Split(decryptWithPasswordErr, "%")[0]
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when trying to get internal password with "+
+			"old external password.\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Tests that the internal password returned by initInternalPassword matches
+// the encrypted one saved to local storage.
+func Test_initInternalPassword(t *testing.T) {
+	externalPassword := "myPassword"
+	ls := utils.GetLocalStorage()
+	rng := csprng.NewSystemRNG()
+
+	internalPassword, err := initInternalPassword(
+		externalPassword, ls, rng, defaultParams())
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	// Attempt to retrieve encrypted internal password from storage
+	encryptedInternalPassword, err := ls.GetItem(passwordKey)
+	if err != nil {
+		t.Errorf(
+			"Failed to load encrypted internal password from storage: %+v", err)
+	}
+
+	// Attempt to retrieve salt from storage
+	salt, err := ls.GetItem(saltKey)
+	if err != nil {
+		t.Errorf("Failed to load salt from storage: %+v", err)
+	}
+
+	// Attempt to decrypt
+	key := deriveKey(externalPassword, salt, defaultParams())
+	decryptedInternalPassword, err :=
+		decryptPassword(encryptedInternalPassword, key)
+	if err != nil {
+		t.Errorf("Failed to load decrpyt internal password: %+v", err)
+	}
+
+	if !bytes.Equal(internalPassword, decryptedInternalPassword) {
+		t.Errorf("Decrypted internal password from storage does not match "+
+			"original.\nexpected: %+v\nreceived: %+v",
+			internalPassword, decryptedInternalPassword)
+	}
+}
+
+// Tests that initInternalPassword returns an error when the RNG returns an
+// error when read.
+func Test_initInternalPassword_CsprngReadError(t *testing.T) {
+	externalPassword := "myPassword"
+	ls := utils.GetLocalStorage()
+	b := bytes.NewBuffer([]byte{})
+
+	expectedErr := strings.Split(readInternalPasswordErr, "%")[0]
+
+	_, err := initInternalPassword(externalPassword, ls, b, defaultParams())
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when RNG returns a read error."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Tests that initInternalPassword returns  an error when the RNG does not
+// return enough bytes.
+func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) {
+	externalPassword := "myPassword"
+	ls := utils.GetLocalStorage()
+	b := bytes.NewBuffer(make([]byte, internalPasswordLen/2))
+
+	expectedErr := fmt.Sprintf(
+		internalPasswordNumBytesErr, internalPasswordLen, internalPasswordLen/2)
+
+	_, err := initInternalPassword(externalPassword, ls, b, defaultParams())
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when RNG does not return enough bytes."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Tests that getInternalPassword returns the internal password that is saved
+// to local storage by initInternalPassword.
+func Test_getInternalPassword(t *testing.T) {
+	externalPassword := "myPassword"
+	ls := utils.GetLocalStorage()
+	rng := csprng.NewSystemRNG()
+
+	internalPassword, err := initInternalPassword(
+		externalPassword, ls, rng, defaultParams())
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	loadedInternalPassword, err := getInternalPassword(externalPassword, ls)
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	if !bytes.Equal(internalPassword, loadedInternalPassword) {
+		t.Errorf("Internal password from storage does not match original."+
+			"\nexpected: %+v\nreceived: %+v",
+			internalPassword, loadedInternalPassword)
+	}
+}
+
+// Tests that getInternalPassword returns an error when the password cannot be
+// loaded from local storage.
+func Test_getInternalPassword_LocalStorageGetPasswordError(t *testing.T) {
+	externalPassword := "myPassword"
+	ls := utils.GetLocalStorage()
+	ls.Clear()
+
+	expectedErr := strings.Split(getPasswordStorageErr, "%")[0]
+
+	_, err := getInternalPassword(externalPassword, ls)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when password cannot be loaded from storage."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Tests that getInternalPassword returns an error when the salt cannot be
+// loaded from local storage.
+func Test_getInternalPassword_LocalStorageGetError(t *testing.T) {
+	externalPassword := "myPassword"
+	ls := utils.GetLocalStorage()
+	ls.Clear()
+	ls.SetItem(passwordKey, []byte("password"))
+
+	expectedErr := strings.Split(getSaltStorageErr, "%")[0]
+
+	_, err := getInternalPassword(externalPassword, ls)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when salt cannot be loaded from storage."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Tests that getInternalPassword returns an error when the password cannot be
+// decrypted.
+func Test_getInternalPassword_DecryptPasswordError(t *testing.T) {
+	externalPassword := "myPassword"
+	ls := utils.GetLocalStorage()
+	ls.Clear()
+	ls.SetItem(saltKey, []byte("salt"))
+	ls.SetItem(passwordKey, []byte("password"))
+	ls.SetItem(argonParamsKey, []byte(`{"Time": 1, "Memory": 65536, "Threads": 4}`))
+
+	expectedErr := strings.Split(decryptPasswordErr, "%")[0]
+
+	_, err := getInternalPassword(externalPassword, ls)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when the password is decrypted."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Smoke test of encryptPassword and decryptPassword.
+func Test_encryptPassword_decryptPassword(t *testing.T) {
+	plaintext := []byte("Hello, World!")
+	password := []byte("test_password")
+	ciphertext := encryptPassword(plaintext, password, rand.Reader)
+	decrypted, err := decryptPassword(ciphertext, password)
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+
+	for i := range plaintext {
+		if plaintext[i] != decrypted[i] {
+			t.Errorf("%b != %b", plaintext[i], decrypted[i])
+		}
+	}
+}
+
+// Tests that decryptPassword does not panic when given too little data.
+func Test_decryptPassword_ShortData(t *testing.T) {
+	// Anything under 24 should cause an error.
+	ciphertext := []byte{
+		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
+	_, err := decryptPassword(ciphertext, []byte("dummyPassword"))
+	expectedErr := fmt.Sprintf(readNonceLenErr, 24)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error on short decryption."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+
+	// Empty string shouldn't panic should cause an error.
+	ciphertext = []byte{}
+	_, err = decryptPassword(ciphertext, []byte("dummyPassword"))
+	expectedErr = fmt.Sprintf(readNonceLenErr, 0)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error on short decryption."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Tests that deriveKey returns a key of the correct length and that it is the
+// same for the same set of password and salt. Also checks that keys with the
+// same salt or passwords do not collide.
+func Test_deriveKey(t *testing.T) {
+	p := testParams()
+	salts := make([][]byte, 6)
+	passwords := make([]string, len(salts))
+	keys := make(map[string]bool, len(salts)*len(passwords))
+
+	for i := range salts {
+		prng := csprng.NewSystemRNG()
+		salt, _ := makeSalt(prng)
+		salts[i] = salt
+
+		password := make([]byte, 16)
+		_, _ = prng.Read(password)
+		passwords[i] = base64.StdEncoding.EncodeToString(password)[:16]
+	}
+
+	for _, salt := range salts {
+		for _, password := range passwords {
+			key := deriveKey(password, salt, p)
+
+			// Check that the length of the key is correct
+			if len(key) != keyLen {
+				t.Errorf("Incorrect key length.\nexpected: %d\nreceived: %d",
+					keyLen, len(key))
+			}
+
+			// Check that the same key is generated when the same password and salt
+			// are used
+			key2 := deriveKey(password, salt, p)
+
+			if !bytes.Equal(key, key2) {
+				t.Errorf("Keys with same password and salt do not match."+
+					"\nexpected: %v\nreceived: %v", key, key2)
+			}
+
+			if keys[string(key)] {
+				t.Errorf("Key already exists.")
+			}
+			keys[string(key)] = true
+		}
+	}
+}
+
+// Tests that multiple calls to makeSalt results in unique salts of the
+// specified length.
+func Test_makeSalt(t *testing.T) {
+	salts := make(map[string]bool, 50)
+	for i := 0; i < 50; i++ {
+		salt, err := makeSalt(csprng.NewSystemRNG())
+		if err != nil {
+			t.Errorf("MakeSalt returned an error: %+v", err)
+		}
+
+		if len(salt) != saltLen {
+			t.Errorf("Incorrect salt length.\nexpected: %d\nreceived: %d",
+				saltLen, len(salt))
+		}
+
+		if salts[string(salt)] {
+			t.Errorf("Salt already exists (%d).", i)
+		}
+		salts[string(salt)] = true
+	}
+}
+
+// Tests that makeSalt returns an error when the RNG returns an error when read.
+func Test_makeSalt_ReadError(t *testing.T) {
+	b := bytes.NewBuffer([]byte{})
+
+	expectedErr := strings.Split(readSaltErr, "%")[0]
+	_, err := makeSalt(b)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when RNG returns a read error."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Tests that makeSalt returns an error when the RNG does not return enough
+// bytes.
+func Test_makeSalt_ReadNumBytesError(t *testing.T) {
+	b := bytes.NewBuffer(make([]byte, saltLen/2))
+
+	expectedErr := fmt.Sprintf(saltNumBytesErr, saltLen, saltLen/2)
+	_, err := makeSalt(b)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("Unexpected error when RNG does not return enough bytes."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// testParams returns params used in testing that are quick.
+func testParams() argonParams {
+	return argonParams{
+		Time:    1,
+		Memory:  1,
+		Threads: 1,
+	}
+}
diff --git a/go.mod b/go.mod
index 2c3a02b54abc153fc5b86b8962a4134602b18297..6f3b57ee3f8e2288dba8ce8035076bb4fada07b8 100644
--- a/go.mod
+++ b/go.mod
@@ -10,45 +10,32 @@ require (
 	gitlab.com/elixxir/client v1.5.1-0.20221006000407-60c78c92fd15
 	gitlab.com/elixxir/crypto v0.0.7-0.20221003185354-b091598d2322
 	gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6
+	gitlab.com/xx_network/crypto v0.0.5-0.20220913213008-98764f5b3287
 	gitlab.com/xx_network/primitives v0.0.4-0.20220809193445-9fc0a5209548
+	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
 )
 
 require (
 	git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65 // indirect
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	github.com/badoux/checkmail v1.2.1 // indirect
-	github.com/beevik/ntp v0.3.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/cloudflare/circl v1.2.0 // indirect
 	github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
 	github.com/elliotchance/orderedmap v1.4.0 // indirect
-	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/improbable-eng/grpc-web v0.15.0 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.11.7 // indirect
 	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
-	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
-	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
-	github.com/pelletier/go-toml v1.9.5 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
-	github.com/pkg/profile v1.6.0 // indirect
 	github.com/rs/cors v1.8.2 // indirect
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
 	github.com/soheilhy/cmux v0.1.5 // indirect
-	github.com/spf13/afero v1.9.2 // indirect
-	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/cobra v1.5.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/spf13/viper v1.12.0 // indirect
-	github.com/subosito/gotenv v1.4.0 // indirect
 	github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
 	github.com/ttacon/libphonenumber v1.2.1 // indirect
 	github.com/tyler-smith/go-bip39 v1.1.0 // indirect
@@ -57,19 +44,14 @@ require (
 	gitlab.com/elixxir/comms v0.0.4-0.20221005205938-10f2defa5b33 // indirect
 	gitlab.com/elixxir/ekv v0.2.1 // indirect
 	gitlab.com/xx_network/comms v0.0.4-0.20221005205845-b34d538ffd85 // indirect
-	gitlab.com/xx_network/crypto v0.0.5-0.20220913213008-98764f5b3287 // indirect
 	gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/ratelimit v0.2.0 // indirect
-	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
 	golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect
 	golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect
 	golang.org/x/text v0.3.7 // indirect
 	google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
 	google.golang.org/grpc v1.49.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
-	gopkg.in/ini.v1 v1.66.6 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 	nhooyr.io/websocket v1.8.7 // indirect
 )
diff --git a/go.sum b/go.sum
index f9f4acba6d43d560378ae4833a6f65ff731b68b9..c164cd8ecefe4e7146e1915f45326fbd93ddec91 100644
--- a/go.sum
+++ b/go.sum
@@ -87,8 +87,6 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ=
 github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0=
-github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw=
-github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@@ -167,7 +165,6 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2
 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -333,7 +330,6 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@@ -351,7 +347,6 @@ github.com/improbable-eng/grpc-web v0.12.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6
 github.com/improbable-eng/grpc-web v0.14.1/go.mod h1:zEjGHa8DAlkoOXmswrNvhUGEYQA9UI7DhrGeHR1DMGU=
 github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
 github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -407,7 +402,6 @@ github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0U
 github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4=
 github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE=
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
-github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
 github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -440,7 +434,6 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -486,10 +479,8 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
-github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
-github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
 github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
@@ -499,7 +490,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
-github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
 github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
 github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -564,19 +554,14 @@ github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJ
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
-github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
 github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
 github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
 github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@@ -595,7 +580,6 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
-github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs=
 github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0=
@@ -630,26 +614,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40=
 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
-gitlab.com/elixxir/client v1.5.1-0.20221004163122-5a4635dce0fa h1:sjZ+73Jesh/wU036YbZ5UAGjLIeKCVscf7sQDHMC4DM=
-gitlab.com/elixxir/client v1.5.1-0.20221004163122-5a4635dce0fa/go.mod h1:wuTIcLuMnvIGSo8i/Gg/SbYF57bE+CbKPpA1Xbk2AKk=
-gitlab.com/elixxir/client v1.5.1-0.20221005170957-7bd7001c3f39 h1:KDgJOCkKwKQBBDM6q9MF3OBNkr0Y7h/YEcgX8OH/Chg=
-gitlab.com/elixxir/client v1.5.1-0.20221005170957-7bd7001c3f39/go.mod h1:wuTIcLuMnvIGSo8i/Gg/SbYF57bE+CbKPpA1Xbk2AKk=
-gitlab.com/elixxir/client v1.5.1-0.20221005204036-f587d711ed7f h1:NLftJa6tnVt2qsV9UrnAsMP4tPWypkTVypQ7kr8C2RM=
-gitlab.com/elixxir/client v1.5.1-0.20221005204036-f587d711ed7f/go.mod h1:wuTIcLuMnvIGSo8i/Gg/SbYF57bE+CbKPpA1Xbk2AKk=
-gitlab.com/elixxir/client v1.5.1-0.20221005210039-a909ab7496c4 h1:YBFNjqoZYwk8qwBeDReehS6Woau1R8XBDWLQkvH4Iok=
-gitlab.com/elixxir/client v1.5.1-0.20221005210039-a909ab7496c4/go.mod h1:PtvF9F/m8eDK8WfFfXkSa+RN6yVYUq3oy/SQ5+DsWIM=
-gitlab.com/elixxir/client v1.5.1-0.20221005215834-e857bc44efec h1:Al03sdDXqYkfbH0sbtNPm4qEtvSXvSPQQJfdLsf/oOo=
-gitlab.com/elixxir/client v1.5.1-0.20221005215834-e857bc44efec/go.mod h1:2lMkL4403zPYS3ndydbe4iqxI1BXpwjKNlJd4G/jjf8=
-gitlab.com/elixxir/client v1.5.1-0.20221005220305-f198ad1b1c55 h1:rh2yIrASX/sItHDtA6sxhkdTMUbP20NS1ov+xUZYX2A=
-gitlab.com/elixxir/client v1.5.1-0.20221005220305-f198ad1b1c55/go.mod h1:2lMkL4403zPYS3ndydbe4iqxI1BXpwjKNlJd4G/jjf8=
-gitlab.com/elixxir/client v1.5.1-0.20221005221039-2a972160e7b9 h1:3eu0FHuwR2DzsluF57571idWyqEtC8TmB52hoflShIg=
-gitlab.com/elixxir/client v1.5.1-0.20221005221039-2a972160e7b9/go.mod h1:2lMkL4403zPYS3ndydbe4iqxI1BXpwjKNlJd4G/jjf8=
-gitlab.com/elixxir/client v1.5.1-0.20221005222316-6aad5ef9fb23 h1:wRXqIMw7EzJMWxmi3XbX9m9jDR/gtNF+pTirCd4Sdsk=
-gitlab.com/elixxir/client v1.5.1-0.20221005222316-6aad5ef9fb23/go.mod h1:2lMkL4403zPYS3ndydbe4iqxI1BXpwjKNlJd4G/jjf8=
 gitlab.com/elixxir/client v1.5.1-0.20221006000407-60c78c92fd15 h1:jo3AWj63GcLN1ANkzTSZZb58U1Oayyy+d7sjP+bK1VE=
 gitlab.com/elixxir/client v1.5.1-0.20221006000407-60c78c92fd15/go.mod h1:PtvF9F/m8eDK8WfFfXkSa+RN6yVYUq3oy/SQ5+DsWIM=
-gitlab.com/elixxir/comms v0.0.4-0.20220916185715-f1e9a5eda939 h1:+VRx2ULHKs040bBhDAOKNCZnbcXxUk3jD9JoKQzQpLk=
-gitlab.com/elixxir/comms v0.0.4-0.20220916185715-f1e9a5eda939/go.mod h1:AO6XkMhaHJW8eXlgL5m3UUcJqsSP8F5Wm1GX+wyq/rw=
 gitlab.com/elixxir/comms v0.0.4-0.20221005205938-10f2defa5b33 h1:mtn/b+/+cMoZNSEo6293U48uqz+aE0si90mPwlhh08w=
 gitlab.com/elixxir/comms v0.0.4-0.20221005205938-10f2defa5b33/go.mod h1:oRteMH+R5t1j/FZ+KJJnZUcqJO2sLXnWksN5HPkZUIo=
 gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c=
@@ -668,9 +634,6 @@ gitlab.com/elixxir/primitives v0.0.3-0.20220810173935-592f34a88326/go.mod h1:9Bb
 gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 h1:/cxxZBP5jTPDpC3zgOx9vV1ojmJyG8pYtkl3IbcewNQ=
 gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA=
 gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw=
-gitlab.com/xx_network/comms v0.0.4-0.20220913215811-c4bf83b27de3/go.mod h1:E2QKOKyPKLRjLUwMxgZpTKueEsHDEqshfqOHJ54ttxU=
-gitlab.com/xx_network/comms v0.0.4-0.20220916185248-8a984b8594de h1:44VKuVgT6X1l+MX8/oNmYORA+pa4nkOWV8hYxi4SCzc=
-gitlab.com/xx_network/comms v0.0.4-0.20220916185248-8a984b8594de/go.mod h1:E2QKOKyPKLRjLUwMxgZpTKueEsHDEqshfqOHJ54ttxU=
 gitlab.com/xx_network/comms v0.0.4-0.20221005205845-b34d538ffd85 h1:bX2IYFnEbWTNGhZHfzHME19pkfD4Q7oTxFGI70PM2PM=
 gitlab.com/xx_network/comms v0.0.4-0.20221005205845-b34d538ffd85/go.mod h1:E2QKOKyPKLRjLUwMxgZpTKueEsHDEqshfqOHJ54ttxU=
 gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE=
@@ -1246,7 +1209,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
 gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
diff --git a/main.go b/main.go
index a7042228bd1a49304f7bccaae7c4c973755a359c..cc39e0fb437a9482fd8f96bf434ce95643daec3b 100644
--- a/main.go
+++ b/main.go
@@ -13,6 +13,7 @@ import (
 	"fmt"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/bindings"
+	"gitlab.com/elixxir/xxdk-wasm/creds"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/elixxir/xxdk-wasm/wasm"
 	"os"
@@ -36,6 +37,11 @@ func main() {
 	js.Global().Set("Base64ToUint8Array", js.FuncOf(utils.Base64ToUint8Array))
 	js.Global().Set("Uint8ArrayEquals", js.FuncOf(utils.Uint8ArrayEquals))
 
+	// creds/password.go
+	js.Global().Set("GetOrInit", js.FuncOf(creds.GetOrInitJS))
+	js.Global().Set("ChangeExternalPassword",
+		js.FuncOf(creds.ChangeExternalPasswordJS))
+
 	// wasm/backup.go
 	js.Global().Set("NewCmixFromBackup", js.FuncOf(wasm.NewCmixFromBackup))
 	js.Global().Set("InitializeBackup", js.FuncOf(wasm.InitializeBackup))
diff --git a/utils/localStorage.go b/utils/localStorage.go
new file mode 100644
index 0000000000000000000000000000000000000000..11b49c82f23ebeddafaaed17905ef5e52b07ef48
--- /dev/null
+++ b/utils/localStorage.go
@@ -0,0 +1,120 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 utils
+
+import (
+	"encoding/base64"
+	"os"
+	"syscall/js"
+)
+
+// LocalStorage contains the js.Value representation of localStorage.
+type LocalStorage struct {
+	v js.Value
+}
+
+// jsStorage is the global that stores Javascript as window.localStorage.
+//
+//  - Specification:
+//    https://html.spec.whatwg.org/multipage/webstorage.html#dom-localstorage-dev
+//  - Documentation:
+//    https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
+var jsStorage = LocalStorage{js.Global().Get("localStorage")}
+
+// GetLocalStorage returns Javascript's local storage.
+func GetLocalStorage() *LocalStorage {
+	return &jsStorage
+}
+
+// GetItem returns a key's value from the local storage given its name. Returns
+// os.ErrNotExist if the key does not exist. Underneath, it calls
+// localStorage.GetItem().
+//
+//  - Specification:
+//    https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-getitem-dev
+//  - Documentation:
+//    https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem
+func (s *LocalStorage) GetItem(keyName string) ([]byte, error) {
+	keyValue := s.v.Call("getItem", keyName)
+	if keyValue.IsNull() {
+		return nil, os.ErrNotExist
+	}
+
+	decodedKeyValue, err := base64.StdEncoding.DecodeString(keyValue.String())
+	if err != nil {
+		return nil, err
+	}
+
+	return decodedKeyValue, nil
+}
+
+// SetItem adds a key's value to local storage given its name. Underneath, it
+// calls localStorage.SetItem().
+//
+//  - Specification:
+//    https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-setitem-dev
+//  - Documentation:
+//    https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem
+func (s *LocalStorage) SetItem(keyName string, keyValue []byte) {
+	encodedKeyValue := base64.StdEncoding.EncodeToString(keyValue)
+	s.v.Call("setItem", keyName, encodedKeyValue)
+}
+
+// RemoveItem removes a key's value from local storage given its name. If there
+// is no item with the given key, this function does nothing. Underneath, it
+// calls localStorage.RemoveItem().
+//
+//  - Specification:
+//    https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-removeitem-dev
+//  - Documentation:
+//    https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
+func (s *LocalStorage) RemoveItem(keyName string) {
+	s.v.Call("removeItem", keyName)
+}
+
+// Clear clears all the keys in storage. Underneath, it calls
+// localStorage.clear().
+//
+//  - Specification:
+//    https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-clear-dev
+//  - Documentation:
+//    https://developer.mozilla.org/en-US/docs/Web/API/Storage/clear
+func (s *LocalStorage) Clear() {
+	s.v.Call("clear")
+}
+
+// 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
+// localStorage.key().
+//
+//  - Specification:
+//    https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key-dev
+//  - Documentation:
+//    https://developer.mozilla.org/en-US/docs/Web/API/Storage/key
+func (s *LocalStorage) Key(n int) (string, error) {
+	keyName := s.v.Call("key", n)
+	if keyName.IsNull() {
+		return "", os.ErrNotExist
+	}
+
+	return keyName.String(), nil
+}
+
+// Length returns the number of keys in localStorage. Underneath, it accesses
+// the property localStorage.length.
+//
+//  - Specification:
+//    https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key-dev
+//  - Documentation:
+//    https://developer.mozilla.org/en-US/docs/Web/API/Storage/length
+func (s *LocalStorage) Length() int {
+	return s.v.Get("length").Int()
+}
diff --git a/utils/localStorage_test.go b/utils/localStorage_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..953872cd03ecb20f3ac0338efc16409bf20fbf09
--- /dev/null
+++ b/utils/localStorage_test.go
@@ -0,0 +1,169 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 utils
+
+import (
+	"bytes"
+	"github.com/pkg/errors"
+	"os"
+	"strconv"
+	"testing"
+)
+
+// Tests that a value set with LocalStorage.SetItem and retrieved with
+// LocalStorage.GetItem matches the original.
+func TestLocalStorage_GetItem_SetItem(t *testing.T) {
+	values := map[string][]byte{
+		"key1": []byte("key value"),
+		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+		"key3": {0, 49, 0, 0, 0, 38, 249, 93, 242, 189, 222, 32, 138, 248, 121,
+			151, 42, 108, 82, 199, 163, 61, 4, 200, 140, 231, 225, 20, 35, 243,
+			253, 161, 61, 2, 227, 208, 173, 183, 33, 66, 236, 107, 105, 119, 26,
+			42, 44, 60, 109, 172, 38, 47, 220, 17, 129, 4, 234, 241, 141, 81,
+			84, 185, 32, 120, 115, 151, 128, 196, 143, 117, 222, 78, 44, 115,
+			109, 20, 249, 46, 158, 139, 231, 157, 54, 219, 141, 252},
+	}
+
+	for keyName, keyValue := range values {
+		jsStorage.SetItem(keyName, keyValue)
+
+		loadedValue, err := jsStorage.GetItem(keyName)
+		if err != nil {
+			t.Errorf("Failed to load %q: %+v", keyName, err)
+		}
+
+		if !bytes.Equal(keyValue, loadedValue) {
+			t.Errorf("Loaded value does not match original for %q"+
+				"\nexpected: %q\nreceived: %q", keyName, keyValue, loadedValue)
+		}
+	}
+}
+
+// Tests that LocalStorage.GetItem returns the error os.ErrNotExist when the key
+// does not exist in storage.
+func TestLocalStorage_GetItem_NotExistError(t *testing.T) {
+	_, err := jsStorage.GetItem("someKey")
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Incorrect error for non existant key."+
+			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
+	}
+}
+
+// Tests that LocalStorage.RemoveItem deletes a key from store and that it
+// cannot be retrieved.
+func TestLocalStorage_RemoveItem(t *testing.T) {
+	keyName := "key"
+	jsStorage.SetItem(keyName, []byte("value"))
+	jsStorage.RemoveItem(keyName)
+
+	_, err := jsStorage.GetItem(keyName)
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Failed to remove %q: %+v", keyName, err)
+	}
+}
+
+// Tests that LocalStorage.Clear deletes all keys from storage.
+func TestLocalStorage_Clear(t *testing.T) {
+	for i := 0; i < 10; i++ {
+		jsStorage.SetItem(strconv.Itoa(i), []byte(strconv.Itoa(i)))
+	}
+
+	jsStorage.Clear()
+
+	l := jsStorage.Length()
+
+	if l > 0 {
+		t.Errorf("Clear did not delete all keys. Found %d keys.", l)
+	}
+}
+
+// Tests that LocalStorage.Key return all added keys when looping through all
+// indexes.
+func TestLocalStorage_Key(t *testing.T) {
+	jsStorage.v.Call("clear")
+	values := map[string][]byte{
+		"key1": []byte("key value"),
+		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
+	}
+
+	for keyName, keyValue := range values {
+		jsStorage.SetItem(keyName, keyValue)
+	}
+
+	numKeys := len(values)
+	for i := 0; i < numKeys; i++ {
+		keyName, err := jsStorage.Key(i)
+		if err != nil {
+			t.Errorf("No key found for index %d: %+v", i, err)
+		}
+
+		if _, exists := values[keyName]; !exists {
+			t.Errorf("No key with name %q added to storage.", keyName)
+		}
+		delete(values, keyName)
+	}
+
+	if len(values) != 0 {
+		t.Errorf("%d keys not read from storage: %q", len(values), values)
+	}
+}
+
+// Tests that LocalStorage.Key returns the error os.ErrNotExist when the index
+// is greater than or equal to the number of keys.
+func TestLocalStorage_Key_NotExistError(t *testing.T) {
+	jsStorage.v.Call("clear")
+	jsStorage.SetItem("key", []byte("value"))
+
+	_, err := jsStorage.Key(1)
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Incorrect error for non existant key index."+
+			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
+	}
+
+	_, err = jsStorage.Key(2)
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Incorrect error for non existant key index."+
+			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
+	}
+}
+
+// Tests that LocalStorage.Length returns the correct Length when adding and
+// removing various keys.
+func TestLocalStorage_Length(t *testing.T) {
+	jsStorage.v.Call("clear")
+	values := map[string][]byte{
+		"key1": []byte("key value"),
+		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
+	}
+
+	i := 0
+	for keyName, keyValue := range values {
+		jsStorage.SetItem(keyName, keyValue)
+		i++
+
+		if jsStorage.Length() != i {
+			t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d",
+				i, jsStorage.Length())
+		}
+	}
+
+	i = len(values)
+	for keyName := range values {
+		jsStorage.RemoveItem(keyName)
+		i--
+
+		if jsStorage.Length() != i {
+			t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d",
+				i, jsStorage.Length())
+		}
+	}
+}