diff --git a/Gruntfile.js b/Gruntfile.js
index 4a20457c7..e769102ca 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -67,9 +67,6 @@ module.exports = grunt => {
           'libtextsecure/errors.js',
           'libtextsecure/libsignal-protocol.js',
           'libtextsecure/crypto.js',
-          'libtextsecure/storage.js',
-          'libtextsecure/storage/user.js',
-          'libtextsecure/helpers.js',
         ],
         dest: 'js/libtextsecure.js',
       },
diff --git a/background.html b/background.html
index 5004ac77f..6a3ee28aa 100644
--- a/background.html
+++ b/background.html
@@ -32,7 +32,6 @@
   -->
 
     
-    
     
 
     
diff --git a/js/background.js b/js/background.js
index 548649428..aa2f349bc 100644
--- a/js/background.js
+++ b/js/background.js
@@ -329,8 +329,7 @@
           initialLoadComplete,
         });
       }
-    });
-
+    };
 
     Whisper.events.on('openInbox', () => {
       appView.openInbox({
diff --git a/js/storage.js b/js/storage.js
deleted file mode 100644
index 017eee1b3..000000000
--- a/js/storage.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/* eslint-disable more/no-then */
-
-// eslint-disable-next-line func-names
-(function() {
-  'use strict';
-
-  window.Whisper = window.Whisper || {};
-
-  let ready = false;
-  let items;
-  let callbacks = [];
-
-  reset();
-
-  async function put(key, value) {
-    if (value === undefined) {
-      throw new Error('Tried to store undefined');
-    }
-    if (!ready) {
-      window.log.warn('Called storage.put before storage is ready. key:', key);
-    }
-
-    const data = { id: key, value };
-
-    items[key] = data;
-    await window.Signal.Data.createOrUpdateItem(data);
-  }
-
-  function get(key, defaultValue) {
-    if (!ready) {
-      window.log.warn('Called storage.get before storage is ready. key:', key);
-    }
-
-    const item = items[key];
-    if (!item) {
-      return defaultValue;
-    }
-
-    return item.value;
-  }
-
-  async function remove(key) {
-    if (!ready) {
-      window.log.warn('Called storage.get before storage is ready. key:', key);
-    }
-
-    delete items[key];
-    await window.Signal.Data.removeItemById(key);
-  }
-
-  function onready(callback) {
-    if (ready) {
-      callback();
-    } else {
-      callbacks.push(callback);
-    }
-  }
-
-  function callListeners() {
-    if (ready) {
-      callbacks.forEach(callback => callback());
-      callbacks = [];
-    }
-  }
-
-  async function fetch() {
-    this.reset();
-    const array = await window.Signal.Data.getAllItems();
-
-    for (let i = 0, max = array.length; i < max; i += 1) {
-      const item = array[i];
-      const { id } = item;
-      items[id] = item;
-    }
-
-    ready = true;
-    callListeners();
-  }
-
-  function reset() {
-    ready = false;
-    items = Object.create(null);
-  }
-
-  const storage = {
-    fetch,
-    put,
-    get,
-    remove,
-    onready,
-    reset,
-  };
-
-  // Keep a reference to this storage system, since there are scenarios where
-  //   we need to replace it with the legacy storage system for a while.
-  window.newStorage = storage;
-
-  window.textsecure = window.textsecure || {};
-  window.textsecure.storage = window.textsecure.storage || {};
-
-  window.installStorage = newStorage => {
-    window.storage = newStorage;
-    window.textsecure.storage.impl = newStorage;
-  };
-
-  window.installStorage(storage);
-})();
diff --git a/libtextsecure/helpers.js b/libtextsecure/helpers.js
deleted file mode 100644
index 87574d975..000000000
--- a/libtextsecure/helpers.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/* global window, dcodeIO */
-
-/* eslint-disable no-proto, no-restricted-syntax, guard-for-in */
-
-window.textsecure = window.textsecure || {};
-
-/** *******************************
- *** Type conversion utilities ***
- ******************************** */
-// Strings/arrays
-// TODO: Throw all this shit in favor of consistent types
-// TODO: Namespace
-const StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
-const StaticArrayBufferProto = new ArrayBuffer().__proto__;
-const StaticUint8ArrayProto = new Uint8Array().__proto__;
-function getString(thing) {
-  if (thing === Object(thing)) {
-    if (thing.__proto__ === StaticUint8ArrayProto) {
-      return String.fromCharCode.apply(null, thing);
-    }
-    if (thing.__proto__ === StaticArrayBufferProto) {
-      return getString(new Uint8Array(thing));
-    }
-    if (thing.__proto__ === StaticByteBufferProto) {
-      return thing.toString('binary');
-    }
-  }
-  return thing;
-}
-
-function getStringable(thing) {
-  return (
-    typeof thing === 'string' ||
-    typeof thing === 'number' ||
-    typeof thing === 'boolean' ||
-    (thing === Object(thing) &&
-      (thing.__proto__ === StaticArrayBufferProto ||
-        thing.__proto__ === StaticUint8ArrayProto ||
-        thing.__proto__ === StaticByteBufferProto))
-  );
-}
-
-// Number formatting utils
-window.textsecure.utils = (() => {
-  const self = {};
-  self.unencodeNumber = number => number.split('.');
-  self.isNumberSane = number => number[0] === '+' && /^[0-9]+$/.test(number.substring(1));
-
-  /** ************************
-   *** JSON'ing Utilities ***
-   ************************* */
-  function ensureStringed(thing) {
-    if (getStringable(thing)) {
-      return getString(thing);
-    } else if (thing instanceof Array) {
-      const res = [];
-      for (let i = 0; i < thing.length; i += 1) {
-        res[i] = ensureStringed(thing[i]);
-      }
-      return res;
-    } else if (thing === Object(thing)) {
-      const res = {};
-      for (const key in thing) {
-        res[key] = ensureStringed(thing[key]);
-      }
-      return res;
-    } else if (thing === null) {
-      return null;
-    }
-    throw new Error(`unsure of how to jsonify object of type ${typeof thing}`);
-  }
-
-  self.jsonThing = thing => JSON.stringify(ensureStringed(thing));
-
-  return self;
-})();
diff --git a/libtextsecure/storage.js b/libtextsecure/storage.js
deleted file mode 100644
index 300f7e913..000000000
--- a/libtextsecure/storage.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/* global window, textsecure, localStorage */
-
-// eslint-disable-next-line func-names
-(function() {
-  /** **********************************************
-   *** Utilities to store data in local storage ***
-   *********************************************** */
-  window.textsecure = window.textsecure || {};
-  window.textsecure.storage = window.textsecure.storage || {};
-
-  // Overrideable storage implementation
-  window.textsecure.storage.impl = window.textsecure.storage.impl || {
-    /** ***************************
-     *** Base Storage Routines ***
-     **************************** */
-    put(key, value) {
-      if (value === undefined) {
-        throw new Error('Tried to store undefined');
-      }
-      localStorage.setItem(`${key}`, textsecure.utils.jsonThing(value));
-    },
-
-    get(key, defaultValue) {
-      const value = localStorage.getItem(`${key}`);
-      if (value === null) {
-        return defaultValue;
-      }
-      return JSON.parse(value);
-    },
-
-    remove(key) {
-      localStorage.removeItem(`${key}`);
-    },
-  };
-
-  window.textsecure.storage.put = (key, value) => textsecure.storage.impl.put(key, value);
-  window.textsecure.storage.get = (key, defaultValue) =>
-    textsecure.storage.impl.get(key, defaultValue);
-  window.textsecure.storage.remove = key => textsecure.storage.impl.remove(key);
-})();
diff --git a/libtextsecure/storage/user.js b/libtextsecure/storage/user.js
deleted file mode 100644
index b4b771e88..000000000
--- a/libtextsecure/storage/user.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/* global textsecure, window */
-
-// eslint-disable-next-line func-names
-(function() {
-  /** *******************************************
-   *** Utilities to store data about the user ***
-   ********************************************* */
-  window.textsecure = window.textsecure || {};
-  window.textsecure.storage = window.textsecure.storage || {};
-
-  window.textsecure.storage.user = {
-    setNumberAndDeviceId(number, deviceId, deviceName) {
-      textsecure.storage.put('number_id', `${number}.${deviceId}`);
-      if (deviceName) {
-        textsecure.storage.put('device_name', deviceName);
-      }
-    },
-
-    getNumber() {
-      const numberId = textsecure.storage.get('number_id');
-      if (numberId === undefined) {
-        return undefined;
-      }
-      return textsecure.utils.unencodeNumber(numberId)[0];
-    },
-
-    isSignInByLinking() {
-      const isSignInByLinking = textsecure.storage.get('is_sign_in_by_linking');
-      if (isSignInByLinking === undefined) {
-        return false;
-      }
-      return isSignInByLinking;
-    },
-
-    setSignInByLinking(isLinking) {
-      textsecure.storage.put('is_sign_in_by_linking', isLinking);
-    },
-
-    isSignWithRecoveryPhrase() {
-      const isRecoveryPhraseUsed = textsecure.storage.get('is_sign_in_recovery_phrase');
-      if (isRecoveryPhraseUsed === undefined) {
-        return false;
-      }
-      return isRecoveryPhraseUsed;
-    },
-    setSignWithRecoveryPhrase(isRecoveryPhraseUsed) {
-      textsecure.storage.put('is_sign_in_recovery_phrase', isRecoveryPhraseUsed);
-    },
-
-    getLastProfileUpdateTimestamp() {
-      return textsecure.storage.get('last_profile_update_timestamp');
-    },
-
-    setLastProfileUpdateTimestamp(lastUpdateTimestamp) {
-      textsecure.storage.put('last_profile_update_timestamp', lastUpdateTimestamp);
-    },
-
-    getDeviceId() {
-      const numberId = textsecure.storage.get('number_id');
-      if (numberId === undefined) {
-        return undefined;
-      }
-      return textsecure.utils.unencodeNumber(numberId)[1];
-    },
-
-    getDeviceName() {
-      return textsecure.storage.get('device_name');
-    },
-  };
-})();
diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx
index cc02b2862..b3e993e73 100644
--- a/ts/components/conversation/SessionConversation.tsx
+++ b/ts/components/conversation/SessionConversation.tsx
@@ -20,7 +20,7 @@ import { SplitViewContainer } from '../SplitViewContainer';
 import { LightboxGallery, MediaItemType } from '../lightbox/LightboxGallery';
 import { getLastMessageInConversation, getPubkeysInPublicConversation } from '../../data/data';
 import { getConversationController } from '../../session/conversations';
-import { ToastUtils, UserUtils } from '../../session/utils';
+import { ToastUtils } from '../../session/utils';
 import {
   openConversationToSpecificMessage,
   quoteMessage,
@@ -50,6 +50,7 @@ import { blobToArrayBuffer } from 'blob-util';
 import { MAX_ATTACHMENT_FILESIZE_BYTES } from '../../session/constants';
 import { ConversationMessageRequestButtons } from './ConversationRequestButtons';
 import { ConversationRequestinfo } from './ConversationRequestInfo';
+import { getCurrentRecoveryPhrase } from '../../util/storage';
 // tslint:disable: jsx-curly-spacing
 
 interface State {
@@ -176,8 +177,7 @@ export class SessionConversation extends React.Component {
       await this.scrollToNow();
     };
 
-    // const recoveryPhrase = window.textsecure.storage.get('mnemonic');
-    const recoveryPhrase = UserUtils.getCurrentRecoveryPhrase();
+    const recoveryPhrase = getCurrentRecoveryPhrase() as string;
 
     // string replace to fix case where pasted text contains invis characters causing false negatives
     if (msg.body.replace(/\s/g, '').includes(recoveryPhrase.replace(/\s/g, ''))) {
diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx
index b115a44db..4447e1020 100644
--- a/ts/components/dialog/EditProfileDialog.tsx
+++ b/ts/components/dialog/EditProfileDialog.tsx
@@ -21,6 +21,7 @@ import { MAX_USERNAME_LENGTH } from '../registration/RegistrationStages';
 import { SessionWrapperModal } from '../SessionWrapperModal';
 import { pickFileForAvatar } from '../../types/attachments/VisualAttachment';
 import { sanitizeSessionUsername } from '../../session/utils/String';
+import { setLastProfileUpdateTimestamp } from '../../util/storage';
 
 interface State {
   profileName: string;
@@ -319,7 +320,7 @@ async function commitProfileEdits(newName: string, scaledAvatarUrl: string | nul
   });
   // might be good to not trigger a sync if the name did not change
   await conversation.commit();
-  UserUtils.setLastProfileUpdateTimestamp(Date.now());
+  await setLastProfileUpdateTimestamp(Date.now());
   await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
 }
 
diff --git a/ts/components/dialog/SessionSeedModal.tsx b/ts/components/dialog/SessionSeedModal.tsx
index 08b8f4f2a..c24c66373 100644
--- a/ts/components/dialog/SessionSeedModal.tsx
+++ b/ts/components/dialog/SessionSeedModal.tsx
@@ -1,6 +1,6 @@
 import React, { useEffect, useState } from 'react';
 
-import { ToastUtils, UserUtils } from '../../session/utils';
+import { ToastUtils } from '../../session/utils';
 import { PasswordUtil } from '../../util';
 import { getPasswordHash } from '../../data/data';
 import { QRCode } from 'react-qr-svg';
@@ -10,6 +10,7 @@ import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
 import { useDispatch } from 'react-redux';
 import { SessionButton, SessionButtonColor } from '../basic/SessionButton';
 import { SessionWrapperModal } from '../SessionWrapperModal';
+import { getCurrentRecoveryPhrase } from '../../util/storage';
 
 interface PasswordProps {
   setPasswordValid: (val: boolean) => any;
@@ -168,7 +169,7 @@ const SessionSeedModalInner = (props: ModalInnerProps) => {
     if (recoveryPhrase) {
       return false;
     }
-    const newRecoveryPhrase = UserUtils.getCurrentRecoveryPhrase();
+    const newRecoveryPhrase = getCurrentRecoveryPhrase();
     setRecoveryPhrase(newRecoveryPhrase);
     setLoadingSeed(false);
 
diff --git a/ts/components/leftpane/LeftPaneSectionHeader.tsx b/ts/components/leftpane/LeftPaneSectionHeader.tsx
index a2610e7f4..f75f2a227 100644
--- a/ts/components/leftpane/LeftPaneSectionHeader.tsx
+++ b/ts/components/leftpane/LeftPaneSectionHeader.tsx
@@ -7,9 +7,9 @@ import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
 import { Flex } from '../basic/Flex';
 import { getFocusedSection, getOverlayMode } from '../../state/selectors/section';
 import { SectionType, setOverlayMode } from '../../state/ducks/section';
-import { UserUtils } from '../../session/utils';
 import { SessionButton, SessionButtonType } from '../basic/SessionButton';
 import { SessionIcon, SessionIconButton } from '../icon';
+import { isSignWithRecoveryPhrase } from '../../util/storage';
 
 const SectionTitle = styled.h1`
   padding: 0 var(--margins-sm);
@@ -94,7 +94,7 @@ const BannerInner = () => {
 
 export const LeftPaneBanner = () => {
   const section = useSelector(getFocusedSection);
-  const isSignInWithRecoveryPhrase = UserUtils.isSignWithRecoveryPhrase();
+  const isSignInWithRecoveryPhrase = isSignWithRecoveryPhrase();
 
   if (section !== SectionType.Message || isSignInWithRecoveryPhrase) {
     return null;
diff --git a/ts/components/registration/RegistrationStages.tsx b/ts/components/registration/RegistrationStages.tsx
index 80b0f1b0f..4f1f8a63f 100644
--- a/ts/components/registration/RegistrationStages.tsx
+++ b/ts/components/registration/RegistrationStages.tsx
@@ -5,7 +5,7 @@ import { createOrUpdateItem, removeAll } from '../../data/data';
 import { getSwarmPollingInstance } from '../../session/apis/snode_api';
 import { getConversationController } from '../../session/conversations';
 import { mn_decode } from '../../session/crypto/mnemonic';
-import { PromiseUtils, StringUtils, ToastUtils, UserUtils } from '../../session/utils';
+import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils';
 import { TaskTimedOutError } from '../../session/utils/Promise';
 import { trigger } from '../../shims/events';
 import {
@@ -15,14 +15,15 @@ import {
   signInByLinkingDevice,
 } from '../../util/accountManager';
 import { fromHex } from '../../session/utils/String';
+import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage';
 
 export const MAX_USERNAME_LENGTH = 26;
 // tslint:disable: use-simple-attributes
 
 export async function resetRegistration() {
   await removeAll();
-  await window.storage.reset();
-  await window.storage.fetch();
+  Storage.reset();
+  await Storage.fetch();
   getConversationController().reset();
   await getConversationController().load();
 }
@@ -64,7 +65,7 @@ export async function signUp(signUpDetails: {
       value: true,
       timestamp: Date.now(),
     });
-    UserUtils.setSignWithRecoveryPhrase(false);
+    setSignWithRecoveryPhrase(false);
     trigger('openInbox');
   } catch (e) {
     await resetRegistration();
@@ -95,7 +96,7 @@ export async function signInWithRecovery(signInDetails: {
     await resetRegistration();
 
     await registerSingleDevice(userRecoveryPhrase, 'english', trimName);
-    UserUtils.setSignWithRecoveryPhrase(true);
+    setSignWithRecoveryPhrase(true);
 
     trigger('openInbox');
   } catch (e) {
@@ -120,10 +121,10 @@ export async function signInWithLinking(signInDetails: { userRecoveryPhrase: str
     await getSwarmPollingInstance().start();
 
     await PromiseUtils.waitForTask(done => {
-      window.Whisper.events.on('configurationMessageReceived', (displayName: string) => {
+      window.Whisper.events.on('configurationMessageReceived', async (displayName: string) => {
         window.Whisper.events.off('configurationMessageReceived');
-        UserUtils.setSignInByLinking(false);
-        UserUtils.setSignWithRecoveryPhrase(true);
+        await setSignInByLinking(false);
+        await setSignWithRecoveryPhrase(true);
         done(displayName);
 
         displayNameFromNetwork = displayName;
diff --git a/ts/components/registration/SessionRegistrationView.tsx b/ts/components/registration/SessionRegistrationView.tsx
index a830c5244..41919d3ef 100644
--- a/ts/components/registration/SessionRegistrationView.tsx
+++ b/ts/components/registration/SessionRegistrationView.tsx
@@ -4,9 +4,9 @@ import { AccentText } from './AccentText';
 import { RegistrationStages } from './RegistrationStages';
 import { SessionIcon } from '../icon';
 import { SessionToastContainer } from '../SessionToastContainer';
-import { setSignInByLinking } from '../../session/utils/User';
 import { SessionTheme } from '../../state/ducks/SessionTheme';
 import { Flex } from '../basic/Flex';
+import { setSignInByLinking } from '../../util/storage';
 
 export const SessionRegistrationView = () => {
   useEffect(() => {
diff --git a/ts/data/data.ts b/ts/data/data.ts
index 733b1c64a..f8ded917c 100644
--- a/ts/data/data.ts
+++ b/ts/data/data.ts
@@ -19,6 +19,7 @@ import { PubKey } from '../session/types';
 import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String';
 import { ReduxConversationType } from '../state/ducks/conversations';
 import { ExpirationTimerOptions } from '../util/expiringMessages';
+import { Storage } from '../util/storage';
 import { channels } from './channels';
 import { channelsToMake as channelstoMakeOpenGroupV2 } from './opengroups';
 
@@ -469,7 +470,7 @@ export async function generateAttachmentKeyIfEmpty() {
       value: encryptingKey,
     });
     // be sure to write the new key to the cache. so we can access it straight away
-    window.textsecure.storage.put('local_attachment_encrypted_key', encryptingKey);
+    await Storage.put('local_attachment_encrypted_key', encryptingKey);
   }
 }
 
diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts
index 3f3c49cf8..2e035b4df 100644
--- a/ts/interactions/conversationInteractions.ts
+++ b/ts/interactions/conversationInteractions.ts
@@ -45,6 +45,7 @@ import { perfEnd, perfStart } from '../session/utils/Performance';
 import { processNewAttachment } from '../types/MessageAttachment';
 import { urlToBlob } from '../types/attachments/VisualAttachment';
 import { MIME } from '../types';
+import { setLastProfileUpdateTimestamp } from '../util/storage';
 
 export const getCompleteUrlForV2ConvoId = async (convoId: string) => {
   if (convoId.match(openGroupV2ConversationIdRegex)) {
@@ -462,7 +463,7 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
   await createOrUpdateItem({ id: lastAvatarUploadTimestamp, value: newTimestampReupload });
 
   if (newAvatarDecrypted) {
-    UserUtils.setLastProfileUpdateTimestamp(Date.now());
+    await setLastProfileUpdateTimestamp(Date.now());
     await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
   } else {
     window.log.info(
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index fd94a6776..5e2b58bca 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -64,6 +64,7 @@ import {
 import { getOurPubKeyStrFromCache } from '../session/utils/User';
 import { MessageRequestResponse } from '../session/messages/outgoing/controlMessage/MessageRequestResponse';
 import { Notifications } from '../util/notifications';
+import { Storage } from '../util/storage';
 
 export enum ConversationTypeEnum {
   GROUP = 'group',
@@ -1146,10 +1147,10 @@ export class ConversationModel extends Backbone.Model {
     if (this.isPrivate() && read.length && options.sendReadReceipts) {
       window?.log?.info(
         `Sending ${read.length} read receipts?`,
-        window.storage.get(SettingsKey.settingsReadReceipt) || false
+        Storage.get(SettingsKey.settingsReadReceipt) || false
       );
       const dontSendReceipt = this.isBlocked() || this.isIncomingRequest();
-      if (window.storage.get(SettingsKey.settingsReadReceipt) && !dontSendReceipt) {
+      if (Storage.get(SettingsKey.settingsReadReceipt) && !dontSendReceipt) {
         const timestamps = _.map(read, 'timestamp').filter(t => !!t) as Array;
         const receiptMessage = new ReadReceiptMessage({
           timestamp: Date.now(),
@@ -1658,7 +1659,7 @@ export class ConversationModel extends Backbone.Model {
     // for typing to happen, this must be a private unblocked active convo, and the settings to be on
     if (
       !this.isActive() ||
-      !window.storage.get(SettingsKey.settingsTypingIndicator) ||
+      !Storage.get(SettingsKey.settingsTypingIndicator) ||
       this.isBlocked() ||
       !this.isPrivate()
     ) {
diff --git a/ts/models/message.ts b/ts/models/message.ts
index 9148c15eb..4f066ef2a 100644
--- a/ts/models/message.ts
+++ b/ts/models/message.ts
@@ -62,6 +62,7 @@ import {
 } from '../types/MessageAttachment';
 import { ExpirationTimerOptions } from '../util/expiringMessages';
 import { Notifications } from '../util/notifications';
+import { Storage } from '../util/storage';
 // tslint:disable: cyclomatic-complexity
 
 /**
@@ -428,7 +429,7 @@ export class MessageModel extends Backbone.Model {
     }
 
     const readBy = this.get('read_by') || [];
-    if (window.storage.get(SettingsKey.settingsReadReceipt) && readBy.length > 0) {
+    if (Storage.get(SettingsKey.settingsReadReceipt) && readBy.length > 0) {
       return 'read';
     }
     const sent = this.get('sent');
diff --git a/ts/notifications/getStatus.ts b/ts/notifications/getStatus.ts
index c837b132a..379e95e59 100644
--- a/ts/notifications/getStatus.ts
+++ b/ts/notifications/getStatus.ts
@@ -14,7 +14,7 @@ interface Status {
   type: Type;
 }
 
-type UserSetting = 'off' | 'count' | 'name' | 'message';
+export type UserSetting = 'off' | 'count' | 'name' | 'message';
 
 type Type = 'ok' | 'disabled' | 'appIsFocused' | 'noNotifications' | 'userSetting';
 
diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts
index 8c3801673..e76ccf46d 100644
--- a/ts/receiver/configMessage.ts
+++ b/ts/receiver/configMessage.ts
@@ -17,13 +17,14 @@ import { handleNewClosedGroup } from './closedGroups';
 import { updateProfileOneAtATime } from './dataMessage';
 import { EnvelopePlus } from './types';
 import { ConversationInteraction } from '../interactions';
+import { getLastProfileUpdateTimestamp, setLastProfileUpdateTimestamp } from '../util/storage';
 
 async function handleOurProfileUpdate(
   sentAt: number | Long,
   configMessage: SignalService.ConfigurationMessage,
   ourPubkey: string
 ) {
-  const latestProfileUpdateTimestamp = UserUtils.getLastProfileUpdateTimestamp();
+  const latestProfileUpdateTimestamp = getLastProfileUpdateTimestamp();
   if (!latestProfileUpdateTimestamp || sentAt > latestProfileUpdateTimestamp) {
     window?.log?.info(
       `Handling our profileUdpate ourLastUpdate:${latestProfileUpdateTimestamp}, envelope sent at: ${sentAt}`
@@ -41,7 +42,7 @@ async function handleOurProfileUpdate(
       profilePicture,
     };
     await updateProfileOneAtATime(ourConversation, lokiProfile, profileKey);
-    UserUtils.setLastProfileUpdateTimestamp(_.toNumber(sentAt));
+    await setLastProfileUpdateTimestamp(_.toNumber(sentAt));
     // do not trigger a signin by linking if the display name is empty
     if (displayName) {
       trigger(configurationMessageReceived, displayName);
diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts
index 90a8adb71..7a678ba86 100644
--- a/ts/receiver/contentMessage.ts
+++ b/ts/receiver/contentMessage.ts
@@ -20,6 +20,7 @@ import { handleCallMessage } from './callMessage';
 import { SettingsKey } from '../data/settings-key';
 import { ConversationTypeEnum } from '../models/conversation';
 import { ReadReceipts } from '../util/readReceipts';
+import { Storage } from '../util/storage';
 
 export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
   try {
@@ -491,7 +492,7 @@ async function handleTypingMessage(
   await removeFromCache(envelope);
 
   // We don't do anything with incoming typing messages if the setting is disabled
-  if (!window.storage.get(SettingsKey.settingsTypingIndicator)) {
+  if (!Storage.get(SettingsKey.settingsTypingIndicator)) {
     return;
   }
 
diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts
index 02bfcd26c..f3c3266e7 100644
--- a/ts/session/utils/User.ts
+++ b/ts/session/utils/User.ts
@@ -6,6 +6,7 @@ import { PubKey } from '../types';
 import { fromHexToArray, toHex } from './String';
 import { getConversationController } from '../conversations';
 import { LokiProfile } from '../../types/Message';
+import { getNumber, Storage } from '../../util/storage';
 
 export type HexKeyPair = {
   pubKey: string;
@@ -29,7 +30,7 @@ export function isUsFromCache(pubKey: string | PubKey | undefined): boolean {
  * Returns the public key of this current device as a STRING, or throws an error
  */
 export function getOurPubKeyStrFromCache(): string {
-  const ourNumber = window.textsecure.storage.user.getNumber();
+  const ourNumber = getNumber();
   if (!ourNumber) {
     throw new Error('ourNumber is not set');
   }
@@ -78,27 +79,11 @@ export async function getUserED25519KeyPair(): Promise {
   return undefined;
 }
 
-export function isSignInByLinking(): boolean {
-  return window.textsecure.storage.user.isSignInByLinking();
-}
-
-export function setSignInByLinking(isLinking: boolean) {
-  window.textsecure.storage.user.setSignInByLinking(isLinking);
-}
-
-export function isSignWithRecoveryPhrase(): boolean {
-  return window.textsecure.storage.user.isSignWithRecoveryPhrase();
-}
-
-export function setSignWithRecoveryPhrase(isLinking: boolean) {
-  window.textsecure.storage.user.setSignWithRecoveryPhrase(isLinking);
-}
-
 export function getOurProfile(): LokiProfile | undefined {
   try {
     // Secondary devices have their profile stored
     // in their primary device's conversation
-    const ourNumber = window.storage.get('primaryDevicePubKey');
+    const ourNumber = Storage.get('primaryDevicePubKey') as string;
     const ourConversation = getConversationController().get(ourNumber);
     const ourProfileKeyHex = ourConversation.get('profileKey');
     const profileKeyAsBytes = ourProfileKeyHex ? fromHexToArray(ourProfileKeyHex) : null;
@@ -115,19 +100,3 @@ export function getOurProfile(): LokiProfile | undefined {
     return undefined;
   }
 }
-
-export function getLastProfileUpdateTimestamp(): number | undefined {
-  return window.textsecure.storage.user.getLastProfileUpdateTimestamp();
-}
-
-export function setLastProfileUpdateTimestamp(lastUpdateTimestamp: number) {
-  return window.textsecure.storage.user.setLastProfileUpdateTimestamp(lastUpdateTimestamp);
-}
-
-export function getCurrentRecoveryPhrase() {
-  return window.textsecure.storage.get('mnemonic');
-}
-
-export function saveRecoveryPhrase(mnemonic: string) {
-  return window.textsecure.storage.put('mnemonic', mnemonic);
-}
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index 4fbed7e74..f040e5b82 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -33,6 +33,7 @@ import { GenericReadableMessageSelectorProps } from '../../components/conversati
 import { LightBoxOptions } from '../../components/conversation/SessionConversation';
 import { getConversationController } from '../../session/conversations';
 import { UserUtils } from '../../session/utils';
+import { Storage } from '../../util/storage';
 
 export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
 
@@ -129,7 +130,7 @@ export const isPublicGroupConversation = createSelector(
 export const getOurPrimaryConversation = createSelector(
   getConversations,
   (state: ConversationsStateType): ReduxConversationType =>
-    state.conversationLookup[window.storage.get('primaryDevicePubKey')]
+    state.conversationLookup[Storage.get('primaryDevicePubKey') as string]
 );
 
 const getMessagesOfSelectedConversation = createSelector(
diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts
index a8263a623..59fe9f3d6 100644
--- a/ts/types/Attachment.ts
+++ b/ts/types/Attachment.ts
@@ -7,6 +7,7 @@ import { SignalService } from '../protobuf';
 import { isImageTypeSupported, isVideoTypeSupported } from '../util/GoogleChrome';
 import { fromHexToArray } from '../session/utils/String';
 import { ATTACHMENT_DEFAULT_MAX_SIDE } from '../util/attachmentsUtil';
+import { Storage } from '../util/storage';
 
 const MAX_WIDTH = 200;
 const MAX_HEIGHT = MAX_WIDTH;
@@ -396,9 +397,8 @@ export const encryptAttachmentBuffer = async (bufferIn: ArrayBuffer) => {
   if (!isArrayBuffer(bufferIn)) {
     throw new TypeError("'bufferIn' must be an array buffer");
   }
-  const encryptingKey = fromHexToArray(
-    window.textsecure.storage.get('local_attachment_encrypted_key')
-  );
+  const key = Storage.get('local_attachment_encrypted_key') as string;
+  const encryptingKey = fromHexToArray(key);
   return window.callWorker('encryptAttachmentBuffer', encryptingKey, bufferIn);
 };
 
@@ -406,8 +406,7 @@ export const decryptAttachmentBuffer = async (bufferIn: ArrayBuffer): Promise;
+let callbacks: Array<() => void> = [];
+
+reset();
+
+async function put(key: string, value: ValueType) {
+  if (value === undefined) {
+    throw new Error('Tried to store undefined');
+  }
+  if (!ready) {
+    window.log.warn('Called storage.put before storage is ready. key:', key);
+  }
+
+  const data: InsertedValueType = { id: key, value };
+
+  items[key] = data;
+  await Data.createOrUpdateItem(data);
+}
+
+function get(key: string, defaultValue?: ValueType) {
+  if (!ready) {
+    window.log.warn('Called storage.get before storage is ready. key:', key);
+  }
+
+  const item = items[key];
+  if (!item) {
+    return defaultValue;
+  }
+
+  return item.value;
+}
+
+async function remove(key: string) {
+  if (!ready) {
+    window.log.warn('Called storage.get before storage is ready. key:', key);
+  }
+
+  // tslint:disable-next-line: no-dynamic-delete
+  delete items[key];
+  await Data.removeItemById(key);
+}
+
+function onready(callback: () => void) {
+  if (ready) {
+    callback();
+  } else {
+    callbacks.push(callback);
+  }
+}
+
+function callListeners() {
+  if (ready) {
+    callbacks.forEach(callback => {
+      callback();
+    });
+    callbacks = [];
+  }
+}
+
+async function fetch() {
+  reset();
+  const array = await Data.getAllItems();
+
+  // tslint:disable-next-line: one-variable-per-declaration
+  for (let i = 0, max = array.length; i < max; i += 1) {
+    const item = array[i];
+    const { id } = item;
+    items[id] = item;
+  }
+
+  ready = true;
+  callListeners();
+}
+
+function reset() {
+  ready = false;
+  items = Object.create(null);
+}
+
+export async function setLocalPubKey(pubkey: string) {
+  await put('number_id', `${pubkey}.1`);
+}
+
+export function getNumber() {
+  const numberId = get('number_id') as string | undefined;
+  if (numberId === undefined) {
+    return undefined;
+  }
+  return numberId.split('.')[0];
+}
+
+export function isSignInByLinking() {
+  const isByLinking = get('is_sign_in_by_linking');
+  if (isByLinking === undefined) {
+    return false;
+  }
+  return isByLinking;
+}
+
+export async function setSignInByLinking(isLinking: boolean) {
+  await put('is_sign_in_by_linking', isLinking);
+}
+
+export function isSignWithRecoveryPhrase() {
+  const isRecoveryPhraseUsed = get('is_sign_in_recovery_phrase');
+  if (isRecoveryPhraseUsed === undefined) {
+    return false;
+  }
+  return isRecoveryPhraseUsed;
+}
+
+export async function setSignWithRecoveryPhrase(isRecoveryPhraseUsed: boolean) {
+  await put('is_sign_in_recovery_phrase', isRecoveryPhraseUsed);
+}
+
+export function getLastProfileUpdateTimestamp() {
+  return get('last_profile_update_timestamp');
+}
+
+export async function setLastProfileUpdateTimestamp(lastUpdateTimestamp: number) {
+  await put('last_profile_update_timestamp', lastUpdateTimestamp);
+}
+
+export function getCurrentRecoveryPhrase() {
+  return Storage.get('mnemonic') as string;
+}
+
+export async function saveRecoveryPhrase(mnemonic: string) {
+  return Storage.put('mnemonic', mnemonic);
+}
+
+export const Storage = { fetch, put, get, remove, onready, reset };