about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--README.md9
-rw-r--r--babel.config.js15
-rw-r--r--ios/Podfile.lock6
-rw-r--r--metro.config.js13
-rw-r--r--package.json4
-rw-r--r--src/App.native.tsx3
-rw-r--r--src/api/auth.ts68
-rw-r--r--src/env.ts6
-rw-r--r--src/platform/polyfills.native.ts21
-rw-r--r--src/platform/polyfills.web.ts1
-rw-r--r--src/state/auth.ts142
-rw-r--r--src/state/env.ts6
-rw-r--r--src/state/index.ts2
-rw-r--r--src/state/models/session.ts2
-rw-r--r--yarn.lock26
15 files changed, 243 insertions, 81 deletions
diff --git a/README.md b/README.md
index 779a78556..ba8b3cf25 100644
--- a/README.md
+++ b/README.md
@@ -24,10 +24,17 @@ Uses:
   - Web: `yarn web`
 - Tips
   - `npx react-native info` Checks what has been installed.
+  - On M1 macs, you need to exclude "arm64" from the target architectures
+    - Annoyingly this must be re-set via XCode after every pod install
 
 ## Various notes
 
 - ["SSO" flows on mobile](https://developer.okta.com/blog/2022/01/13/mobile-sso)
   - Suggests we might want to use `ASWebAuthenticationSession` on iOS
   - [react-native-inappbrowser-reborn](https://www.npmjs.com/package/react-native-inappbrowser-reborn) with `openAuth: true` might be worth exploring
-  - We might even [get rejected by the app store](https://community.auth0.com/t/react-native-ios-app-rejected-on-appstore-for-using-react-native-auth0/36793) if we don't
\ No newline at end of file
+  - We might even [get rejected by the app store](https://community.auth0.com/t/react-native-ios-app-rejected-on-appstore-for-using-react-native-auth0/36793) if we don't
+- Cryptography
+  - We rely on [isomorphic-webcrypto](https://github.com/kevlened/isomorphic-webcrypto)
+  - For the CRNG this uses [react-native-securerandom](https://github.com/robhogan/react-native-securerandom) which provides proper random on mobile
+  - For the crypto this uses [msrcrypto](https://github.com/kevlened/msrCrypto) - but we should consider switching to [the MS maintained version](https://github.com/microsoft/MSR-JavaScript-Crypto)
+  - In the future it might be preferable to move off of msrcrypto and use iOS and Android native modules, but nothing is available right now
\ No newline at end of file
diff --git a/babel.config.js b/babel.config.js
index cf1f9fbbc..e11e3a9df 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,3 +1,18 @@
 module.exports = {
   presets: ['module:metro-react-native-babel-preset'],
+  plugins: [
+    [
+      'module:react-native-dotenv',
+      {
+        // envName: 'APP_ENV',
+        moduleName: '@env',
+        path: '.env',
+        blocklist: null,
+        allowlist: null,
+        safe: false,
+        allowUndefined: true,
+        verbose: false,
+      },
+    ],
+  ],
 }
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 8be1857f2..63cc844f3 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -296,6 +296,8 @@ PODS:
   - RNScreens (3.13.1):
     - React-Core
     - React-RCTImage
+  - RNSecureRandom (1.0.0):
+    - React
   - Yoga (1.14.0)
 
 DEPENDENCIES:
@@ -335,6 +337,7 @@ DEPENDENCIES:
   - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
   - RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
   - RNScreens (from `../node_modules/react-native-screens`)
+  - RNSecureRandom (from `../node_modules/react-native-securerandom`)
   - Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
 
 SPEC REPOS:
@@ -410,6 +413,8 @@ EXTERNAL SOURCES:
     :path: "../node_modules/react-native-inappbrowser-reborn"
   RNScreens:
     :path: "../node_modules/react-native-screens"
+  RNSecureRandom:
+    :path: "../node_modules/react-native-securerandom"
   Yoga:
     :path: "../node_modules/react-native/ReactCommon/yoga"
 
@@ -449,6 +454,7 @@ SPEC CHECKSUMS:
   RNCAsyncStorage: 466b9df1a14bccda91da86e0b7d9a345d78e1673
   RNInAppBrowser: 3ff3a3b8f458aaf25aaee879d057352862edf357
   RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
+  RNSecureRandom: 0dcee021fdb3d50cd5cee5db0ebf583c42f5af0e
   Yoga: 99652481fcd320aefa4a7ef90095b95acd181952
 
 PODFILE CHECKSUM: cf94853ebcb0d8e0d027dca9ab7a4ede886a8f20
diff --git a/metro.config.js b/metro.config.js
index 9c99c9e98..9dfaece89 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -11,9 +11,11 @@ console.log(metroResolver)
 module.exports = {
   resolver: {
     resolveRequest: (context, moduleName, platform) => {
+      // HACK
       // metro doesn't support the "exports" directive in package.json
       // so we have to manually fix some imports
       // see https://github.com/facebook/metro/issues/670
+      // -prf
       if (moduleName.startsWith('ucans')) {
         const subpath = moduleName.split('/').slice(1)
         if (subpath.length === 0) {
@@ -34,14 +36,19 @@ module.exports = {
           filePath,
         }
       }
+      // HACK
+      // this module has the same problem with the "exports" module
+      // but also we need modules to use our version of webcrypto
+      // so here we're routing to a module we define
+      // -prf
       if (moduleName === 'one-webcrypto') {
         return {
           type: 'sourceFile',
           filePath: path.join(
             context.projectRoot,
-            'node_modules',
-            'one-webcrypto',
-            'browser.mjs',
+            'src',
+            'platform',
+            'polyfills.native.ts',
           ),
         }
       }
diff --git a/package.json b/package.json
index 635bc7fe3..af95df9eb 100644
--- a/package.json
+++ b/package.json
@@ -20,15 +20,18 @@
     "@react-navigation/native": "^6.0.10",
     "@react-navigation/native-stack": "^6.6.2",
     "@react-navigation/stack": "^6.2.1",
+    "@zxing/text-encoding": "^0.9.0",
     "mobx": "^6.6.0",
     "mobx-react-lite": "^3.4.0",
     "mobx-state-tree": "^5.1.5",
+    "msrcrypto": "^1.5.8",
     "react": "17.0.2",
     "react-dom": "17.0.2",
     "react-native": "0.68.2",
     "react-native-inappbrowser-reborn": "^3.6.3",
     "react-native-safe-area-context": "^4.3.1",
     "react-native-screens": "^3.13.1",
+    "react-native-securerandom": "^1.0.0",
     "react-native-web": "^0.17.7",
     "ucans": "0.9.1"
   },
@@ -49,6 +52,7 @@
     "eslint": "^7.32.0",
     "jest": "^26.6.3",
     "metro-react-native-babel-preset": "^0.67.0",
+    "react-native-dotenv": "^3.3.1",
     "react-scripts": "^5.0.1",
     "react-test-renderer": "17.0.2",
     "typescript": "^4.4.4"
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 2fadf993f..511a8401a 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -1,4 +1,5 @@
 import React, {useState, useEffect} from 'react'
+import {whenWebCrypto} from './platform/polyfills.native'
 import {RootStore, setupState, RootStoreProvider} from './state'
 import * as Routes from './routes'
 
@@ -7,7 +8,7 @@ function App() {
 
   // init
   useEffect(() => {
-    setupState().then(setRootStore)
+    whenWebCrypto.then(() => setupState()).then(setRootStore)
   }, [])
 
   // show nothing prior to init
diff --git a/src/api/auth.ts b/src/api/auth.ts
deleted file mode 100644
index 60ff1a3f2..000000000
--- a/src/api/auth.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import {Linking} from 'react-native'
-import * as auth from '@adxp/auth'
-import {InAppBrowser} from 'react-native-inappbrowser-reborn'
-import {isWeb} from '../platform/detection'
-import {makeAppUrl} from '../platform/urls'
-import * as env from '../env'
-
-const SCOPE = auth.writeCap(
-  'did:key:z6MkfRiFMLzCxxnw6VMrHK8pPFt4QAHS3jX3XM87y9rta6kP',
-  'did:example:microblog',
-)
-
-export async function isAuthed(authStore: auth.BrowserStore) {
-  return await authStore.hasUcan(SCOPE)
-}
-
-export async function logout(authStore: auth.BrowserStore) {
-  await authStore.reset()
-}
-
-export async function parseUrlForUcan() {
-  // @ts-ignore window is defined -prf
-  const fragment = window.location.hash
-  if (fragment.length < 1) {
-    return undefined
-  }
-  try {
-    const ucan = await auth.parseLobbyResponseHashFragment(fragment)
-    // @ts-ignore window is defined -prf
-    window.location.hash = ''
-    return ucan
-  } catch (err) {
-    return undefined
-  }
-}
-
-export async function requestAppUcan(authStore: auth.BrowserStore) {
-  const did = await authStore.getDid()
-  const returnUrl = makeAppUrl()
-  const fragment = auth.requestAppUcanHashFragment(did, SCOPE, returnUrl)
-  const url = `${env.AUTH_LOBBY}#${fragment}`
-
-  if (isWeb) {
-    // @ts-ignore window is defined -prf
-    window.location.href = url
-    return false
-  }
-
-  if (await InAppBrowser.isAvailable()) {
-    const res = await InAppBrowser.openAuth(url, returnUrl, {
-      // iOS Properties
-      ephemeralWebSession: false,
-      // Android Properties
-      showTitle: false,
-      enableUrlBarHiding: true,
-      enableDefaultShare: false,
-    })
-    if (res.type === 'success' && res.url) {
-      Linking.openURL(res.url)
-    } else {
-      console.error('Bad response', res)
-      return false
-    }
-  } else {
-    Linking.openURL(url)
-  }
-  return true
-}
diff --git a/src/env.ts b/src/env.ts
index 78fb88acc..47bceac77 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -1,5 +1,7 @@
-if (typeof process.env.REACT_APP_AUTH_LOBBY !== 'string') {
+import {REACT_APP_AUTH_LOBBY} from '@env'
+
+if (typeof REACT_APP_AUTH_LOBBY !== 'string') {
   throw new Error('ENV: No auth lobby provided')
 }
 
-export const AUTH_LOBBY = process.env.REACT_APP_AUTH_LOBBY
+export const AUTH_LOBBY = REACT_APP_AUTH_LOBBY
diff --git a/src/platform/polyfills.native.ts b/src/platform/polyfills.native.ts
new file mode 100644
index 000000000..b4d38f04f
--- /dev/null
+++ b/src/platform/polyfills.native.ts
@@ -0,0 +1,21 @@
+import {generateSecureRandom} from 'react-native-securerandom'
+import crypto from 'msrcrypto'
+import '@zxing/text-encoding' // TextEncoder / TextDecoder
+
+export const whenWebCrypto = new Promise(async (resolve, reject) => {
+  try {
+    const bytes = await generateSecureRandom(48)
+    crypto.initPrng(Array.from(bytes))
+
+    // @ts-ignore global.window exists -prf
+    if (!global.window.crypto) {
+      // @ts-ignore global.window exists -prf
+      global.window.crypto = crypto
+    }
+    resolve(true)
+  } catch (e: any) {
+    reject(e)
+  }
+})
+
+export const webcrypto = crypto
diff --git a/src/platform/polyfills.web.ts b/src/platform/polyfills.web.ts
new file mode 100644
index 000000000..c6035e5e3
--- /dev/null
+++ b/src/platform/polyfills.web.ts
@@ -0,0 +1 @@
+// do nothing
diff --git a/src/state/auth.ts b/src/state/auth.ts
new file mode 100644
index 000000000..b49a11d90
--- /dev/null
+++ b/src/state/auth.ts
@@ -0,0 +1,142 @@
+import {Linking} from 'react-native'
+import * as auth from '@adxp/auth'
+import * as ucan from 'ucans'
+import {InAppBrowser} from 'react-native-inappbrowser-reborn'
+import {isWeb} from '../platform/detection'
+import {makeAppUrl} from '../platform/urls'
+import * as storage from './storage'
+import * as env from '../env'
+
+const SCOPE = auth.writeCap(
+  'did:key:z6MkfRiFMLzCxxnw6VMrHK8pPFt4QAHS3jX3XM87y9rta6kP',
+  'did:example:microblog',
+)
+
+export async function isAuthed(authStore: ReactNativeStore) {
+  return await authStore.hasUcan(SCOPE)
+}
+
+export async function logout(authStore: ReactNativeStore) {
+  await authStore.reset()
+}
+
+export async function parseUrlForUcan() {
+  if (isWeb) {
+    // @ts-ignore window is defined -prf
+    const fragment = window.location.hash
+    if (fragment.length < 1) {
+      return undefined
+    }
+    try {
+      const ucan = await auth.parseLobbyResponseHashFragment(fragment)
+      // @ts-ignore window is defined -prf
+      window.location.hash = ''
+      return ucan
+    } catch (err) {
+      return undefined
+    }
+  } else {
+    // TODO
+  }
+}
+
+export async function requestAppUcan(authStore: ReactNativeStore) {
+  const did = await authStore.getDid()
+  const returnUrl = makeAppUrl()
+  const fragment = auth.requestAppUcanHashFragment(did, SCOPE, returnUrl)
+  const url = `${env.AUTH_LOBBY}#${fragment}`
+
+  if (isWeb) {
+    // @ts-ignore window is defined -prf
+    window.location.href = url
+    return false
+  }
+
+  if (await InAppBrowser.isAvailable()) {
+    const res = await InAppBrowser.openAuth(url, returnUrl, {
+      // iOS Properties
+      ephemeralWebSession: false,
+      // Android Properties
+      showTitle: false,
+      enableUrlBarHiding: true,
+      enableDefaultShare: false,
+    })
+    if (res.type === 'success' && res.url) {
+      Linking.openURL(res.url)
+    } else {
+      console.error('Bad response', res)
+      return false
+    }
+  } else {
+    Linking.openURL(url)
+  }
+  return true
+}
+
+export class ReactNativeStore extends auth.AuthStore {
+  private keypair: ucan.EdKeypair
+  private ucanStore: ucan.Store
+
+  constructor(keypair: ucan.EdKeypair, ucanStore: ucan.Store) {
+    super()
+    this.keypair = keypair
+    this.ucanStore = ucanStore
+  }
+
+  static async load(): Promise<ReactNativeStore> {
+    const keypair = await ReactNativeStore.loadOrCreateKeypair()
+
+    const storedUcans = await ReactNativeStore.getStoredUcanStrs()
+    const ucanStore = await ucan.Store.fromTokens(storedUcans)
+
+    return new ReactNativeStore(keypair, ucanStore)
+  }
+
+  static async loadOrCreateKeypair(): Promise<ucan.EdKeypair> {
+    const storedKey = await storage.loadString('adxKey')
+    if (storedKey) {
+      return ucan.EdKeypair.fromSecretKey(storedKey)
+    } else {
+      // @TODO: again just stand in since no actual root keys
+      const keypair = await ucan.EdKeypair.create({exportable: true})
+      storage.saveString('adxKey', await keypair.export())
+      return keypair
+    }
+  }
+
+  static async getStoredUcanStrs(): Promise<string[]> {
+    const storedStr = await storage.loadString('adxUcans')
+    if (!storedStr) {
+      return []
+    }
+    return storedStr.split(',')
+  }
+
+  static setStoredUcanStrs(ucans: string[]): void {
+    storage.saveString('adxUcans', ucans.join(','))
+  }
+
+  protected async getKeypair(): Promise<ucan.EdKeypair> {
+    return this.keypair
+  }
+
+  async addUcan(token: ucan.Chained): Promise<void> {
+    this.ucanStore.add(token)
+    const storedUcans = await ReactNativeStore.getStoredUcanStrs()
+    ReactNativeStore.setStoredUcanStrs([...storedUcans, token.encoded()])
+  }
+
+  async getUcanStore(): Promise<ucan.Store> {
+    return this.ucanStore
+  }
+
+  async clear(): Promise<void> {
+    storage.clear()
+  }
+
+  async reset(): Promise<void> {
+    this.clear()
+    this.keypair = await ReactNativeStore.loadOrCreateKeypair()
+    this.ucanStore = await ucan.Store.fromTokens([])
+  }
+}
diff --git a/src/state/env.ts b/src/state/env.ts
index f47a7037f..c1e11ebd0 100644
--- a/src/state/env.ts
+++ b/src/state/env.ts
@@ -4,17 +4,17 @@
  */
 
 import {getEnv, IStateTreeNode} from 'mobx-state-tree'
-import * as auth from '@adxp/auth'
+import {ReactNativeStore} from './auth'
 import {API} from '../api'
 
 export class Environment {
   api = new API()
-  authStore?: auth.BrowserStore
+  authStore?: ReactNativeStore
 
   constructor() {}
 
   async setup() {
-    this.authStore = await auth.BrowserStore.load()
+    this.authStore = await ReactNativeStore.load()
   }
 }
 
diff --git a/src/state/index.ts b/src/state/index.ts
index 0e70055e0..fa7c9518d 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -6,7 +6,7 @@ import {
 } from './models/root-store'
 import {Environment} from './env'
 import * as storage from './storage'
-import * as auth from '../api/auth'
+import * as auth from './auth'
 
 const ROOT_STATE_STORAGE_KEY = 'root'
 
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index 06c4bb1aa..c032d7594 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -1,6 +1,6 @@
 import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
 // import {UserConfig} from '../../api'
-import * as auth from '../../api/auth'
+import * as auth from '../auth'
 import {withEnvironment} from '../env'
 
 export const SessionModel = types
diff --git a/yarn.lock b/yarn.lock
index e6d92d256..b0eb1149e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3067,6 +3067,11 @@
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
 
+"@zxing/text-encoding@^0.9.0":
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b"
+  integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==
+
 abab@^2.0.3, abab@^2.0.5:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
@@ -3833,7 +3838,7 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
-base64-js@^1.1.2, base64-js@^1.3.1, base64-js@^1.5.1:
+base64-js@*, base64-js@^1.1.2, base64-js@^1.3.1, base64-js@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
   integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@@ -9411,6 +9416,11 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
+msrcrypto@^1.5.8:
+  version "1.5.8"
+  resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c"
+  integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q==
+
 multicast-dns@^7.2.5:
   version "7.2.5"
   resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced"
@@ -11016,6 +11026,13 @@ react-native-codegen@^0.0.17:
     jscodeshift "^0.13.1"
     nullthrows "^1.1.1"
 
+react-native-dotenv@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.3.1.tgz#8f399cf28ca77d860d8e7f7323e439fa60a8ca0b"
+  integrity sha512-gAKXout1XCwCqJ3QPGoQAF2eRzOHgOnwg3x19z+ssow8bDIksJeKBqvoHDyGziVilAIP1J0bEC9Jf+VF8nFang==
+  dependencies:
+    dotenv "^10.0.0"
+
 react-native-gradle-plugin@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.6.tgz#b61a9234ad2f61430937911003cddd7e15c72b45"
@@ -11042,6 +11059,13 @@ react-native-screens@^3.13.1:
     react-freeze "^1.0.0"
     warn-once "^0.1.0"
 
+react-native-securerandom@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/react-native-securerandom/-/react-native-securerandom-1.0.0.tgz#1cff2f727c90c9ec3318b42dbf825a628b53b49b"
+  integrity sha512-lnhcsWloFzMN/HffyDBlh4VlqdhDH8uxEzUIH3aJPgC1PxV6OKZkvAk409EwsAhcmG/z3yZuVKegKpUr5IM9ug==
+  dependencies:
+    base64-js "*"
+
 react-native-web@^0.17.7:
   version "0.17.7"
   resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.17.7.tgz#038899dbc94467a0ca0be214b88a30e0c117b176"