Files
notesnook/apps/mobile/app/common/database/encryption.ts

480 lines
13 KiB
TypeScript
Raw Normal View History

/*
This file is part of the Notesnook project (https://notesnook.com/)
2023-01-16 13:44:52 +05:00
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/>.
*/
2022-08-30 16:13:11 +05:00
2024-11-25 13:11:03 +05:00
import Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
2024-12-13 14:07:15 +05:00
import { SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
2024-02-06 12:08:41 +05:00
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
2024-02-06 12:08:41 +05:00
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
2024-11-25 13:11:03 +05:00
import { MMKV } from "./mmkv";
2023-12-28 14:35:15 +05:00
// 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";
2023-12-28 14:35:15 +05:00
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
2024-02-14 10:27:58 +05:00
2024-05-15 16:24:31 +05:00
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
2024-02-14 10:27:58 +05:00
2024-02-06 12:08:41 +05:00
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
2022-02-28 13:48:59 +05:00
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
2022-02-28 13:48:59 +05:00
},
android: {}
});
2023-12-28 14:35:15 +05:00
function generatePassword() {
const length = 80;
2024-11-25 13:11:03 +05:00
//@ts-ignore
2023-12-28 14:35:15 +05:00
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;
}
2024-11-25 13:11:03 +05:00
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
2023-12-28 14:35:15 +05:00
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
2024-02-06 12:08:41 +05:00
NOTESNOOK_APPLOCK_KEY_SALT
2023-12-28 14:35:15 +05:00
);
2024-11-25 13:11:03 +05:00
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
2024-02-06 12:08:41 +05:00
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
2023-12-28 14:35:15 +05:00
// We reset the database key from keychain once app lock password is set.
2024-02-06 12:08:41 +05:00
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
2023-12-28 14:35:15 +05:00
return true;
}
2024-11-25 13:11:03 +05:00
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
2025-10-23 21:39:20 +05:00
const databaseKeyCipher: Cipher = CipherStorage.getMap(
DB_KEY_CIPHER
) as Cipher;
2024-11-25 13:11:03 +05:00
const databaseKey = (await decrypt(
2023-12-28 14:35:15 +05:00
{
password: appLockPassword
},
databaseKeyCipher
2024-11-25 13:11:03 +05:00
)) as string;
2023-12-28 14:35:15 +05:00
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
2024-02-06 12:08:41 +05:00
MMKV.removeItem(DB_KEY_CIPHER);
2023-12-28 14:35:15 +05:00
return true;
}
2024-11-25 13:11:03 +05:00
export async function setAppLockVerificationCipher(appLockPassword: string) {
2022-02-28 13:48:59 +05:00
try {
2023-12-28 14:35:15 +05:00
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
2024-02-06 12:08:41 +05:00
NOTESNOOK_APPLOCK_KEY_SALT
);
2024-11-25 13:11:03 +05:00
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
2024-02-06 12:08:41 +05:00
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
2024-02-06 12:08:41 +05:00
DatabaseLogger.error(e);
}
2022-02-28 13:48:59 +05:00
}
2023-12-28 14:35:15 +05:00
export async function clearAppLockVerificationCipher() {
2024-02-06 12:08:41 +05:00
CipherStorage.removeItem(APPLOCK_CIPHER);
2023-12-28 14:35:15 +05:00
}
2024-11-25 13:11:03 +05:00
export async function validateAppLockPassword(appLockPassword: string) {
2023-12-28 14:35:15 +05:00
try {
2025-10-23 21:39:20 +05:00
const appLockCipher: Cipher = CipherStorage.getMap(
APPLOCK_CIPHER
) as Cipher;
2023-12-28 14:35:15 +05:00
if (!appLockCipher) return true;
2024-05-15 16:24:31 +05:00
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
2024-02-06 12:08:41 +05:00
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
2023-12-28 14:35:15 +05:00
} catch (e) {
2024-02-06 12:08:41 +05:00
DatabaseLogger.error(e);
2023-12-28 14:35:15 +05:00
return false;
}
}
2024-11-25 13:11:03 +05:00
let DB_KEY: string | undefined;
2023-12-28 14:35:15 +05:00
export function clearDatabaseKey() {
DB_KEY = undefined;
2024-02-06 12:08:41 +05:00
DatabaseLogger.info("Cleared database key");
2023-12-28 14:35:15 +05:00
}
2024-11-25 13:11:03 +05:00
export async function getDatabaseKey(appLockPassword?: string) {
2023-12-28 14:35:15 +05:00
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
2025-10-23 21:39:20 +05:00
const databaseKeyCipher: Cipher = CipherStorage.getMap(
"databaseKeyCipher"
) as Cipher;
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
2023-12-28 14:35:15 +05:00
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
2024-02-14 10:27:58 +05:00
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
2024-02-14 10:27:58 +05:00
}
}
2024-02-14 10:27:58 +05:00
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
2024-02-14 10:27:58 +05:00
DB_KEY = derivedDatabaseKey.key as string;
2024-02-14 10:27:58 +05:00
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
2023-12-28 14:35:15 +05:00
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
2024-02-14 10:27:58 +05:00
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
2023-12-28 14:35:15 +05:00
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
2023-12-28 14:35:15 +05:00
if (!DB_KEY) {
throw new Error(
`Failed to get database key, ${await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
)}`
);
2023-12-28 14:35:15 +05:00
}
return DB_KEY;
2023-12-28 14:35:15 +05:00
}
2024-12-13 14:07:15 +05:00
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
try {
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
);
if (!credentials) return;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function deriveCryptoKey(data: SerializedKey) {
2023-12-28 14:35:15 +05:00
try {
2024-11-25 13:11:03 +05:00
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const credentials = (await Sodium.deriveKey(
data.password,
data.salt
)) as Password;
const userKeyCipher = (await encrypt(
2023-12-28 14:35:15 +05:00
{
2024-11-25 13:11:03 +05:00
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
2023-12-28 14:35:15 +05:00
},
2024-11-25 13:11:03 +05:00
credentials.key as string
2024-12-13 14:07:15 +05:00
)) as Cipher<"base64">;
2024-11-25 13:11:03 +05:00
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
2024-02-07 23:03:38 +05:00
2023-12-28 14:35:15 +05:00
// Store encrypted user key in MMKV
2024-02-06 12:08:41 +05:00
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
2023-12-28 14:35:15 +05:00
} catch (e) {
2024-02-06 12:08:41 +05:00
DatabaseLogger.error(e);
2023-12-28 14:35:15 +05:00
}
}
2024-11-25 13:11:03 +05:00
export async function getCryptoKey() {
2023-12-28 14:35:15 +05:00
try {
2025-10-23 21:39:20 +05:00
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
2024-02-07 23:03:38 +05:00
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
2024-12-13 14:07:15 +05:00
return undefined;
2024-02-07 23:03:38 +05:00
}
const key = await decrypt(
2023-12-28 14:35:15 +05:00
{
2024-11-25 13:11:03 +05:00
key: (await getDatabaseKey()) as string,
salt: keyCipher.salt
2023-12-28 14:35:15 +05:00
},
keyCipher
);
return key;
} catch (e) {
2024-02-06 12:08:41 +05:00
DatabaseLogger.error(e);
}
2022-02-28 13:48:59 +05:00
}
2024-11-25 13:11:03 +05:00
export async function removeCryptoKey() {
2022-02-28 13:48:59 +05:00
try {
2024-02-06 12:08:41 +05:00
MMKV.removeItem(USER_KEY_CIPHER);
2023-12-28 14:35:15 +05:00
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
2024-02-06 12:08:41 +05:00
DatabaseLogger.error(e);
}
2022-02-28 13:48:59 +05:00
}
2024-11-25 13:11:03 +05:00
export async function getRandomBytes(length: number) {
2022-02-28 13:48:59 +05:00
return await generateSecureRandom(length);
}
2024-12-13 14:07:15 +05:00
export async function hash(
password: string,
email: string,
options?: { usesFallback?: boolean }
) {
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
if (options?.usesFallback && Platform.OS !== "ios") {
2024-12-23 15:55:33 +05:00
return "";
2024-12-13 14:07:15 +05:00
}
return (
options?.usesFallback
? await Sodium.hashPasswordFallback?.(password, email)
: await Sodium.hashPassword(password, email)
) as string;
2022-02-28 13:48:59 +05:00
}
2024-11-25 13:11:03 +05:00
export async function generateCryptoKey(password: string, salt?: string) {
2024-12-23 15:55:33 +05:00
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
}
export async function generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return Sodium.deriveKeyFallback?.(
password,
salt as string
) as Promise<SerializedKey>;
2022-02-28 13:48:59 +05:00
}
2024-11-25 13:11:03 +05:00
export function getAlgorithm(base64Variant: number) {
2022-02-28 13:48:59 +05:00
return `xcha-argon2i13-${base64Variant}`;
}
2024-12-13 14:07:15 +05:00
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
2024-11-25 13:11:03 +05:00
const _data = { ...data };
_data.output = "plain";
2024-02-14 10:27:58 +05:00
if (!password.salt) password.salt = data.salt;
2024-12-13 14:07:15 +05:00
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decrypt(key, _data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decrypt(fallbackKey, _data);
} else {
throw e;
}
}
}
2022-02-28 13:48:59 +05:00
return await Sodium.decrypt(password, _data);
}
2024-12-13 14:07:15 +05:00
export async function decryptMulti(
password: Password,
data: Cipher<"base64">[]
) {
2023-08-26 12:02:13 +05:00
data = data.map((d) => {
d.output = "plain";
return d;
});
2024-02-14 10:27:58 +05:00
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
2024-12-13 14:07:15 +05:00
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decryptMulti(key, data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt as string
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decryptMulti(fallbackKey, data);
} else {
throw e;
}
}
}
2023-08-26 12:02:13 +05:00
return await Sodium.decryptMulti(password, data);
}
2024-11-25 13:11:03 +05:00
export function parseAlgorithm(alg: string) {
2022-02-28 13:48:59 +05:00
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
2022-02-28 13:48:59 +05:00
return {
encryptionAlgorithm: enc,
kdfAlgorithm: kdf,
compressionAlgorithm: compressionAlg,
isCompress: compressed === "1",
2022-02-28 13:48:59 +05:00
base64_variant: base64variant
};
}
2024-12-13 14:07:15 +05:00
export async function encrypt(password: SerializedKey, plainText: string) {
const result = await Sodium.encrypt<"base64">(password, {
type: "plain",
2024-12-13 14:07:15 +05:00
data: plainText
2024-11-25 13:11:03 +05:00
});
2022-02-28 13:48:59 +05:00
return {
...result,
alg: getAlgorithm(7)
};
}
2023-09-01 18:00:05 +05:00
2024-12-13 14:07:15 +05:00
export async function encryptMulti(
password: SerializedKey,
plainText: string[]
) {
const results = await Sodium.encryptMulti<"base64">(
2023-09-01 18:00:05 +05:00
password,
2024-12-13 14:07:15 +05:00
plainText.map((item) => ({
2023-09-01 18:00:05 +05:00
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}