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

368 lines
10 KiB
JavaScript
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
2023-12-28 14:35:15 +05:00
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";
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 { 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-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: {
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
);
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");
} 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-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
);
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-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) {
let credentials = await Keychain.getInternetCredentials(
2023-12-28 14:35:15 +05:00
KEYCHAIN_SERVER_DBKEY,
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;
} catch (e) {
2024-02-06 12:08:41 +05:00
DatabaseLogger.error(e);
}
2022-02-28 13:48:59 +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;
} catch (e) {
2024-02-06 12:08:41 +05:00
DatabaseLogger.error(e);
}
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;
if (password.password && password.password === "" && !password.key)
return undefined;
2022-02-28 13:48:59 +05:00
let _data = { ...data };
_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 {};
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
};
}
export async function encrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
2022-02-28 13:48:59 +05:00
let message = {
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)
}));
}