/*
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 .
*/
import { CHECK_IDS, EV, EVENTS, checkIsUserPremium } from "../common";
import { tinyToTiptap } from "../migrations";
const ERASE_TIME = 1000 * 60 * 30;
var ERASER_TIMEOUT = null;
export default class Vault {
get _password() {
return this._vaultPassword;
}
set _password(value) {
this._vaultPassword = value;
if (value) {
this._startEraser();
}
}
_startEraser() {
clearTimeout(ERASER_TIMEOUT);
ERASER_TIMEOUT = setTimeout(() => {
this._password = null;
EV.publish(EVENTS.vaultLocked);
}, ERASE_TIME);
}
/**
*
* @param {import('./index').default} db
*/
constructor(db) {
this._db = db;
this._storage = db.storage;
this._key = "svvaads1212#2123";
this._vaultPassword = null;
this.ERRORS = {
noVault: "ERR_NO_VAULT",
vaultLocked: "ERR_VAULT_LOCKED",
wrongPassword: "ERR_WRONG_PASSWORD"
};
EV.subscribe(EVENTS.userLoggedOut, () => {
this._password = null;
});
}
/**
* Creates a new vault
* @param {string} password The password
* @returns {Promise}
*/
async create(password) {
if (!(await checkIsUserPremium(CHECK_IDS.vaultAdd))) return;
const vaultKey = await this._getKey();
if (!vaultKey || !vaultKey.cipher || !vaultKey.iv) {
const encryptedData = await this._storage.encrypt(
{ password },
this._key
);
await this._setKey(encryptedData);
this._password = password;
}
return true;
}
/**
* Unlocks the vault with the given password
* @param {string} password The password
* @throws ERR_NO_VAULT | ERR_WRONG_PASSWORD
* @returns {Promise}
*/
async unlock(password) {
const vaultKey = await this._getKey();
if (!(await this.exists(vaultKey))) throw new Error(this.ERRORS.noVault);
try {
await this._storage.decrypt({ password }, vaultKey);
} catch (e) {
throw new Error(this.ERRORS.wrongPassword);
}
this._password = password;
return true;
}
async changePassword(oldPassword, newPassword) {
if (await this.unlock(oldPassword)) {
await this._db.notes.init();
const contentItems = [];
for (const note of this._db.notes.locked) {
try {
let encryptedContent = await this._db.content.raw(note.contentId);
let content = await this.decryptContent(
encryptedContent,
oldPassword
);
contentItems.push({
...content,
id: note.contentId,
noteId: note.id
});
} catch (e) {
throw new Error(
`Could not decrypt content of note ${note.id}. Error: ${e.message}`
);
}
}
for (const content of contentItems) {
await this._encryptContent(
content.id,
null,
content.data,
content.type,
newPassword
);
}
await this._storage.remove("vaultKey");
await this.create(newPassword);
}
}
async clear(password) {
if (await this.unlock(password)) {
await this._db.notes.init();
for (var note of this._db.notes.locked) {
await this._unlockNote(note, password, true);
}
}
}
async delete(deleteAllLockedNotes = false) {
if (deleteAllLockedNotes) {
await this._db.notes.init();
await this._db.notes.remove(
...this._db.notes.locked.map((note) => note.id)
);
}
await this._storage.remove("vaultKey");
this._password = null;
}
/**
* Locks (add to vault) a note
* @param {string} noteId The id of the note to lock
*/
async add(noteId) {
if (!(await checkIsUserPremium(CHECK_IDS.vaultAdd))) return;
await this._check();
await this._lockNote({ id: noteId }, this._password);
await this._db.noteHistory.clearSessions(noteId);
}
/**
* Permanently unlocks (remove from vault) a note
* @param {string} noteId The note id
* @param {string} password The password to unlock note with
*/
async remove(noteId, password) {
const note = this._db.notes.note(noteId);
if (!note) return;
await this._unlockNote(note.data, password, true);
}
/**
* Temporarily unlock (open) a note
* @param {string} noteId The note id
* @param {string} password The password to open note with
*/
async open(noteId, password) {
const note = this._db.notes.note(noteId);
if (!note) return;
const unlockedNote = await this._unlockNote(note.data, password, false);
this._password = password;
return unlockedNote;
}
/**
* Saves a note in the vault
* @param {{Object}} note The note to save into the vault
*/
async save(note) {
if (!note) return;
await this._check();
// roll over erase timer
this._startEraser();
return await this._lockNote(note, this._password);
}
async exists(vaultKey) {
if (!vaultKey) vaultKey = await this._getKey();
return vaultKey && vaultKey.cipher && vaultKey.iv;
}
// Private & internal methods
/** @private */
_locked() {
return !this._password || !this._password.length;
}
/** @private */
async _check() {
if (!(await this.exists())) {
throw new Error(this.ERRORS.noVault);
}
if (this._locked()) {
throw new Error(this.ERRORS.vaultLocked);
}
}
/** @private */
async _encryptContent(contentId, sessionId, content, type, password) {
let encryptedContent = await this._storage.encrypt(
{ password },
JSON.stringify(content)
);
await this._db.content.add({
id: contentId,
sessionId,
data: encryptedContent,
type
});
}
async decryptContent(encryptedContent, password = null) {
if (!password) {
await this._check();
password = this._password;
}
if (encryptedContent.noteId && typeof encryptedContent.data !== "object") {
await this._db.notes.add({
id: encryptedContent.noteId,
locked: false
});
return encryptedContent;
}
let decryptedContent = await this._storage.decrypt(
{ password },
encryptedContent.data
);
const content = {
type: encryptedContent.type,
data: JSON.parse(decryptedContent)
};
// #MIGRATION: convert tiny to tiptap
if (content.type === "tiny") {
content.type = "tiptap";
content.data = tinyToTiptap(content.data);
}
return content;
}
/** @private */
async _lockNote(note, password) {
let { id, content: { type, data } = {}, sessionId, title } = note;
note = this._db.notes.note(id);
if (!note) return;
note = note.data;
const contentId = note.contentId;
if (!contentId) throw new Error("Cannot lock note because it is empty.");
// Case: when note is being newly locked
if (!note.locked && (!data || !type)) {
let content = await this._db.content.raw(contentId, false);
// NOTE:
// At this point, the note already has all the attachments extracted
// so we should just encrypt it as normal.
data = content.data;
type = content.type;
} else if (data && type) {
const content = await this._db.content.extractAttachments({
data,
type,
noteId: id
});
data = content.data;
type = content.type;
}
if (data && type)
await this._encryptContent(contentId, sessionId, data, type, password);
return await this._db.notes.add({
id,
locked: true,
headline: "",
title: title || note.title,
favorite: note.favorite,
localOnly: note.localOnly,
readonly: note.readonly,
dateEdited: Date.now()
});
}
/** @private */
async _unlockNote(note, password, perm = false) {
let encryptedContent = await this._db.content.raw(note.contentId);
let content = await this.decryptContent(encryptedContent, password);
if (perm) {
await this._db.notes.add({
id: note.id,
locked: false,
headline: note.headline,
contentId: note.contentId,
content
});
// await this._db.content.add({ id: note.contentId, data: content });
return;
}
return {
...note,
content
};
}
/** @inner */
async _getKey() {
return await this._storage.read("vaultKey");
}
/** @inner */
async _setKey(vaultKey) {
if (!vaultKey) return;
await this._storage.write("vaultKey", vaultKey);
}
}