mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-02 12:14:48 +02:00
281 lines
7.5 KiB
TypeScript
281 lines
7.5 KiB
TypeScript
/*
|
|
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 { IStorage } from "@notesnook/core";
|
|
import {
|
|
IndexedDBKVStore,
|
|
LocalStorageKVStore,
|
|
MemoryKVStore,
|
|
IKVStore
|
|
} from "./key-value";
|
|
import { NNCrypto } from "./nncrypto";
|
|
import type {
|
|
Cipher,
|
|
SerializedKey,
|
|
SerializedKeyPair
|
|
} from "@notesnook/crypto";
|
|
import { isFeatureSupported } from "../utils/feature-check";
|
|
import { IKeyStore } from "./key-store";
|
|
import { User } from "@notesnook/core";
|
|
import * as openpgp from "openpgp";
|
|
import { strings } from "@notesnook/intl";
|
|
|
|
type EncryptedKey = { iv: Uint8Array; cipher: BufferSource };
|
|
export type DatabasePersistence = "memory" | "db";
|
|
|
|
const APP_SALT = "oVzKtazBo7d8sb7TBvY9jw";
|
|
|
|
export class NNStorage implements IStorage {
|
|
database: IKVStore;
|
|
|
|
constructor(
|
|
name: string,
|
|
private readonly keyStore: () => IKeyStore | null = () => null,
|
|
persistence: DatabasePersistence = "db"
|
|
) {
|
|
this.database =
|
|
persistence === "memory"
|
|
? new MemoryKVStore()
|
|
: isFeatureSupported("indexedDB")
|
|
? new IndexedDBKVStore(name, "keyvaluepairs")
|
|
: new LocalStorageKVStore();
|
|
}
|
|
|
|
async migrate() {
|
|
if (!this.keyStore) return;
|
|
const user = await this.read<User>("user");
|
|
if (!user) return;
|
|
|
|
const key = await this._getCryptoKey(`_uk_@${user.email}`);
|
|
if (!key) return;
|
|
|
|
await this.database.deleteMany([
|
|
`_uk_@${user.email}`,
|
|
`_uk_@${user.email}@_k`
|
|
]);
|
|
await this.keyStore()?.setValue("userEncryptionKey", key);
|
|
}
|
|
|
|
read<T>(key: string): Promise<T | undefined> {
|
|
if (!key) return Promise.resolve(undefined);
|
|
return this.database.get(key);
|
|
}
|
|
|
|
readMulti<T>(keys: string[]): Promise<[string, T][]> {
|
|
if (keys.length <= 0) return Promise.resolve([]);
|
|
return this.database.getMany(keys.sort());
|
|
}
|
|
|
|
writeMulti<T>(entries: [string, T][]) {
|
|
return this.database.setMany(entries);
|
|
}
|
|
|
|
write<T>(key: string, data: T) {
|
|
return this.database.set(key, data);
|
|
}
|
|
|
|
remove(key: string) {
|
|
return this.database.delete(key);
|
|
}
|
|
|
|
removeMulti(keys: string[]) {
|
|
return this.database.deleteMany(keys);
|
|
}
|
|
|
|
clear() {
|
|
return this.database.clear();
|
|
}
|
|
|
|
getAllKeys() {
|
|
return this.database.keys();
|
|
}
|
|
|
|
async deriveCryptoKey(credentials: SerializedKey) {
|
|
if (!this.keyStore) throw new Error("No key store found!");
|
|
|
|
const { password, salt } = credentials;
|
|
if (!password) throw new Error("Invalid data provided to deriveCryptoKey.");
|
|
|
|
const keyData = await NNCrypto.exportKey(password, salt);
|
|
if (!keyData.key) throw new Error("Invalid key.");
|
|
|
|
await this.keyStore()?.setValue("userEncryptionKey", keyData.key);
|
|
}
|
|
|
|
async getCryptoKey(): Promise<string | undefined> {
|
|
if (!this.keyStore) throw new Error("No key store found!");
|
|
|
|
return this.keyStore()?.getValue("userEncryptionKey");
|
|
}
|
|
|
|
async generateCryptoKey(
|
|
password: string,
|
|
salt?: string
|
|
): Promise<SerializedKey> {
|
|
if (!password)
|
|
throw new Error("Invalid data provided to generateCryptoKey.");
|
|
|
|
return await NNCrypto.exportKey(password, salt);
|
|
}
|
|
|
|
async generatePGPKeyPair(): Promise<SerializedKeyPair> {
|
|
const keys = await openpgp.generateKey({
|
|
userIDs: [{ name: "NN", email: "NN@NN.NN" }]
|
|
});
|
|
return { publicKey: keys.publicKey, privateKey: keys.privateKey };
|
|
}
|
|
|
|
async validatePGPKeyPair(keys: SerializedKeyPair): Promise<{
|
|
isValid: boolean;
|
|
message: string;
|
|
}> {
|
|
try {
|
|
const privateKey = await openpgp.readPrivateKey({
|
|
armoredKey: keys.privateKey
|
|
});
|
|
if (!privateKey.isDecrypted()) {
|
|
return {
|
|
isValid: false,
|
|
message: strings.pgpPrivateKeyProtected()
|
|
};
|
|
}
|
|
|
|
const dummyData = JSON.stringify({
|
|
favorite: true,
|
|
title: "Hello world"
|
|
});
|
|
|
|
const publicKey = await openpgp.readKey({ armoredKey: keys.publicKey });
|
|
const encrypted = await openpgp.encrypt({
|
|
message: await openpgp.createMessage({ text: dummyData }),
|
|
encryptionKeys: publicKey
|
|
});
|
|
|
|
const message = await openpgp.readMessage({ armoredMessage: encrypted });
|
|
const decrypted = await openpgp.decrypt({
|
|
message,
|
|
decryptionKeys: privateKey
|
|
});
|
|
|
|
const isValid = decrypted.data === dummyData;
|
|
return {
|
|
isValid,
|
|
message: isValid ? "" : strings.invalidPgpKeyPair()
|
|
};
|
|
} catch (e) {
|
|
console.error("PGP key pair validation error:", e);
|
|
return {
|
|
isValid: false,
|
|
message: strings.invalidPgpKeyPair()
|
|
};
|
|
}
|
|
}
|
|
|
|
async hash(password: string, email: string): Promise<string> {
|
|
return await NNCrypto.hash(password, `${APP_SALT}${email}`);
|
|
}
|
|
|
|
encrypt(key: SerializedKey, plainText: string): Promise<Cipher<"base64">> {
|
|
return NNCrypto.encrypt(key, plainText, "text", "base64");
|
|
}
|
|
|
|
encryptMulti(
|
|
key: SerializedKey,
|
|
items: string[]
|
|
): Promise<Cipher<"base64">[]> {
|
|
return NNCrypto.encryptMulti(key, items, "text", "base64");
|
|
}
|
|
|
|
decrypt(key: SerializedKey, cipherData: Cipher<"base64">): Promise<string> {
|
|
cipherData.format = "base64";
|
|
return NNCrypto.decrypt(key, cipherData, "text");
|
|
}
|
|
|
|
decryptMulti(
|
|
key: SerializedKey,
|
|
items: Cipher<"base64">[]
|
|
): Promise<string[]> {
|
|
items.forEach((c) => (c.format = "base64"));
|
|
return NNCrypto.decryptMulti(key, items, "text");
|
|
}
|
|
|
|
async decryptPGPMessage(
|
|
privateKeyArmored: string,
|
|
encryptedMessage: string
|
|
): Promise<string> {
|
|
const message = await openpgp.readMessage({
|
|
armoredMessage: encryptedMessage
|
|
});
|
|
const privateKey = await openpgp.readPrivateKey({
|
|
armoredKey: privateKeyArmored
|
|
});
|
|
const decrypted = await openpgp.decrypt({
|
|
message,
|
|
decryptionKeys: privateKey
|
|
});
|
|
return decrypted.data;
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
private async _getCryptoKey(name: string) {
|
|
if (isFeatureSupported("indexedDB") && window?.crypto?.subtle) {
|
|
const pbkdfKey = await this.read<CryptoKey>(name);
|
|
const cipheredKey = await this.read<EncryptedKey | string>(`${name}@_k`);
|
|
if (typeof cipheredKey === "string") return cipheredKey;
|
|
if (!pbkdfKey || !cipheredKey) return;
|
|
return await aesDecrypt(pbkdfKey, cipheredKey);
|
|
} else {
|
|
const key = await this.read<string>(`${name}@_k`);
|
|
if (!key) return;
|
|
return key;
|
|
}
|
|
}
|
|
|
|
// noop
|
|
generateCryptoKeyFallback(
|
|
password: string,
|
|
salt?: string
|
|
): Promise<SerializedKey> {
|
|
return this.generateCryptoKey(password, salt);
|
|
}
|
|
|
|
// noop
|
|
async deriveCryptoKeyFallback(): Promise<void> {}
|
|
}
|
|
|
|
const dec = new TextDecoder();
|
|
async function aesDecrypt(
|
|
cryptoKey: CryptoKey,
|
|
data: EncryptedKey
|
|
): Promise<string> {
|
|
const { iv, cipher } = data;
|
|
|
|
const plainText = await window.crypto.subtle.decrypt(
|
|
{
|
|
name: "AES-GCM",
|
|
iv: iv
|
|
},
|
|
cryptoKey,
|
|
cipher
|
|
);
|
|
return dec.decode(plainText);
|
|
}
|