Skip to content
This repository has been archived by the owner on Jun 15, 2022. It is now read-only.

Commit

Permalink
chore: upgrade snjs (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
moughxyz committed Apr 25, 2022
1 parent eb06632 commit 647b301
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 169 deletions.
8 changes: 3 additions & 5 deletions .eslintrc.js
@@ -1,10 +1,6 @@
module.exports = {
root: true,
extends: [
'@react-native-community',
'prettier',
'./node_modules/@standardnotes/config/src/.eslintrc',
],
extends: ['@react-native-community', 'prettier', './node_modules/@standardnotes/config/src/.eslintrc'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
Expand All @@ -21,6 +17,8 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'warn',
'no-invalid-this': 'warn',
'no-console': 'warn',
eqeqeq: ['warn', 'smart'],
'no-void': 'off',
},
},
],
Expand Down
8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -33,12 +33,12 @@
"@react-navigation/native": "^6.0.10",
"@react-navigation/stack": "^6.2.1",
"@standardnotes/components": "^1.7.15",
"@standardnotes/filepicker": "^1.11.0",
"@standardnotes/filepicker": "^1.13.2",
"@standardnotes/react-native-aes": "^1.4.3",
"@standardnotes/react-native-textview": "1.0.2",
"@standardnotes/react-native-utils": "1.0.1",
"@standardnotes/sncrypto-common": "1.7.6",
"@standardnotes/snjs": "2.97.1",
"@standardnotes/sncrypto-common": "1.7.7",
"@standardnotes/snjs": "2.99.2",
"@standardnotes/stylekit": "5.23.0",
"@types/styled-components-react-native": "5.1.3",
"js-base64": "^3.7.2",
Expand Down Expand Up @@ -84,7 +84,7 @@
"@babel/preset-typescript": "^7.16.7",
"@babel/runtime": "^7.17.9",
"@react-native-community/eslint-config": "^3.0.1",
"@standardnotes/config": "^2.4.0",
"@standardnotes/config": "^2.4.1",
"@types/detox": "^18.1.0",
"@types/faker": "^6.6.9",
"@types/jest": "^27.4.1",
Expand Down
11 changes: 7 additions & 4 deletions src/Lib/Application.ts
Expand Up @@ -45,7 +45,10 @@ export class MobileApplication extends SNApplication {
public editorGroup: NoteGroupController
public iconsController: IconsController
private startedDeinit = false
public Uuid: string // UI remounts when Uuid changes

// UI remounts when Uuid changes
public Uuid: string

static previouslyLaunched = false

constructor(deviceInterface: MobileDeviceInterface, identifier: string) {
Expand All @@ -64,13 +67,13 @@ export class MobileApplication extends SNApplication {
],
defaultHost: IsDev ? 'https://api-dev.standardnotes.com' : 'https://api.standardnotes.com',
appVersion: version,
webSocketUrl: IsDev
? 'wss://sockets-dev.standardnotes.com'
: 'wss://sockets.standardnotes.com',
webSocketUrl: IsDev ? 'wss://sockets-dev.standardnotes.com' : 'wss://sockets.standardnotes.com',
})

this.Uuid = Math.random().toString()
this.editorGroup = new NoteGroupController(this)
this.iconsController = new IconsController()

void this.mobileComponentManager.initialize(this.protocolService)
}

Expand Down
42 changes: 17 additions & 25 deletions src/Lib/InstallationService.ts
@@ -1,10 +1,6 @@
import SNReactNative from '@standardnotes/react-native-utils'
import {
ApplicationService,
ButtonType,
isNullOrUndefined,
StorageValueModes,
} from '@standardnotes/snjs'
import { ApplicationService, ButtonType, isNullOrUndefined, StorageValueModes } from '@standardnotes/snjs'
import { MobileDeviceInterface } from './Interface'

const FIRST_RUN_KEY = 'first_run'

Expand All @@ -21,40 +17,38 @@ export class InstallationService extends ApplicationService {
return this.application?.setValue(FIRST_RUN_KEY, false, StorageValueModes.Nonwrapped)
}

/**
* Needs wipe if has keys but no data. However, since "no data" can be incorrectly reported by underlying
* AsyncStorage failures, we want to confirm with the user before deleting anything.
*/
async needsWipe() {
// Needs wipe if has keys but no data. However, since "no data" can be incorrectly reported by underlying
// AsyncStorage failures, we want to confirm with the user before deleting anything.

const hasNormalKeys = this.application?.hasAccount() || this.application?.hasPasscode()
const keychainKey = await this.application?.deviceInterface?.getRawKeychainValue()
const deviceInterface = this.application?.deviceInterface as MobileDeviceInterface
const keychainKey = await deviceInterface?.getRawKeychainValue()
const hasKeychainValue = !(
isNullOrUndefined(keychainKey) ||
(typeof keychainKey === 'object' && Object.keys(keychainKey).length === 0)
)

const firstRunKey = await this.application?.getValue(
FIRST_RUN_KEY,
StorageValueModes.Nonwrapped
)
const firstRunKey = await this.application?.getValue(FIRST_RUN_KEY, StorageValueModes.Nonwrapped)
let firstRunKeyMissing = isNullOrUndefined(firstRunKey)
/*
* Because of migration failure first run key might not be in non wrapped storage
*/
if (firstRunKeyMissing) {
const fallbackFirstRunValue = await this.application?.deviceInterface?.getRawStorageValue(
FIRST_RUN_KEY
)
const fallbackFirstRunValue = await this.application?.deviceInterface?.getRawStorageValue(FIRST_RUN_KEY)
firstRunKeyMissing = isNullOrUndefined(fallbackFirstRunValue)
}
return !hasNormalKeys && hasKeychainValue && firstRunKeyMissing
}

/**
* On iOS, keychain data is persisted between installs/uninstalls. (https://stackoverflow.com/questions/4747404/delete-keychain-items-when-an-app-is-uninstalled)
* This prevents the user from deleting the app and reinstalling if they forgot their local passocde
* or if fingerprint scanning isn't working. By deleting all data on first run, we allow the user to reset app
* state after uninstall.
*/
async wipeData() {
// On iOS, keychain data is persisted between installs/uninstalls. (https://stackoverflow.com/questions/4747404/delete-keychain-items-when-an-app-is-uninstalled)
// This prevents the user from deleting the app and reinstalling if they forgot their local passocde
// or if fingerprint scanning isn't working. By deleting all data on first run, we allow the user to reset app
// state after uninstall.

const confirmed = await this.application?.alertService?.confirm(
"We've detected a previous installation of Standard Notes based on your keychain data. You must wipe all data from previous installation to continue.\n\nIf you're seeing this message in error, it might mean we're having issues loading your local database. Please restart the app and try again.",
'Previous Installation',
Expand All @@ -65,9 +59,7 @@ export class InstallationService extends ApplicationService {

if (confirmed) {
await this.application?.deviceInterface?.removeAllRawStorageValues()
await this.application?.deviceInterface?.removeAllRawDatabasePayloads(
this.application?.identifier
)
await this.application?.deviceInterface?.removeAllRawDatabasePayloads(this.application?.identifier)
await this.application?.deviceInterface?.clearRawKeychainValue()
} else {
SNReactNative.exitApp()
Expand Down
92 changes: 62 additions & 30 deletions src/Lib/Interface.ts
@@ -1,5 +1,13 @@
import AsyncStorage from '@react-native-community/async-storage'
import { AbstractDevice, ApplicationIdentifier, TransferPayload } from '@standardnotes/snjs'
import {
ApplicationIdentifier,
DeviceInterface,
Environment,
LegacyRawKeychainValue,
NamespacedRootKeyInKeychain,
RawKeychainValue,
TransferPayload,
} from '@standardnotes/snjs'
import { Alert, Linking, Platform } from 'react-native'
import FingerprintScanner from 'react-native-fingerprint-scanner'
import Keychain from './Keychain'
Expand Down Expand Up @@ -35,17 +43,26 @@ const showLoadFailForItemIds = (failedItemIds: string[]) => {
Alert.alert('Unable to load item(s)', text)
}

export class MobileDeviceInterface extends AbstractDevice {
constructor() {
super(setTimeout, setInterval)
}
export class MobileDeviceInterface implements DeviceInterface {
environment: Environment.Mobile = Environment.Mobile

// eslint-disable-next-line @typescript-eslint/no-empty-function
deinit() {}

override deinit() {
super.deinit()
async setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise<void> {
await Keychain.setKeys(value)
}

legacy_setRawKeychainValue(value: any): Promise<void> {
return Keychain.setKeys(value)
public async getJsonParsedRawStorageValue(key: string): Promise<unknown | undefined> {
const value = await this.getRawStorageValue(key)
if (value == undefined) {
return undefined
}
try {
return JSON.parse(value)
} catch (e) {
return value
}
}

private getDatabaseKeyPrefix(identifier: ApplicationIdentifier) {
Expand Down Expand Up @@ -78,7 +95,7 @@ export class MobileDeviceInterface extends AbstractDevice {
results.push({ key, value: item })
}
} catch (e) {
console.log('Error getting item', key, e)
console.error('Error getting item', key, e)
}
}
} else {
Expand All @@ -89,7 +106,7 @@ export class MobileDeviceInterface extends AbstractDevice {
}
}
} catch (e) {
console.log('Error getting items', e)
console.error('Error getting items', e)
}
}
return results
Expand Down Expand Up @@ -151,15 +168,19 @@ export class MobileDeviceInterface extends AbstractDevice {
const keys = await AsyncStorage.getAllKeys()
return this.getRawStorageKeyValues(keys)
}
setRawStorageValue(key: string, value: any): Promise<void> {

setRawStorageValue(key: string, value: string): Promise<void> {
return AsyncStorage.setItem(key, JSON.stringify(value))
}

removeRawStorageValue(key: string): Promise<void> {
return AsyncStorage.removeItem(key)
}

removeAllRawStorageValues(): Promise<void> {
return AsyncStorage.clear()
}

openDatabase(): Promise<{ isNewDatabase?: boolean | undefined } | undefined> {
return Promise.resolve({ isNewDatabase: false })
}
Expand All @@ -171,20 +192,17 @@ export class MobileDeviceInterface extends AbstractDevice {
return this.getDatabaseKeyValues(keys) as Promise<T[]>
}

saveRawDatabasePayload(payload: any, identifier: ApplicationIdentifier): Promise<void> {
saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier): Promise<void> {
return this.saveRawDatabasePayloads([payload], identifier)
}

async saveRawDatabasePayloads(payloads: any[], identifier: ApplicationIdentifier): Promise<void> {
async saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise<void> {
if (payloads.length === 0) {
return
}
await Promise.all(
payloads.map(item => {
return AsyncStorage.setItem(
this.keyForPayloadId(item.uuid, identifier),
JSON.stringify(item)
)
return AsyncStorage.setItem(this.keyForPayloadId(item.uuid, identifier), JSON.stringify(item))
})
)
}
Expand All @@ -196,41 +214,55 @@ export class MobileDeviceInterface extends AbstractDevice {
return AsyncStorage.multiRemove(keys)
}

async getNamespacedKeychainValue(identifier: ApplicationIdentifier) {
async getNamespacedKeychainValue(
identifier: ApplicationIdentifier
): Promise<NamespacedRootKeyInKeychain | undefined> {
const keychain = await this.getRawKeychainValue()

if (isLegacyIdentifier(identifier)) {
return keychain
return keychain as unknown as NamespacedRootKeyInKeychain
}

if (!keychain) {
return
}
return (keychain as any)[identifier]

return keychain[identifier]
}

async setNamespacedKeychainValue(value: any, identifier: ApplicationIdentifier) {
async setNamespacedKeychainValue(
value: NamespacedRootKeyInKeychain,
identifier: ApplicationIdentifier
): Promise<void> {
if (isLegacyIdentifier(identifier)) {
return Keychain.setKeys(value)
await Keychain.setKeys(value)
}

let keychain = await this.getRawKeychainValue()

if (!keychain) {
keychain = {}
}
return Keychain.setKeys({

await Keychain.setKeys({
...keychain,
[identifier]: value,
})
}

async clearNamespacedKeychainValue(identifier: ApplicationIdentifier) {
async clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise<void> {
if (isLegacyIdentifier(identifier)) {
return this.clearRawKeychainValue()
await this.clearRawKeychainValue()
}

const keychain = await this.getRawKeychainValue()

if (!keychain) {
return
}

delete keychain[identifier]
return Keychain.setKeys(keychain)
await Keychain.setKeys(keychain)
}

async getDeviceBiometricsAvailability() {
Expand All @@ -242,12 +274,12 @@ export class MobileDeviceInterface extends AbstractDevice {
}
}

getRawKeychainValue() {
getRawKeychainValue(): Promise<RawKeychainValue | null | undefined> {
return Keychain.getKeys()
}

clearRawKeychainValue() {
return Keychain.clearKeys()
async clearRawKeychainValue(): Promise<void> {
await Keychain.clearKeys()
}

openUrl(url: string) {
Expand Down
19 changes: 5 additions & 14 deletions src/Lib/Keychain.ts
@@ -1,25 +1,18 @@
import { RawKeychainValue } from '@standardnotes/snjs'
import * as RCTKeychain from 'react-native-keychain'

type KeychainValue = Record<string, string>

export default class Keychain {
static async setKeys(keys: object) {
const options = {
/* iOS only */
const iOSOptions = {
accessible: RCTKeychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
}
return RCTKeychain.setGenericPassword('sn', JSON.stringify(keys), options).then(function (
result
) {
console.log('Credentials saved successfully!', result)
})
return RCTKeychain.setGenericPassword('sn', JSON.stringify(keys), iOSOptions)
}

static async getKeys(): Promise<KeychainValue | undefined | null> {
static async getKeys(): Promise<RawKeychainValue | undefined | null> {
return RCTKeychain.getGenericPassword()
.then(function (credentials) {
if (!credentials || !credentials.password) {
console.log('Keychain value empty')
return null
} else {
const keys = JSON.parse(credentials.password)
Expand All @@ -33,8 +26,6 @@ export default class Keychain {
}

static async clearKeys() {
return RCTKeychain.resetGenericPassword().then(function () {
console.log('Credentials successfully deleted')
})
return RCTKeychain.resetGenericPassword()
}
}

0 comments on commit 647b301

Please sign in to comment.