import React, {Component, createRef} from 'react'; import {TouchableOpacity, View} from 'react-native'; import * as Keychain from 'react-native-keychain'; import Share from 'react-native-share'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import {notesnook} from '../../../e2e/test.ids'; import {Actions} from '../../provider/Actions'; import {DDS} from '../../services/DeviceDetection'; import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent, sendNoteEditedEvent, ToastEvent, } from '../../services/EventManager'; import {getElevation, toTXT} from '../../utils'; import {db} from '../../utils/DB'; import { eCloseVaultDialog, eOnLoadNote, eOpenVaultDialog, refreshNotesPage, } from '../../utils/Events'; import {tabBarRef} from '../../utils/Refs'; import {ph, pv, SIZE, WEIGHT} from '../../utils/SizeUtils'; import {Button} from '../Button'; import BaseDialog from '../Dialog/base-dialog'; import DialogButtons from '../Dialog/dialog-buttons'; import DialogHeader from '../Dialog/dialog-header'; import {updateEvent} from '../DialogManager/recievers'; import Input from '../Input'; import {Toast} from '../Toast'; import Paragraph from '../Typography/Paragraph'; const passInputRef = createRef(); const confirmPassRef = createRef(); const changePassInputRef = createRef(); export class VaultDialog extends Component { constructor(props) { super(props); this.state = { visible: false, wrongPassword: false, loading: false, note: {}, vault: false, locked: true, permanant: false, goToEditor: false, share: false, passwordsDontMatch: false, deleteNote: false, focusIndex: null, biometricUnlock: false, isBiometryEnrolled: false, isBiometryAvailable: false, fingerprintAccess: false, changePassword: false, copyNote: false, revokeFingerprintAccess: false, }; this.password = null; this.confirmPassword = null; this.newPassword = null; } componentDidMount() { eSubscribeEvent(eOpenVaultDialog, this.open); eSubscribeEvent(eCloseVaultDialog, this.close); } componentWillUnmount() { eUnSubscribeEvent(eOpenVaultDialog, this.open); eUnSubscribeEvent(eCloseVaultDialog, this.close); } /** * * @param {import('../../services/EventManager').vaultType} data */ open = async (data) => { let biometry = await Keychain.getSupportedBiometryType(); let available = false; let fingerprint = await Keychain.hasInternetCredentials('nn_vault'); if ( biometry === Keychain.BIOMETRY_TYPE.FINGERPRINT || biometry === Keychain.BIOMETRY_TYPE.TOUCH_ID ) { available = true; } this.setState({ note: data.item, novault: data.novault, locked: data.locked, permanant: data.permanant, goToEditor: data.goToEditor, share: data.share, deleteNote: data.deleteNote, copyNote: data.copyNote, isBiometryAvailable: available, biometricUnlock: fingerprint, isBiometryEnrolled: fingerprint, fingerprintAccess: data.fingerprintAccess, changePassword: data.changePassword, revokeFingerprintAccess: data.revokeFingerprintAccess, }); if ( fingerprint && data.novault && !data.fingerprintAccess && !data.revokeFingerprintAccess ) { await this._onPressFingerprintAuth(); } else { this.setState({ visible: true, }); } }; close = () => { if (this.state.loading) { ToastEvent.show( 'Please wait and do not close the app.', 'success', 'local', ); return; } updateEvent({type: Actions.NOTES}); this.password = null; this.confirmPassword = null; this.setState({ visible: false, note: {}, locked: false, permanant: false, goToEditor: false, share: false, novault: false, deleteNote: false, passwordsDontMatch: false, }); }; onPress = async () => { if (this.state.revokeFingerprintAccess) { await this._revokeFingerprintAccess(); close(); return; } if (this.state.loading) return; if (!this.password) { ToastEvent.show('You must fill all the fields.', 'error', 'local'); return; } if (this.password && this.password.length < 3) { ToastEvent.show('Password too short', 'error', 'local'); return; } if (!this.state.novault) { if (this.password !== this.confirmPassword) { ToastEvent.show('Passwords do not match', 'error', 'local'); this.setState({ passwordsDontMatch: true, }); return; } this._createVault(); } else if (this.state.changePassword) { this.setState({ loading: true, }); db.vault .changePassword(this.password, this.newPassword) .then((result) => { this.setState({ loading: false, }); if (this.state.biometricUnlock) { this._enrollFingerprint(this.newPassword); } ToastEvent.show('Vault password changed', 'success'); this.close(); }) .catch((e) => { this.setState({ loading: false, }); if (e.message === db.vault.ERRORS.wrongPassword) { ToastEvent.show('Current password incorrect.', 'error', 'local'); } }); } else if (this.state.locked) { if (!this.password || this.password.trim() === 0) { ToastEvent.show('Password is invalid', 'error', 'local'); this.setState({ wrongPassword: true, }); return; } db.vault .unlock(this.password) .then(async () => { this.setState({ wrongPassword: false, }); if (this.state.note.locked) { await this._unlockNote(); } else { await this._lockNote(); } }) .catch((e) => { this._takeErrorAction(e); }); } else if (this.state.fingerprintEnroll) { this._enrollFingerprint(this.password); } }; async _lockNote() { if (!this.password || this.password.trim() === 0) { ToastEvent.show('Password is invalid', 'error', 'local'); return; } else { db.vault.add(this.state.note.id).then((e) => { this.close(); }); } } async _unlockNote() { if (!this.password || this.password.trim() === 0) { ToastEvent.show('Password is invalid', 'error', 'local'); return; } if (this.state.permanant) { this._permanantUnlock(); } else { await this._openNote(); } } _openNote = async () => { try { let note = await db.vault.open(this.state.note.id, this.password); if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) { await this._enrollFingerprint(this.password); } if (this.state.goToEditor) { this._openInEditor(note); } else if (this.state.share) { this._shareNote(note); } else if (this.state.deleteNote) { await this._deleteNote(); } else if (this.state.copyNote) { this._copyNote(note); } } catch (e) { this._takeErrorAction(e); } }; async _deleteNote() { await db.notes.delete(this.state.note.id); updateEvent({type: Actions.NOTES}); updateEvent({type: Actions.FAVORITES}); eSendEvent(refreshNotesPage); this.close(); ToastEvent.show('Note deleted', 'success', 'local'); } async _enrollFingerprint(password) { try { await Keychain.setInternetCredentials('nn_vault', 'nn_vault', password, { accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE, authenticationPrompt: {cancel: null}, accessible: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS, }); } catch (e) { this._takeErrorAction(e); } } async _createVault() { await db.vault.create(this.password); if (this.state.biometricUnlock) { await this._enrollFingerprint(); } if (this.state.note && this.state.note.id && !this.state.note.locked) { await db.vault.add(this.state.note.id); this.close(); ToastEvent.show('Note added to vault', 'success', 'local'); } else { eSendEvent('vaultUpdated'); this.close(); } } _permanantUnlock() { db.vault .remove(this.state.note.id, this.password) .then((r) => { console.log(r, 'unocking result'); sendNoteEditedEvent(this.state.note.id); updateEvent({type: Actions.NOTES}); eSendEvent(refreshNotesPage); this.close(); }) .catch((e) => { console.log(e, 'unlocking error'); this._takeErrorAction(e); }); } _openInEditor(note) { eSendEvent(eOnLoadNote, note); if (!DDS.isTab) { tabBarRef.current?.goToPage(1); } this.close(); } _copyNote(note) { let text = toTXT(note.content.data); let m = `${note.title}\n \n ${text}`; Clipboard.setString(text); ToastEvent.show('Note copied to clipboard', 'success', 'local'); this.close(); } _shareNote(note) { let text = toTXT(note.content.data); let m = `${note.title}\n \n ${text}`; Share.open({ title: 'Share note to', failOnCancel: false, message: m, }); this.close(); } _takeErrorAction(e) { if (e.message === db.vault.ERRORS.wrongPassword) { ToastEvent.show('Password is incorrect', 'error', 'local'); this.setState({ wrongPassword: true, }); return; } else { } } _revokeFingerprintAccess = async () => { try { await Keychain.resetInternetCredentials('nn_vault', { accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE, authenticationType: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS, authenticationPrompt: { cancel: null, }, }); eSendEvent('vaultUpdated'); ToastEvent.show('Fingerprint access revoked!', 'success'); } catch (e) { ToastEvent.show(e.message, 'error', 'local'); } }; _onPressFingerprintAuth = async () => { try { let credentials = await Keychain.getInternetCredentials('nn_vault', { accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE, authenticationType: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS, authenticationPrompt: { cancel: null, }, }); if (credentials?.password) { this.password = credentials.password; this.onPress(); } } catch (e) { this.setState({ visible: true, }); ToastEvent.show('Fingerprint Authentication Canceled', 'error', 'local'); } }; render() { const {colors} = this.props; const { note, visible, wrongPassword, passwordsDontMatch, novault, locked, permanant, biometricUnlock, deleteNote, share, goToEditor, fingerprintAccess, changePassword, copyNote, loading, } = this.state; if (!visible) return null; return ( { passInputRef.current?.focus(); }} statusBarTranslucent={false} onRequestClose={this.close} visible={true}> {(novault || changePassword) && !this.state.revokeFingerprintAccess ? ( <> { this.password = value; }} marginBottom={ !this.state.biometricUnlock || !this.state.isBiometryEnrolled || !novault || changePassword ? 0 : 10 } secureTextEntry placeholder={changePassword ? 'Current Password' : 'Password'} /> {!this.state.biometricUnlock || !this.state.isBiometryEnrolled || !novault || changePassword ? null : (