2022-08-31 06:33:37 +05:00
|
|
|
/*
|
|
|
|
|
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
|
2022-08-31 06:33:37 +05:00
|
|
|
|
|
|
|
|
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
|
|
|
|
2023-12-28 14:35:15 +05:00
|
|
|
import Sodium from "@ammarahmed/react-native-sodium";
|
2022-08-26 16:19:39 +05:00
|
|
|
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";
|
2022-08-26 16:19:39 +05:00
|
|
|
import { generateSecureRandom } from "react-native-securerandom";
|
2024-02-06 12:08:41 +05:00
|
|
|
import { DatabaseLogger } from ".";
|
2023-06-05 16:26:39 +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();
|
2023-06-05 16:26:39 +05:00
|
|
|
|
|
|
|
|
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-06 12:08:41 +05:00
|
|
|
const NOTESNOOK_APPLOCK_KEY_SALT = "notesnook_applock_key_salt";
|
|
|
|
|
const NOTESNOOK_DB_KEY_SALT = "notesnook_database_key";
|
|
|
|
|
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: {
|
2023-06-05 16:26:39 +05:00
|
|
|
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;
|
|
|
|
|
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,
|
2024-02-06 12:08:41 +05:00
|
|
|
NOTESNOOK_APPLOCK_KEY_SALT
|
2023-12-28 14:35:15 +05:00
|
|
|
);
|
|
|
|
|
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
2024-02-06 12:08:41 +05:00
|
|
|
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
2023-12-28 14:35:15 +05:00
|
|
|
const databaseKey = await decrypt(
|
|
|
|
|
{
|
|
|
|
|
password: appLockPassword
|
|
|
|
|
},
|
|
|
|
|
databaseKeyCipher
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function setAppLockVerificationCipher(appLockPassword) {
|
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
|
2022-08-26 16:19:39 +05:00
|
|
|
);
|
2024-02-06 12:08:41 +05:00
|
|
|
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
2023-12-28 14:35:15 +05:00
|
|
|
|
2024-02-06 12:08:41 +05:00
|
|
|
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
|
|
|
|
|
|
|
|
|
DatabaseLogger.info("setAppLockVerificationCipher");
|
2022-08-27 15:23:11 +05:00
|
|
|
} catch (e) {
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.error(e);
|
2023-12-28 14:35:15 +05:00
|
|
|
console.log(e);
|
2022-08-27 15:23:11 +05:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function validateAppLockPassword(appLockPassword) {
|
|
|
|
|
try {
|
2024-02-06 12:08:41 +05:00
|
|
|
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
2023-12-28 14:35:15 +05:00
|
|
|
if (!appLockCipher) return true;
|
|
|
|
|
const decrypted = await decrypt(
|
|
|
|
|
{
|
|
|
|
|
password: appLockPassword
|
|
|
|
|
},
|
|
|
|
|
appLockCipher
|
2023-06-05 16:26:39 +05:00
|
|
|
);
|
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;
|
2023-06-05 16:26:39 +05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-28 14:35:15 +05:00
|
|
|
let DB_KEY;
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getDatabaseKey(appLockPassword) {
|
|
|
|
|
if (DB_KEY) return DB_KEY;
|
2022-02-28 13:48:59 +05:00
|
|
|
try {
|
2023-12-28 14:35:15 +05:00
|
|
|
if (appLockPassword) {
|
|
|
|
|
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
|
|
|
|
const databaseKey = await decrypt(
|
|
|
|
|
{
|
|
|
|
|
password: appLockPassword
|
|
|
|
|
},
|
|
|
|
|
databaseKeyCipher
|
|
|
|
|
);
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.info("Getting database key from cipher");
|
2023-12-28 14:35:15 +05:00
|
|
|
DB_KEY = databaseKey;
|
|
|
|
|
return databaseKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
|
|
|
|
if (hasKey) {
|
2022-08-26 16:19:39 +05:00
|
|
|
let credentials = await Keychain.getInternetCredentials(
|
2023-12-28 14:35:15 +05:00
|
|
|
KEYCHAIN_SERVER_DBKEY,
|
2022-08-26 16:19:39 +05:00
|
|
|
KEYSTORE_CONFIG
|
|
|
|
|
);
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.info("Getting database key from Keychain");
|
2023-12-28 14:35:15 +05:00
|
|
|
DB_KEY = credentials.password;
|
2022-02-28 13:48:59 +05:00
|
|
|
return credentials.password;
|
|
|
|
|
}
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.info("Generating new database key");
|
2023-12-28 14:35:15 +05:00
|
|
|
const password = generatePassword();
|
|
|
|
|
const derivedDatabaseKey = await Sodium.deriveKey(
|
|
|
|
|
password,
|
2024-02-06 12:08:41 +05:00
|
|
|
NOTESNOOK_DB_KEY_SALT
|
2023-12-28 14:35:15 +05:00
|
|
|
);
|
|
|
|
|
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
|
2024-02-06 12:08:41 +05:00
|
|
|
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
2023-12-28 14:35:15 +05:00
|
|
|
await Keychain.resetInternetCredentials("notesnook");
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
2023-12-28 14:35:15 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB_KEY = derivedDatabaseKey.key;
|
|
|
|
|
|
|
|
|
|
return derivedDatabaseKey.key;
|
|
|
|
|
} catch (e) {
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.error(e);
|
2023-12-28 14:35:15 +05:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 23:03:38 +05:00
|
|
|
export async function deriveCryptoKey(data) {
|
2023-12-28 14:35:15 +05:00
|
|
|
try {
|
|
|
|
|
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
|
|
|
|
const userKeyCipher = await encrypt(
|
|
|
|
|
{
|
|
|
|
|
key: await getDatabaseKey()
|
|
|
|
|
},
|
|
|
|
|
credentials.key
|
|
|
|
|
);
|
2024-02-07 23:03:38 +05:00
|
|
|
DatabaseLogger.info("User key stored: ", !!userKeyCipher, credentials);
|
|
|
|
|
|
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
|
|
|
return credentials.key;
|
|
|
|
|
} catch (e) {
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.error(e);
|
2023-12-28 14:35:15 +05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getCryptoKey(_name) {
|
|
|
|
|
try {
|
2024-02-06 12:08:41 +05:00
|
|
|
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
2023-12-28 14:35:15 +05:00
|
|
|
|
2024-02-07 23:03:38 +05:00
|
|
|
if (!keyCipher) {
|
|
|
|
|
DatabaseLogger.info("User key cipher is null");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const key = await decrypt(
|
2023-12-28 14:35:15 +05:00
|
|
|
{
|
|
|
|
|
key: await getDatabaseKey()
|
|
|
|
|
},
|
|
|
|
|
keyCipher
|
|
|
|
|
);
|
|
|
|
|
|
2024-02-07 23:03:38 +05:00
|
|
|
DatabaseLogger.info("User key decrypted: ", !!key);
|
|
|
|
|
|
2023-12-28 14:35:15 +05:00
|
|
|
return key;
|
2022-08-27 15:23:11 +05:00
|
|
|
} catch (e) {
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.error(e);
|
2022-08-27 15:23:11 +05:00
|
|
|
}
|
2022-02-28 13:48:59 +05:00
|
|
|
}
|
|
|
|
|
|
2022-08-27 15:23:11 +05:00
|
|
|
export async function removeCryptoKey(_name) {
|
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;
|
2022-08-27 15:23:11 +05:00
|
|
|
} catch (e) {
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.error(e);
|
2022-08-27 15:23:11 +05:00
|
|
|
}
|
2022-02-28 13:48:59 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getRandomBytes(length) {
|
|
|
|
|
return await generateSecureRandom(length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function hash(password, email) {
|
|
|
|
|
let result = await Sodium.hashPassword(password, email);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function generateCryptoKey(password, salt) {
|
|
|
|
|
try {
|
|
|
|
|
let credentials = await Sodium.deriveKey(password, salt || null);
|
|
|
|
|
return credentials;
|
|
|
|
|
} catch (e) {
|
2024-02-06 12:08:41 +05:00
|
|
|
DatabaseLogger.error(e);
|
2022-02-28 13:48:59 +05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getAlgorithm(base64Variant) {
|
|
|
|
|
return `xcha-argon2i13-${base64Variant}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function decrypt(password, data) {
|
|
|
|
|
if (!password.password && !password.key) return undefined;
|
2022-08-26 16:19:39 +05:00
|
|
|
if (password.password && password.password === "" && !password.key)
|
|
|
|
|
return undefined;
|
2022-02-28 13:48:59 +05:00
|
|
|
let _data = { ...data };
|
2022-08-26 16:19:39 +05:00
|
|
|
_data.output = "plain";
|
2022-02-28 13:48:59 +05:00
|
|
|
return await Sodium.decrypt(password, _data);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-26 12:02:13 +05:00
|
|
|
export async function decryptMulti(password, data) {
|
|
|
|
|
if (!password.password && !password.key) return undefined;
|
|
|
|
|
if (password.password && password.password === "" && !password.key)
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
|
|
data = data.map((d) => {
|
|
|
|
|
d.output = "plain";
|
|
|
|
|
return d;
|
|
|
|
|
});
|
|
|
|
|
return await Sodium.decryptMulti(password, data);
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-28 13:48:59 +05:00
|
|
|
export function parseAlgorithm(alg) {
|
|
|
|
|
if (!alg) return {};
|
2022-08-26 16:19:39 +05:00
|
|
|
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
2022-02-28 13:48:59 +05:00
|
|
|
return {
|
|
|
|
|
encryptionAlgorithm: enc,
|
|
|
|
|
kdfAlgorithm: kdf,
|
|
|
|
|
compressionAlgorithm: compressionAlg,
|
2022-08-26 16:19:39 +05:00
|
|
|
isCompress: compressed === "1",
|
2022-02-28 13:48:59 +05:00
|
|
|
base64_variant: base64variant
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function encrypt(password, data) {
|
|
|
|
|
if (!password.password && !password.key) return undefined;
|
2022-08-26 16:19:39 +05:00
|
|
|
if (password.password && password.password === "" && !password.key)
|
|
|
|
|
return undefined;
|
2022-02-28 13:48:59 +05:00
|
|
|
|
|
|
|
|
let message = {
|
2022-08-26 16:19:39 +05:00
|
|
|
type: "plain",
|
2022-02-28 13:48:59 +05:00
|
|
|
data: data
|
|
|
|
|
};
|
|
|
|
|
let result = await Sodium.encrypt(password, message);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...result,
|
|
|
|
|
alg: getAlgorithm(7)
|
|
|
|
|
};
|
|
|
|
|
}
|
2023-09-01 18:00:05 +05:00
|
|
|
|
|
|
|
|
export async function encryptMulti(password, data) {
|
|
|
|
|
if (!password.password && !password.key) return undefined;
|
|
|
|
|
if (password.password && password.password === "" && !password.key)
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
|
|
let results = await Sodium.encryptMulti(
|
|
|
|
|
password,
|
|
|
|
|
data.map((item) => ({
|
|
|
|
|
type: "plain",
|
|
|
|
|
data: item
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return !results
|
|
|
|
|
? []
|
|
|
|
|
: results.map((result) => ({
|
|
|
|
|
...result,
|
|
|
|
|
alg: getAlgorithm(7)
|
|
|
|
|
}));
|
|
|
|
|
}
|