mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
mobile: at rest encryption
This commit is contained in:
committed by
Abdullah Atta
parent
8722157f33
commit
aec4c510f1
@@ -43,6 +43,18 @@ I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
|
||||
// How app lock works
|
||||
// 1. User goes to settings and setup app lock with a Pin/Password.
|
||||
// 2. The Pin/Password is used to encrypt a random value or user's encryption key.
|
||||
// 3. The encrypted value is stored in MMKV
|
||||
// 4. When the app launches, the same value is decrypted with user provided key, if it works, we launch the app otherwise it remains locked.
|
||||
// 5. If Biometrics are enabled, the app lock pin/password is stored in keychain. the value can be accessed if fingerprint auth works ONLY.
|
||||
// 6. User can manually enter the pin if biometrics fails.
|
||||
// 7. There is no way to enter the app if user forgets the PIN. The only way is to reset app data and start fresh again.
|
||||
|
||||
// How to handle app lock for existing users...
|
||||
// 1.
|
||||
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
|
||||
@@ -17,16 +17,30 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ProcessingModes, MMKVLoader } from "react-native-mmkv-storage";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
@@ -37,50 +51,194 @@ const KEYSTORE_CONFIG = Platform.select({
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||
const key = getDatabaseKey();
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
"notesnook_applock_key"
|
||||
);
|
||||
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||
MMKV.setMap("databaseKeyCipher", databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials("notesnook:db");
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem("databaseKeyCipher");
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
try {
|
||||
console.log("key", appLockPassword);
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
"notesnook_applock_key_salt"
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, "applock_password");
|
||||
|
||||
CipherStorage.setMap("appLockCipher", encrypted);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem("appLockCipher");
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap("appLockCipher");
|
||||
if (!appLockCipher) return true;
|
||||
const decrypted = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
appLockCipher
|
||||
);
|
||||
return decrypted === "applock_password";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
try {
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
console.log("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
return databaseKey;
|
||||
}
|
||||
|
||||
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
if (hasKey) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
console.log("Getting database key from Keychain");
|
||||
DB_KEY = credentials.password;
|
||||
return credentials.password;
|
||||
}
|
||||
console.log("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
"notesnook_database_key"
|
||||
);
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
derivedDatabaseKey.key,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: derivedDatabaseKey.key
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap("userKeyCipher", userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
console.log("Migrated user credentials to cipher");
|
||||
}
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key;
|
||||
|
||||
return derivedDatabaseKey.key;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(name, data) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
name,
|
||||
credentials.key,
|
||||
KEYSTORE_CONFIG
|
||||
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey()
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap("userKeyCipher", userKeyCipher);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeIOSKeychain(username, password) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
username,
|
||||
password,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// upgrades ios keychain to use accessGroups
|
||||
// so we have access to keychain in share extension.
|
||||
await upgradeIOSKeychain(credentials.username, credentials.password);
|
||||
return credentials.password;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const keyCipher = MMKV.getMap("userKeyCipher");
|
||||
if (!key) return null;
|
||||
|
||||
const key = decrypt(
|
||||
{
|
||||
key: await getDatabaseKey()
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -88,8 +246,9 @@ export async function getCryptoKey(_name) {
|
||||
|
||||
export async function removeCryptoKey(_name) {
|
||||
try {
|
||||
let result = await Keychain.resetInternetCredentials("notesnook");
|
||||
return result;
|
||||
MMKV.removeItem("userKeyCipher");
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
@@ -51,27 +52,35 @@ database.host(
|
||||
}
|
||||
);
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
return new RNSqliteDriver({ async: true, dbName: name });
|
||||
},
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}),
|
||||
tempStore: "memory"
|
||||
}
|
||||
});
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key)
|
||||
throw new Error("Database setup failed, could not get database key");
|
||||
|
||||
console.log("Opening database with key:", key);
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
return new RNSqliteDriver({ async: true, dbName: name });
|
||||
},
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}),
|
||||
tempStore: "memory",
|
||||
password: key
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -35,6 +35,7 @@ import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { validateAppLockPassword } from "../../common/database/encryption";
|
||||
|
||||
const AppLockedOverlay = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -45,11 +46,24 @@ const AppLockedOverlay = () => {
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const password = useRef<string>();
|
||||
const appState = useAppState();
|
||||
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
const appLockHasPasswordSecurity = useSettingStore(
|
||||
(state) => state.settings.appLockHasPasswordSecurity
|
||||
);
|
||||
const biometricsAuthEnabled = useSettingStore(
|
||||
(state) =>
|
||||
state.settings.biometricsAuthEnabled === true ||
|
||||
(state.settings.biometricsAuthEnabled === undefined &&
|
||||
!state.settings.appLockHasPasswordSecurity)
|
||||
);
|
||||
|
||||
const onUnlockAppRequested = useCallback(async () => {
|
||||
if (!(await BiometricService.isBiometryAvailable())) return;
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
)
|
||||
return;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await NotesnookModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
@@ -69,12 +83,15 @@ const AppLockedOverlay = () => {
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
useSettingStore.getState().setRequestBiometrics(false);
|
||||
}, 1);
|
||||
}, [lockApp]);
|
||||
}, [biometricsAuthEnabled, lockApp]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password.current) return;
|
||||
try {
|
||||
const unlocked = await db.user.verifyPassword(password.current);
|
||||
const unlocked = appLockHasPasswordSecurity
|
||||
? validateAppLockPassword(password.current)
|
||||
: await db.user.verifyPassword(password.current);
|
||||
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
@@ -97,109 +114,116 @@ const AppLockedOverlay = () => {
|
||||
}
|
||||
}, [appState, onUnlockAppRequested, appLocked]);
|
||||
|
||||
return (
|
||||
appLocked && (
|
||||
return appLocked ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
width:
|
||||
deviceMode !== "mobile"
|
||||
? "50%"
|
||||
: Platform.OS == "ios"
|
||||
? "95%"
|
||||
: "100%",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<IconButton
|
||||
name="fingerprint"
|
||||
size={100}
|
||||
customStyle={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
marginBottom: 20,
|
||||
marginTop: user ? 0 : 50
|
||||
}}
|
||||
onPress={onUnlockAppRequested}
|
||||
color={colors.primary.border}
|
||||
/>
|
||||
<Heading
|
||||
color={colors.primary.heading}
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
width:
|
||||
deviceMode !== "mobile"
|
||||
? "50%"
|
||||
: Platform.OS == "ios"
|
||||
? "95%"
|
||||
: "100%",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15,
|
||||
alignSelf: "center"
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="fingerprint"
|
||||
size={100}
|
||||
customStyle={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
marginBottom: 20,
|
||||
marginTop: user ? 0 : 50
|
||||
}}
|
||||
onPress={onUnlockAppRequested}
|
||||
color={colors.primary.border}
|
||||
/>
|
||||
<Heading
|
||||
color={colors.primary.heading}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Unlock your notes
|
||||
</Heading>
|
||||
Unlock your notes
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
maxWidth: "90%"
|
||||
}}
|
||||
>
|
||||
{"Please verify it's you"}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
{user || appLockHasPasswordSecurity ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
secureTextEntry
|
||||
keyboardType={
|
||||
appLockHasPasswordSecurity ? "number-pad" : "default"
|
||||
}
|
||||
placeholder={`Enter ${
|
||||
appLockHasPasswordSecurity
|
||||
? `app lock pin`
|
||||
: "account password"
|
||||
}`}
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
maxWidth: "90%"
|
||||
}}
|
||||
>
|
||||
{"Please verify it's you"}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.primary.background
|
||||
marginTop: user ? 25 : 25
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
{user || appLockHasPasswordSecurity ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
secureTextEntry
|
||||
placeholder="Enter account password"
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
<Button
|
||||
title="Continue"
|
||||
type="accent"
|
||||
onPress={onSubmit}
|
||||
width={250}
|
||||
height={45}
|
||||
style={{
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: user ? 25 : 25
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
<>
|
||||
<Button
|
||||
title="Continue"
|
||||
type="accent"
|
||||
onPress={onSubmit}
|
||||
width={250}
|
||||
height={45}
|
||||
style={{
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{biometricsAuthEnabled ? (
|
||||
<Button
|
||||
title="Unlock with Biometrics"
|
||||
width={250}
|
||||
@@ -207,12 +231,12 @@ const AppLockedOverlay = () => {
|
||||
icon={"fingerprint"}
|
||||
type="transparent"
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AppLockedOverlay;
|
||||
|
||||
@@ -36,6 +36,7 @@ import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -60,6 +61,7 @@ const DialogProvider = () => {
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<AppLockPassword />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -65,16 +65,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
};
|
||||
}, [show]);
|
||||
|
||||
const onPressPositive = async () => {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
@@ -102,6 +92,16 @@ export const Dialog = ({ context = "global" }) => {
|
||||
[context]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
};
|
||||
}, [show]);
|
||||
|
||||
const hide = () => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
|
||||
286
apps/mobile/app/components/dialogs/applock-password/index.tsx
Normal file
286
apps/mobile/app/components/dialogs/applock-password/index.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import {
|
||||
clearAppLockVerificationCipher,
|
||||
setAppLockVerificationCipher,
|
||||
validateAppLockPassword
|
||||
} from "../../../common/database/encryption";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eCloseAppLocKPasswordDailog,
|
||||
eOpenAppLockPasswordDialog
|
||||
} from "../../../utils/events";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Toast } from "../../toast";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
|
||||
export const AppLockPassword = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
|
||||
const [visible, setVisible] = useState(false);
|
||||
const currentPasswordInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const confirmPasswordInputRef = useRef<TextInput>(null);
|
||||
const values = useRef<{
|
||||
currentPassword?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
eSubscribeEvent(
|
||||
eOpenAppLockPasswordDialog,
|
||||
(mode: "create" | "change" | "remove") => {
|
||||
setMode(mode);
|
||||
setVisible(true);
|
||||
}
|
||||
),
|
||||
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
|
||||
values.current = {};
|
||||
setVisible(false);
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
subs.forEach((sub) => sub.unsubscribe());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const close = () => {
|
||||
values.current = {};
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
await sleep(100);
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={close}
|
||||
visible={visible}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(10),
|
||||
width: DDS.isTab ? 350 : "85%",
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={
|
||||
mode === "change"
|
||||
? "Change app lock pin"
|
||||
: mode === "remove"
|
||||
? "Remove app lock pin"
|
||||
: "Set up app lock pin"
|
||||
}
|
||||
paragraph={
|
||||
mode === "change"
|
||||
? "Change app lock pin"
|
||||
: mode === "remove"
|
||||
? "Remove app lock pin"
|
||||
: "Set up a custom app lock pin to unlock the app"
|
||||
}
|
||||
icon="shield"
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{mode === "change" ? (
|
||||
<Input
|
||||
fwdRef={currentPasswordInputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.currentPassword = value;
|
||||
}}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Next"
|
||||
keyboardType="number-pad"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
placeholder={"Current pin"}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.password = value;
|
||||
}}
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry
|
||||
placeholder={mode === "change" ? "New pin" : "Pin"}
|
||||
/>
|
||||
|
||||
{mode !== "remove" ? (
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.confirmPassword = value;
|
||||
}}
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
customValidator={() => values.current.password || ""}
|
||||
validationType="confirmPassword"
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={"Confirm pin"}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={close}
|
||||
onPressPositive={async () => {
|
||||
if (mode === "create") {
|
||||
if (!values.current.password || !values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error("All inputs are required"),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error("Pin does not match"),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
} else if (mode === "change") {
|
||||
if (
|
||||
!values.current.currentPassword ||
|
||||
!values.current.password ||
|
||||
!values.current.confirmPassword
|
||||
) {
|
||||
ToastManager.error(
|
||||
new Error("All inputs are required"),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error("Pin does not match"),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.currentPassword
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error("Pin incorrect"),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
new Error("All inputs are required"),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.password
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(new Error("Pin incorrect"), "local");
|
||||
return;
|
||||
}
|
||||
clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", false);
|
||||
}
|
||||
|
||||
close();
|
||||
}}
|
||||
positiveTitle="Save"
|
||||
negativeTitle="Cancel"
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
doneText=""
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
|
||||
eSendEvent(eOpenAppLockPasswordDialog, mode);
|
||||
};
|
||||
@@ -49,7 +49,7 @@ import { User } from "@notesnook/core/dist/api/user-manager";
|
||||
import { EventManagerSubscription } from "@notesnook/core/dist/utils/event-manager";
|
||||
//@ts-ignore
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { DatabaseLogger, db, setupDatabase } from "../common/database";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import Migrate from "../components/sheets/migrate";
|
||||
import NewFeature from "../components/sheets/new-feature";
|
||||
@@ -645,34 +645,44 @@ export const useAppEvents = () => {
|
||||
}
|
||||
}, [loading, onUserUpdated]);
|
||||
|
||||
const initializeDatabase = useCallback(async () => {
|
||||
try {
|
||||
const initializeDatabase = useCallback(
|
||||
async (password?: string) => {
|
||||
if (useUserStore.getState().appLocked) return;
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
await db.init();
|
||||
try {
|
||||
await setupDatabase(password);
|
||||
await db.init();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(
|
||||
e as Error,
|
||||
"Error initializing database",
|
||||
"global"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (db.isInitialized) {
|
||||
useSettingStore.getState().setAppLoading(false);
|
||||
}
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
setImmediate(() => {
|
||||
useSettingStore.getState().setAppLoading(false);
|
||||
});
|
||||
Walkthrough.init();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(e as Error, "Error initializing database", "global");
|
||||
}
|
||||
}, [IsDatabaseMigrationRequired]);
|
||||
},
|
||||
[IsDatabaseMigrationRequired]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let sub: () => void;
|
||||
if (appLocked) {
|
||||
const sub = useUserStore.subscribe((state) => {
|
||||
if (
|
||||
!state.appLocked &&
|
||||
db.isInitialized &&
|
||||
useSettingStore.getState().isAppLoading
|
||||
) {
|
||||
initializeDatabase();
|
||||
if (!state.appLocked && useSettingStore.getState().isAppLoading) {
|
||||
initializeDatabase(useSettingStore.getState().dbPassword);
|
||||
useSettingStore.setState({
|
||||
dbPassword: undefined
|
||||
});
|
||||
|
||||
sub();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
deactivateKeepAwake
|
||||
} from "@sayem314/react-native-keep-awake";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Platform, StatusBar, View } from "react-native";
|
||||
import { Dimensions, Platform, StatusBar, View } from "react-native";
|
||||
import changeNavigationBarColor from "react-native-navigation-bar-color";
|
||||
import {
|
||||
addOrientationListener,
|
||||
@@ -211,6 +211,11 @@ const _TabsHolder = () => {
|
||||
checkDeviceType(size);
|
||||
};
|
||||
|
||||
if (!deviceMode) {
|
||||
const size = Dimensions.get("window");
|
||||
checkDeviceType(size);
|
||||
}
|
||||
|
||||
function checkDeviceType(size) {
|
||||
setDimensions({
|
||||
width: size.width,
|
||||
|
||||
@@ -210,7 +210,8 @@ const AppLock = ({ route }) => {
|
||||
if (
|
||||
!(await BiometicService.isBiometryAvailable()) &&
|
||||
!useUserStore.getState().user &&
|
||||
item.value !== modes[0].value
|
||||
item.value !== modes[0].value &&
|
||||
!SettingsService.getProperty("appLockHasPasswordSecurity")
|
||||
) {
|
||||
ToastManager.show({
|
||||
heading: "Biometrics not enrolled",
|
||||
@@ -220,6 +221,26 @@ const AppLock = ({ route }) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!SettingsService.getProperty(
|
||||
"appLockHasPasswordSecurity"
|
||||
) &&
|
||||
item.value !== modes[0].value
|
||||
) {
|
||||
const verified = await BiometicService.validateUser(
|
||||
"Verify it's you"
|
||||
);
|
||||
if (verified) {
|
||||
SettingsService.setProperty(
|
||||
"biometricsAuthEnabled",
|
||||
true
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SettingsService.set({ appLockMode: item.value });
|
||||
}}
|
||||
customStyle={{
|
||||
|
||||
@@ -68,6 +68,7 @@ import { useDragState } from "./editor/state";
|
||||
import { verifyUser } from "./functions";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import { AppLockPassword } from "../../components/dialogs/applock-password";
|
||||
type User = any;
|
||||
|
||||
export const settingsGroups: SettingSection[] = [
|
||||
@@ -767,11 +768,84 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "app-lock",
|
||||
name: "App lock",
|
||||
description: "Change app lock mode to suit your needs",
|
||||
icon: "fingerprint",
|
||||
modifer: () => {
|
||||
AppLock.present();
|
||||
}
|
||||
type: "screen",
|
||||
description: "Enhanced at rest encryption with app lock",
|
||||
icon: "lock",
|
||||
sections: [
|
||||
{
|
||||
id: "app-lock-mode",
|
||||
name: "App lock mode",
|
||||
description:
|
||||
"Select the mode for the desired level of app lock security.",
|
||||
icon: "fingerprint",
|
||||
modifer: () => {
|
||||
AppLock.present();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-pin",
|
||||
name: "Setup app lock pin",
|
||||
description: "Set up a new pin for app lock",
|
||||
hidden: () => {
|
||||
return !!SettingsService.getProperty(
|
||||
"appLockHasPasswordSecurity"
|
||||
);
|
||||
},
|
||||
property: "appLockHasPasswordSecurity",
|
||||
modifer: () => {
|
||||
AppLockPassword.present("create");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-pin-change",
|
||||
name: "Change app lock pin",
|
||||
description: "Set up a new pin for the app lock",
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty("appLockHasPasswordSecurity");
|
||||
},
|
||||
property: "appLockHasPasswordSecurity",
|
||||
modifer: () => {
|
||||
AppLockPassword.present("change");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-pin-remove",
|
||||
name: "Remove app lock pin",
|
||||
description:
|
||||
"Remove app lock pin, app lock will fallback to using account password to unlock the app",
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty("appLockHasPasswordSecurity");
|
||||
},
|
||||
property: "appLockHasPasswordSecurity",
|
||||
modifer: () => {
|
||||
AppLockPassword.present("remove");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-fingerprint",
|
||||
name: "Unlock with biometrics",
|
||||
description: "Allow biometric authentication to unlock the app",
|
||||
type: "switch",
|
||||
property: "biometricsAuthEnabled",
|
||||
onChange: async () => {
|
||||
if (await BiometicService.isBiometryAvailable()) {
|
||||
const verified = await BiometicService.validateUser(
|
||||
"Verify it's you"
|
||||
);
|
||||
if (!verified) {
|
||||
SettingsService.setProperty("biometricsAuthEnabled", false);
|
||||
}
|
||||
} else {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
"Biometric authentication is unavailable on this device."
|
||||
)
|
||||
);
|
||||
SettingsService.setProperty("biometricsAuthEnabled", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -74,13 +74,13 @@ async function getCredentials(title?: string, description?: string) {
|
||||
|
||||
const options = Platform.select({
|
||||
ios: {
|
||||
fallbackEnabled: true,
|
||||
fallbackEnabled: false,
|
||||
description: description
|
||||
},
|
||||
android: {
|
||||
title: title,
|
||||
description: description,
|
||||
deviceCredentialAllowed: true
|
||||
deviceCredentialAllowed: false
|
||||
}
|
||||
});
|
||||
await FingerprintScanner.authenticate(options as AuthenticateIOS);
|
||||
@@ -125,13 +125,13 @@ async function validateUser(title: string, description?: string) {
|
||||
await FingerprintScanner.authenticate(
|
||||
Platform.select({
|
||||
ios: {
|
||||
fallbackEnabled: true,
|
||||
fallbackEnabled: false,
|
||||
description: title
|
||||
},
|
||||
android: {
|
||||
title: title,
|
||||
description: description,
|
||||
deviceCredentialAllowed: true
|
||||
deviceCredentialAllowed: false
|
||||
}
|
||||
}) as AuthenticateIOS
|
||||
);
|
||||
|
||||
@@ -75,6 +75,8 @@ export type Settings = {
|
||||
lighTheme: ThemeDefinition;
|
||||
darkTheme: ThemeDefinition;
|
||||
markdownShortcuts?: boolean;
|
||||
appLockHasPasswordSecurity?: boolean;
|
||||
biometricsAuthEnabled?: boolean;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -110,6 +112,8 @@ export interface SettingStore extends State {
|
||||
setInsets: (insets: Insets) => void;
|
||||
timeFormat: string;
|
||||
dateFormat: string;
|
||||
dbPassword?: string;
|
||||
isOldAppLock: () => boolean;
|
||||
}
|
||||
|
||||
const { width, height } = Dimensions.get("window");
|
||||
@@ -156,10 +160,13 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
colorScheme: "light",
|
||||
lighTheme: ThemeLight,
|
||||
darkTheme: ThemeDark,
|
||||
markdownShortcuts: true
|
||||
markdownShortcuts: true,
|
||||
biometricsAuthEnabled: false,
|
||||
appLockHasPasswordSecurity: false
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
dbPassword: undefined,
|
||||
settings: { ...defaultSettings },
|
||||
sheetKeyboardHandler: true,
|
||||
fullscreen: false,
|
||||
@@ -184,6 +191,13 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
appDidEnterBackgroundForAction: false,
|
||||
isOldAppLock: () => {
|
||||
return (
|
||||
get().settings.appLockHasPasswordSecurity === undefined &&
|
||||
get().settings.biometricsAuthEnabled === undefined &&
|
||||
get().settings.appLockMode !== "none"
|
||||
);
|
||||
},
|
||||
insets: initialWindowMetrics?.insets
|
||||
? initialWindowMetrics.insets
|
||||
: { top: 0, right: 0, left: 0, bottom: 0 }
|
||||
|
||||
@@ -163,3 +163,6 @@ export const eLoginSessionExpired = "609";
|
||||
export const eDBItemUpdate = "610";
|
||||
export const eGroupOptionsUpdated = "611";
|
||||
export const eOnRefreshSearch = "612";
|
||||
|
||||
export const eOpenAppLockPasswordDialog = "613";
|
||||
export const eCloseAppLocKPasswordDailog = "614";
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
DatabaseAccessor,
|
||||
DatabaseSchema,
|
||||
SQLiteOptions,
|
||||
changeDatabasePassword,
|
||||
createDatabase
|
||||
} from "../database";
|
||||
import { Kysely, Transaction, sql } from "kysely";
|
||||
@@ -223,7 +224,8 @@ class Database {
|
||||
"DELETE FROM sqlite_master",
|
||||
"PRAGMA writable_schema = 0",
|
||||
"VACUUM",
|
||||
"PRAGMA integrity_check"
|
||||
"PRAGMA integrity_check",
|
||||
"PRAGMA rekey = ''"
|
||||
]) {
|
||||
await sql.raw(statement).execute(this.sql());
|
||||
}
|
||||
@@ -234,6 +236,11 @@ class Database {
|
||||
)) as unknown as Kysely<DatabaseSchema>;
|
||||
}
|
||||
|
||||
async changePassword(password?: string) {
|
||||
if (!this._sql) return;
|
||||
await changeDatabasePassword(this._sql, password);
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.options)
|
||||
throw new Error(
|
||||
|
||||
@@ -216,12 +216,17 @@ export type SQLiteOptions = {
|
||||
tempStore?: "memory" | "file" | "default";
|
||||
cacheSize?: number;
|
||||
pageSize?: number;
|
||||
password?: string;
|
||||
};
|
||||
export async function createDatabase(name: string, options: SQLiteOptions) {
|
||||
const db = new Kysely<RawDatabaseSchema>({
|
||||
dialect: options.dialect(name),
|
||||
plugins: [new SqliteBooleanPlugin()]
|
||||
});
|
||||
if (options.password)
|
||||
await sql`PRAGMA key = ${sql.ref(options.password)}`
|
||||
.execute(db)
|
||||
.then((r) => console.log(r));
|
||||
|
||||
const migrator = new Migrator({
|
||||
db,
|
||||
@@ -275,6 +280,13 @@ export async function createDatabase(name: string, options: SQLiteOptions) {
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function changeDatabasePassword(
|
||||
db: Kysely<DatabaseSchema>,
|
||||
password?: string
|
||||
) {
|
||||
await sql`PRAGMA rekey = "${password ? password : ""}"`.execute(db);
|
||||
}
|
||||
|
||||
export function isFalse<TB extends keyof DatabaseSchema>(
|
||||
column: ReferenceExpression<DatabaseSchema, TB>
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user