diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-06-15 17:40:18 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-06-15 17:40:18 -0500 |
commit | 77b938845aa909a70f896b759b04ba7c1b1d9aa6 (patch) | |
tree | 0d5f65c3efa2c7702f4c5eee024248b73ec36f07 /src | |
parent | b2dd8d4f440243ac2eb12e7013d5a024b4e95f07 (diff) | |
download | voidsky-77b938845aa909a70f896b759b04ba7c1b1d9aa6.tar.zst |
Polyfills for native crypto
Diffstat (limited to 'src')
-rw-r--r-- | src/App.native.tsx | 3 | ||||
-rw-r--r-- | src/api/auth.ts | 68 | ||||
-rw-r--r-- | src/env.ts | 6 | ||||
-rw-r--r-- | src/platform/polyfills.native.ts | 21 | ||||
-rw-r--r-- | src/platform/polyfills.web.ts | 1 | ||||
-rw-r--r-- | src/state/auth.ts | 142 | ||||
-rw-r--r-- | src/state/env.ts | 6 | ||||
-rw-r--r-- | src/state/index.ts | 2 | ||||
-rw-r--r-- | src/state/models/session.ts | 2 |
9 files changed, 175 insertions, 76 deletions
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 |