mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +01:00
feat: add 3-layer-encryption for attachments
This commit is contained in:
@@ -78,7 +78,7 @@ class Merger {
|
|||||||
notebooks = [],
|
notebooks = [],
|
||||||
content = [],
|
content = [],
|
||||||
trash = [],
|
trash = [],
|
||||||
vaultKey = [],
|
vaultKey,
|
||||||
settings = [],
|
settings = [],
|
||||||
attachments = [],
|
attachments = [],
|
||||||
} = serverResponse;
|
} = serverResponse;
|
||||||
|
|||||||
@@ -159,6 +159,45 @@ class UserManager {
|
|||||||
return { key, salt: user.salt };
|
return { key, salt: user.salt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAttachmentsKey() {
|
||||||
|
let user = await this.getUser();
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
let token = await this.tokenManager.getAccessToken();
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
if (!user.attachmentsKey) {
|
||||||
|
user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userEncryptionKey = await this.getEncryptionKey();
|
||||||
|
if (!userEncryptionKey) return;
|
||||||
|
|
||||||
|
if (!user.attachmentsKey) {
|
||||||
|
const key = await this._storage.generateRandomKey();
|
||||||
|
const encryptedKey = await this._storage.encrypt(
|
||||||
|
userEncryptionKey,
|
||||||
|
JSON.stringify(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
user.attachmentsKey = encryptedKey;
|
||||||
|
await http.patch.json(
|
||||||
|
`${constants.API_HOST}${ENDPOINTS.user}`,
|
||||||
|
user,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.setUser(user);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainData = await this._storage.decrypt(
|
||||||
|
userEncryptionKey,
|
||||||
|
user.attachmentsKey
|
||||||
|
);
|
||||||
|
return JSON.parse(plainData);
|
||||||
|
}
|
||||||
|
|
||||||
async sendVerificationEmail() {
|
async sendVerificationEmail() {
|
||||||
let token = await this.tokenManager.getAccessToken();
|
let token = await this.tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|||||||
@@ -12,36 +12,25 @@ export default class Attachments extends Collection {
|
|||||||
this.key = null;
|
this.key = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getEncryptionKey() {
|
|
||||||
if (!this.key) this.key = await this._db.user.getEncryptionKey();
|
|
||||||
if (!this.key)
|
|
||||||
throw new Error(
|
|
||||||
"Failed to get user encryption key. Cannot cache attachments."
|
|
||||||
);
|
|
||||||
return this.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {{
|
* @param {{
|
||||||
* iv: string,
|
* iv: string,
|
||||||
* salt: string,
|
|
||||||
* length: number,
|
* length: number,
|
||||||
* alg: string,
|
* alg: string,
|
||||||
* hash: string,
|
* hash: string,
|
||||||
* hashType: string,
|
* hashType: string,
|
||||||
* filename: string,
|
* filename: string,
|
||||||
* type: string
|
* type: string,
|
||||||
|
* salt: string,
|
||||||
|
* key: {}
|
||||||
* }} attachment
|
* }} attachment
|
||||||
* @param {string} noteId Optional as attachments will be parsed at extraction time
|
* @param {string} noteId Optional as attachments will be parsed at extraction time
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
add(attachment, noteId) {
|
async add(attachment, noteId) {
|
||||||
if (!attachment)
|
if (!attachment) return console.error("attachment cannot be undefined");
|
||||||
return console.error("attachment or noteId cannot be null");
|
|
||||||
|
|
||||||
if (attachment.remote) return this._collection.addItem(attachment);
|
if (attachment.remote) return this._collection.addItem(attachment);
|
||||||
|
|
||||||
if (!attachment.hash) throw new Error("Please provide attachment hash.");
|
if (!attachment.hash) throw new Error("Please provide attachment hash.");
|
||||||
|
|
||||||
const oldAttachment = this.all.find(
|
const oldAttachment = this.all.find(
|
||||||
@@ -55,21 +44,23 @@ export default class Attachments extends Collection {
|
|||||||
return this._collection.updateItem(oldAttachment);
|
return this._collection.updateItem(oldAttachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { iv, salt, length, alg, hash, hashType, filename, type } =
|
const { iv, length, alg, hash, hashType, filename, salt, type, key } =
|
||||||
attachment;
|
attachment;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!iv ||
|
!iv ||
|
||||||
!salt ||
|
|
||||||
!length ||
|
!length ||
|
||||||
!alg ||
|
!alg ||
|
||||||
!hash ||
|
!hash ||
|
||||||
!hashType ||
|
!hashType ||
|
||||||
!filename ||
|
!filename ||
|
||||||
!type
|
!type ||
|
||||||
|
!salt ||
|
||||||
|
!key
|
||||||
)
|
)
|
||||||
throw new Error("Could not add attachment: all properties are required.");
|
throw new Error("Could not add attachment: all properties are required.");
|
||||||
|
|
||||||
|
const encryptedKey = await this._encryptKey(key);
|
||||||
const attachmentItem = {
|
const attachmentItem = {
|
||||||
id: id(),
|
id: id(),
|
||||||
noteIds: noteId ? [noteId] : [],
|
noteIds: noteId ? [noteId] : [],
|
||||||
@@ -77,6 +68,7 @@ export default class Attachments extends Collection {
|
|||||||
salt,
|
salt,
|
||||||
length,
|
length,
|
||||||
alg,
|
alg,
|
||||||
|
key: encryptedKey,
|
||||||
metadata: {
|
metadata: {
|
||||||
hash,
|
hash,
|
||||||
hashType,
|
hashType,
|
||||||
@@ -91,6 +83,17 @@ export default class Attachments extends Collection {
|
|||||||
return this._collection.addItem(attachmentItem);
|
return this._collection.addItem(attachmentItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateKey() {
|
||||||
|
await this._getEncryptionKey();
|
||||||
|
return await this._db.storage.generateRandomKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptKey(key) {
|
||||||
|
const encryptionKey = await this._getEncryptionKey();
|
||||||
|
const plainData = await this._db.storage.decrypt(encryptionKey, key);
|
||||||
|
return JSON.parse(plainData);
|
||||||
|
}
|
||||||
|
|
||||||
async delete(hash, noteId) {
|
async delete(hash, noteId) {
|
||||||
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
||||||
if (!attachment || !deleteItem(attachment.noteIds, noteId)) return;
|
if (!attachment || !deleteItem(attachment.noteIds, noteId)) return;
|
||||||
@@ -253,4 +256,28 @@ export default class Attachments extends Collection {
|
|||||||
get all() {
|
get all() {
|
||||||
return this._collection.getItems();
|
return this._collection.getItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _encryptKey(key) {
|
||||||
|
const encryptionKey = await this._getEncryptionKey();
|
||||||
|
const encryptedKey = await this._db.storage.encrypt(
|
||||||
|
encryptionKey,
|
||||||
|
JSON.stringify(key)
|
||||||
|
);
|
||||||
|
return encryptedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _getEncryptionKey() {
|
||||||
|
if (!this.key) this.key = await this._db.user.getAttachmentsKey();
|
||||||
|
if (!this.key)
|
||||||
|
throw new Error(
|
||||||
|
"Failed to get user encryption key. Cannot cache attachments."
|
||||||
|
);
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { randomBytes } from "../utils/random";
|
||||||
|
|
||||||
export default class Storage {
|
export default class Storage {
|
||||||
constructor(context) {
|
constructor(context) {
|
||||||
this.storage = context;
|
this.storage = context;
|
||||||
@@ -46,4 +48,10 @@ export default class Storage {
|
|||||||
getCryptoKey(name) {
|
getCryptoKey(name) {
|
||||||
return this.storage.getCryptoKey(name);
|
return this.storage.getCryptoKey(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateRandomKey() {
|
||||||
|
const passwordBytes = randomBytes(124);
|
||||||
|
const password = passwordBytes.toString("base64");
|
||||||
|
return await this.storage.generateCryptoKey(password, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user