half way of encryption refactor

This commit is contained in:
fyears 2024-03-23 16:38:58 +08:00
parent 98380b6c92
commit 6825241071
16 changed files with 322 additions and 207 deletions

View File

@ -58,6 +58,7 @@
"@aws-sdk/signature-v4-crt": "^3.474.0",
"@aws-sdk/types": "^3.468.0",
"@azure/msal-node": "^2.6.0",
"@fyears/rclone-crypt": "^0.0.6",
"@fyears/tsqueue": "^1.0.1",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@smithy/fetch-http-handler": "^2.3.1",

View File

@ -88,6 +88,8 @@ export type SyncDirectionType =
| "incremental_pull_only"
| "incremental_push_only";
export type CipherMethodType = "rclone-base64" | "openssl-base64" | "unknown";
export interface RemotelySavePluginSettings {
s3: S3Config;
webdav: WebdavConfig;
@ -119,6 +121,8 @@ export interface RemotelySavePluginSettings {
enableMobileStatusBar?: boolean;
encryptionMethod?: CipherMethodType;
/**
* @deprecated
*/

122
src/encryptUnified.ts Normal file
View File

@ -0,0 +1,122 @@
import { CipherMethodType } from "./baseTypes";
import * as openssl from "./encryptOpenSSL";
import { isVaildText } from "./misc";
export class Cipher {
readonly password: string;
readonly method: CipherMethodType;
constructor(password: string, method: CipherMethodType) {
this.password = password ?? "";
this.method = method;
}
isPasswordEmpty() {
return this.password === "";
}
async encryptContent(content: ArrayBuffer) {
if (this.password === "") {
return content;
}
if (this.method === "openssl-base64") {
return await openssl.encryptArrayBuffer(content, this.password);
} else if (this.method === "rclone-base64") {
throw Error("not implemented yet");
} else {
throw Error(`not supported encrypt method=${this.method}`);
}
}
async decryptContent(content: ArrayBuffer) {
if (this.password === "") {
return content;
}
if (this.method === "openssl-base64") {
return await openssl.decryptArrayBuffer(content, this.password);
} else if (this.method === "rclone-base64") {
throw Error("not implemented yet");
} else {
throw Error(`not supported encrypt method=${this.method}`);
}
}
async encryptName(name: string) {
if (this.password === "") {
return name;
}
if (this.method === "openssl-base64") {
return await openssl.encryptStringToBase64url(name, this.password);
} else if (this.method === "rclone-base64") {
throw Error("not implemented yet");
} else {
throw Error(`not supported encrypt method=${this.method}`);
}
}
async decryptName(name: string) {
if (this.password === "") {
return name;
}
if (this.method === "openssl-base64") {
if (name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE32)) {
// backward compitable with the openssl-base32
try {
const res = await openssl.decryptBase32ToString(name, this.password);
if (isVaildText(res)) {
return res;
} else {
throw Error(`cannot decrypt name=${name}`);
}
} catch (error) {
throw Error(`cannot decrypt name=${name}`);
}
} else if (name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE64URL)) {
try {
const res = await openssl.decryptBase64urlToString(
name,
this.password
);
if (isVaildText(res)) {
return res;
} else {
throw Error(`cannot decrypt name=${name}`);
}
} catch (error) {
throw Error(`cannot decrypt name=${name}`);
}
}
} else if (this.method === "rclone-base64") {
throw Error("not implemented yet");
} else {
throw Error(`not supported encrypt method=${this.method}`);
}
}
getSizeFromOrigToEnc(x: number) {
if (this.password === "") {
return x;
}
if (this.method === "openssl-base64") {
return openssl.getSizeFromOrigToEnc(x);
} else if (this.method === "rclone-base64") {
throw Error("not implemented yet");
} else {
throw Error(`not supported encrypt method=${this.method}`);
}
}
/**
* quick guess, no actual decryption here
* @param name
* @returns
*/
static isLikelyEncryptedName(name: string): boolean {
if (
name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE32) ||
name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE64URL)
) {
return true;
}
return false;
}
}

View File

@ -109,7 +109,12 @@
"modal_sizesconflict_copynotice": "All the sizes conflicts info have been copied to the clipboard!",
"settings_basic": "Basic Settings",
"settings_password": "Encryption Password",
"settings_password_desc": "Password for E2E encryption. Empty for no password. You need to click \"Confirm\". Attention: the password and other info are saved locally.",
"settings_password_desc": "Password for E2E encryption. Empty for no password. You need to click \"Confirm\". Attention: The password and other info are saved locally. After changing the password, you need to manually delete every original files in the remote, and re-sync (so that upload) the encrypted files again.",
"settings_encryptionmethod": "Encryption Method",
"settings_encryptionmethod_desc": "Encryption method for E2E encryption. RClone method is recommended but it doesn't encrypt path structure. OpenSSL is the legacy method of this plugin. Attention: After switching the method, you need to manually delete every original files in the remote and re-sync (so that upload) the encrypted files again.",
"settings_encryptionmethod_rclone": "RClone (recommended)",
"settings_encryptionmethod_openssl": "OpenSSL (legacy)",
"settings_autorun": "Schedule For Auto Run",
"settings_autorun_desc": "The plugin tries to schedule the running after every interval. Battery may be impacted.",
"settings_autorun_notset": "(not set)",

View File

@ -109,7 +109,11 @@
"modal_sizesconflict_copynotice": "所有的文件大小冲突信息,已被复制到剪贴板!",
"settings_basic": "基本设置",
"settings_password": "密码",
"settings_password_desc": "端到端加密的密码。不填写则代表没密码。您需要点击“确认”来修改。注意:密码和其它信息都会在本地保存。",
"settings_password_desc": "端到端加密的密码。不填写则代表没密码。您需要点击“确认”来修改。注意:密码和其它信息都会在本地保存。如果您修改了密码,您需要手动删除远端的所有文件,重新同步(从而上传)加密文件。",
"settings_encryptionmethod": "加密方法",
"settings_encryptionmethod_desc": "端到端加密的方法。推荐选用 RClone 方法但是它没有加密文件路径结构。OpenSSL 是本插件一开始就支持的方式。如果您修改了加密方法您需要手动删除远端的所有文件,重新同步(从而上传)加密文件。",
"settings_encryptionmethod_rclone": "RClone推荐",
"settings_encryptionmethod_openssl": "OpenSSL旧方法",
"settings_autorun": "自动运行",
"settings_autorun_desc": "每隔一段时间,此插件尝试自动同步。会影响到电池用量。",
"settings_autorun_notset": "(不设置)",

View File

@ -109,7 +109,11 @@
"modal_sizesconflict_copynotice": "所有的檔案大小衝突資訊,已被複制到剪貼簿!",
"settings_basic": "基本設定",
"settings_password": "密碼",
"settings_password_desc": "端到端加密的密碼。不填寫則代表沒密碼。您需要點選“確認”來修改。注意:密碼和其它資訊都會在本地儲存。",
"settings_password_desc": "端到端加密的密碼。不填寫則代表沒密碼。您需要點選“確認”來修改。注意:密碼和其它資訊都會在本地儲存。如果您修改了密碼,您需要手動刪除遠端的所有檔案,重新同步(從而上傳)加密檔案。",
"settings_encryptionmethod": "加密方法",
"settings_encryptionmethod_desc": "端到端加密的方法。推薦選用 RClone 方法但是它沒有加密檔案路徑結構。OpenSSL 是本外掛一開始就支援的方式。如果您修改了加密方法您需要手動刪除遠端的所有檔案,重新同步(從而上傳)加密檔案。",
"settings_encryptionmethod_rclone": "RClone推薦",
"settings_encryptionmethod_openssl": "OpenSSL舊方法",
"settings_autorun": "自動執行",
"settings_autorun_desc": "每隔一段時間,此外掛嘗試自動同步。會影響到電池用量。",
"settings_autorun_notset": "(不設定)",

View File

@ -67,6 +67,7 @@ import { SyncAlgoV3Modal } from "./syncAlgoV3Notice";
import AggregateError from "aggregate-error";
import { exportVaultSyncPlansToFiles } from "./debugMode";
import { changeMobileStatusBar, compareVersion } from "./misc";
import { Cipher } from "./encryptUnified";
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
s3: DEFAULT_S3_CONFIG,
@ -97,6 +98,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
syncDirection: "bidirectional",
obfuscateSettingFile: true,
enableMobileStatusBar: false,
encryptionMethod: "unknown",
};
interface OAuth2Info {
@ -254,10 +256,12 @@ export default class RemotelySavePlugin extends Plugin {
getNotice(t("syncrun_step3"));
}
this.syncStatus = "checking_password";
const passwordCheckResult = await isPasswordOk(
remoteEntityList,
this.settings.password
const cipher = new Cipher(
this.settings.password,
this.settings.encryptionMethod ?? "unknown"
);
const passwordCheckResult = await isPasswordOk(remoteEntityList, cipher);
if (!passwordCheckResult.ok) {
getNotice(t("syncrun_passworderr"));
throw Error(passwordCheckResult.reason);
@ -306,7 +310,7 @@ export default class RemotelySavePlugin extends Plugin {
this.app.vault.configDir,
this.settings.syncUnderscoreItems ?? false,
this.settings.ignorePaths ?? [],
this.settings.password,
cipher,
this.settings.serviceType
);
mixedEntityMappings = await getSyncPlanInplace(
@ -341,7 +345,7 @@ export default class RemotelySavePlugin extends Plugin {
this.vaultRandomID,
profileID,
this.app.vault,
this.settings.password,
cipher,
this.settings.concurrency ?? 5,
(key: string) => self.trash(key),
this.settings.protectModifyPercentage ?? 50,
@ -911,6 +915,22 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.enableMobileStatusBar = false;
}
if (
this.settings.encryptionMethod === undefined ||
this.settings.encryptionMethod === "unknown"
) {
if (
this.settings.password === undefined ||
this.settings.password === ""
) {
// we have a preferred way
this.settings.encryptionMethod = "rclone-base64";
} else {
// likely to be inherited from the old version
this.settings.encryptionMethod = "openssl-base64";
}
}
await this.saveSettings();
}

View File

@ -12,6 +12,7 @@ import * as dropbox from "./remoteForDropbox";
import * as onedrive from "./remoteForOnedrive";
import * as s3 from "./remoteForS3";
import * as webdav from "./remoteForWebdav";
import { Cipher } from "./encryptUnified";
export class RemoteClient {
readonly serviceType: SUPPORTED_SERVICES_TYPE;
@ -105,8 +106,8 @@ export class RemoteClient {
uploadToRemote = async (
fileOrFolderPath: string,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
isRecursively: boolean,
cipher: Cipher,
remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined,
uploadRaw: boolean = false,
@ -119,7 +120,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
isRecursively,
password,
cipher,
remoteEncryptedKey,
uploadRaw,
rawContent
@ -130,7 +131,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
isRecursively,
password,
cipher,
remoteEncryptedKey,
uploadRaw,
rawContent
@ -141,7 +142,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
isRecursively,
password,
cipher,
remoteEncryptedKey,
foldersCreatedBefore,
uploadRaw,
@ -153,7 +154,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
isRecursively,
password,
cipher,
remoteEncryptedKey,
foldersCreatedBefore,
uploadRaw,
@ -185,7 +186,7 @@ export class RemoteClient {
fileOrFolderPath: string,
vault: Vault,
mtime: number,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = "",
skipSaving: boolean = false
) => {
@ -196,7 +197,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
mtime,
password,
cipher,
remoteEncryptedKey,
skipSaving
);
@ -206,7 +207,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
mtime,
password,
cipher,
remoteEncryptedKey,
skipSaving
);
@ -216,7 +217,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
mtime,
password,
cipher,
remoteEncryptedKey,
skipSaving
);
@ -226,7 +227,7 @@ export class RemoteClient {
fileOrFolderPath,
vault,
mtime,
password,
cipher,
remoteEncryptedKey,
skipSaving
);
@ -237,7 +238,7 @@ export class RemoteClient {
deleteFromRemote = async (
fileOrFolderPath: string,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = ""
) => {
if (this.serviceType === "s3") {
@ -245,28 +246,28 @@ export class RemoteClient {
s3.getS3Client(this.s3Config!),
this.s3Config!,
fileOrFolderPath,
password,
cipher,
remoteEncryptedKey
);
} else if (this.serviceType === "webdav") {
return await webdav.deleteFromRemote(
this.webdavClient!,
fileOrFolderPath,
password,
cipher,
remoteEncryptedKey
);
} else if (this.serviceType === "dropbox") {
return await dropbox.deleteFromRemote(
this.dropboxClient!,
fileOrFolderPath,
password,
cipher,
remoteEncryptedKey
);
} else if (this.serviceType === "onedrive") {
return await onedrive.deleteFromRemote(
this.onedriveClient!,
fileOrFolderPath,
password,
cipher,
remoteEncryptedKey
);
} else {

View File

@ -10,7 +10,6 @@ import {
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
UploadedType,
} from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import {
bufferToArrayBuffer,
getFolderLevels,
@ -18,6 +17,7 @@ import {
headersToRecord,
mkdirpInVault,
} from "./misc";
import { Cipher } from "./encryptUnified";
export { Dropbox } from "dropbox";
@ -451,8 +451,8 @@ export const uploadToRemote = async (
client: WrappedDropboxClient,
fileOrFolderPath: string,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
isRecursively: boolean,
cipher: Cipher,
remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined,
uploadRaw: boolean = false,
@ -463,7 +463,7 @@ export const uploadToRemote = async (
await client.init();
let uploadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error(
`uploadToRemote(dropbox) you have password but remoteEncryptedKey is empty!`
@ -497,7 +497,7 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
}
// folder
if (password === "") {
if (cipher.isPasswordEmpty()) {
// if not encrypted, mkdir a remote folder
if (foldersCreatedBefore?.has(uploadFile)) {
// created, pass
@ -564,8 +564,8 @@ export const uploadToRemote = async (
localContent = await vault.adapter.readBinary(fileOrFolderPath);
}
let remoteContent = localContent;
if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password);
if (!cipher.isPasswordEmpty()) {
remoteContent = await cipher.encryptContent(localContent);
}
// in dropbox, we don't need to create folders before uploading! cool!
// TODO: filesUploadSession for larger files (>=150 MB)
@ -670,7 +670,7 @@ export const downloadFromRemote = async (
fileOrFolderPath: string,
vault: Vault,
mtime: number,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = "",
skipSaving: boolean = false
) => {
@ -691,14 +691,14 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0);
} else {
let downloadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey;
}
downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
localContent = await decryptArrayBuffer(remoteContent, password);
if (!cipher.isPasswordEmpty()) {
localContent = await cipher.decryptContent(remoteContent);
}
if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@ -712,14 +712,14 @@ export const downloadFromRemote = async (
export const deleteFromRemote = async (
client: WrappedDropboxClient,
fileOrFolderPath: string,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = ""
) => {
if (fileOrFolderPath === "/") {
return;
}
let remoteFileName = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir);

View File

@ -17,13 +17,13 @@ import {
Entity,
UploadedType,
} from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import {
bufferToArrayBuffer,
getRandomArrayBuffer,
getRandomIntInclusive,
mkdirpInVault,
} from "./misc";
import { Cipher } from "./encryptUnified";
const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"];
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
@ -694,8 +694,8 @@ export const uploadToRemote = async (
client: WrappedOnedriveClient,
fileOrFolderPath: string,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
isRecursively: boolean,
cipher: Cipher,
remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined,
uploadRaw: boolean = false,
@ -704,7 +704,7 @@ export const uploadToRemote = async (
await client.init();
let uploadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error(
`uploadToRemote(onedrive) you have password but remoteEncryptedKey is empty!`
@ -734,7 +734,7 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
}
// folder
if (password === "") {
if (cipher.isPasswordEmpty()) {
// if not encrypted, mkdir a remote folder
if (foldersCreatedBefore?.has(uploadFile)) {
// created, pass
@ -770,9 +770,8 @@ export const uploadToRemote = async (
1,
65536 /* max allowed */
);
const arrBufRandom = await encryptArrayBuffer(
getRandomArrayBuffer(byteLengthRandom),
password
const arrBufRandom = await cipher.encryptContent(
getRandomArrayBuffer(byteLengthRandom)
);
// an encrypted folder is always small, we just use put here
@ -816,8 +815,8 @@ export const uploadToRemote = async (
localContent = await vault.adapter.readBinary(fileOrFolderPath);
}
let remoteContent = localContent;
if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password);
if (!cipher.isPasswordEmpty()) {
remoteContent = await cipher.encryptContent(localContent);
}
// no need to create parent folders firstly, cool!
@ -930,7 +929,7 @@ export const downloadFromRemote = async (
fileOrFolderPath: string,
vault: Vault,
mtime: number,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = "",
skipSaving: boolean = false
) => {
@ -948,14 +947,14 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0);
} else {
let downloadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey;
}
downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
localContent = await decryptArrayBuffer(remoteContent, password);
if (!cipher.isPasswordEmpty()) {
localContent = await cipher.decryptContent(remoteContent);
}
if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@ -969,14 +968,14 @@ export const downloadFromRemote = async (
export const deleteFromRemote = async (
client: WrappedOnedriveClient,
fileOrFolderPath: string,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = ""
) => {
if (fileOrFolderPath === "/") {
return;
}
let remoteFileName = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir);

View File

@ -33,7 +33,6 @@ import {
UploadedType,
VALID_REQURL,
} from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import {
arrayBufferToBuffer,
bufferToArrayBuffer,
@ -43,6 +42,7 @@ import {
export { S3Client } from "@aws-sdk/client-s3";
import PQueue from "p-queue";
import { Cipher } from "./encryptUnified";
////////////////////////////////////////////////////////////////////////////////
// special handler using Obsidian requestUrl
@ -358,8 +358,8 @@ export const uploadToRemote = async (
s3Config: S3Config,
fileOrFolderPath: string,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
isRecursively: boolean,
cipher: Cipher,
remoteEncryptedKey: string = "",
uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = "",
@ -368,7 +368,7 @@ export const uploadToRemote = async (
): Promise<UploadedType> => {
console.debug(`uploading ${fileOrFolderPath}`);
let uploadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error(
`uploadToRemote(s3) you have password but remoteEncryptedKey is empty!`
@ -416,7 +416,7 @@ export const uploadToRemote = async (
// file
// we ignore isRecursively parameter here
let contentType = DEFAULT_CONTENT_TYPE;
if (password === "") {
if (cipher.isPasswordEmpty()) {
contentType =
mime.contentType(
mime.lookup(fileOrFolderPath) || DEFAULT_CONTENT_TYPE
@ -447,8 +447,8 @@ export const uploadToRemote = async (
}
}
let remoteContent = localContent;
if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password);
if (!cipher.isPasswordEmpty()) {
remoteContent = await cipher.encryptContent(localContent);
}
const bytesIn5MB = 5242880;
@ -645,8 +645,8 @@ export const downloadFromRemote = async (
fileOrFolderPath: string,
vault: Vault,
mtime: number,
password: string = "",
remoteEncryptedKey: string = "",
cipher: Cipher,
remoteEncryptedKey: string,
skipSaving: boolean = false
) => {
const isFolder = fileOrFolderPath.endsWith("/");
@ -664,7 +664,7 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0);
} else {
let downloadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey;
}
downloadFile = getRemoteWithPrefixPath(
@ -677,8 +677,8 @@ export const downloadFromRemote = async (
downloadFile
);
let localContent = remoteContent;
if (password !== "") {
localContent = await decryptArrayBuffer(remoteContent, password);
if (!cipher.isPasswordEmpty()) {
localContent = await cipher.decryptContent(remoteContent);
}
if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@ -700,14 +700,14 @@ export const deleteFromRemote = async (
s3Client: S3Client,
s3Config: S3Config,
fileOrFolderPath: string,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = ""
) => {
if (fileOrFolderPath === "/") {
return;
}
let remoteFileName = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getRemoteWithPrefixPath(
@ -721,7 +721,7 @@ export const deleteFromRemote = async (
})
);
if (fileOrFolderPath.endsWith("/") && password === "") {
if (fileOrFolderPath.endsWith("/") && cipher.isPasswordEmpty()) {
const x = await listFromRemoteRaw(s3Client, s3Config, remoteFileName);
x.forEach(async (element) => {
await s3Client.send(
@ -731,7 +731,7 @@ export const deleteFromRemote = async (
})
);
});
} else if (fileOrFolderPath.endsWith("/") && password !== "") {
} else if (fileOrFolderPath.endsWith("/") && !cipher.isPasswordEmpty()) {
// TODO
} else {
// pass

View File

@ -4,10 +4,11 @@ import { Platform, Vault, requestUrl } from "obsidian";
import { Queue } from "@fyears/tsqueue";
import chunk from "lodash/chunk";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";
import { getReasonPhrase } from "http-status-codes";
import { Entity, UploadedType, VALID_REQURL, WebdavConfig } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
import { Cipher } from "./encryptUnified";
import type {
FileStat,
@ -139,7 +140,6 @@ if (VALID_REQURL) {
// @ts-ignore
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
import cloneDeep from "lodash/cloneDeep";
export type { WebDAVClient } from "webdav";
export const DEFAULT_WEBDAV_CONFIG = {
@ -316,15 +316,15 @@ export const uploadToRemote = async (
client: WrappedWebdavClient,
fileOrFolderPath: string,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
isRecursively: boolean,
cipher: Cipher,
remoteEncryptedKey: string = "",
uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = ""
): Promise<UploadedType> => {
await client.init();
let uploadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error(
`uploadToRemote(webdav) you have password but remoteEncryptedKey is empty!`
@ -343,7 +343,7 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
}
// folder
if (password === "") {
if (cipher.isPasswordEmpty()) {
// if not encrypted, mkdir a remote folder
await client.client.createDirectory(uploadFile, {
recursive: true,
@ -386,8 +386,8 @@ export const uploadToRemote = async (
mtimeCli = (await vault.adapter.stat(fileOrFolderPath))?.mtime;
}
let remoteContent = localContent;
if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password);
if (!cipher.isPasswordEmpty()) {
remoteContent = await cipher.encryptContent(localContent);
}
// updated 20220326: the algorithm guarantee this
// // we need to create folders before uploading
@ -491,7 +491,7 @@ export const downloadFromRemote = async (
fileOrFolderPath: string,
vault: Vault,
mtime: number,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = "",
skipSaving: boolean = false
) => {
@ -512,15 +512,15 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0);
} else {
let downloadFile = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey;
}
downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir);
// console.info(`downloadFile=${downloadFile}`);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
localContent = await decryptArrayBuffer(remoteContent, password);
if (!cipher.isPasswordEmpty()) {
localContent = await cipher.decryptContent(remoteContent);
}
if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@ -534,14 +534,14 @@ export const downloadFromRemote = async (
export const deleteFromRemote = async (
client: WrappedWebdavClient,
fileOrFolderPath: string,
password: string = "",
cipher: Cipher,
remoteEncryptedKey: string = ""
) => {
if (fileOrFolderPath === "/") {
return;
}
let remoteFileName = fileOrFolderPath;
if (password !== "") {
if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir);

View File

@ -22,6 +22,7 @@ import {
VALID_REQURL,
WebdavAuthType,
WebdavDepthType,
CipherMethodType,
} from "./baseTypes";
import { exportVaultSyncPlansToFiles } from "./debugMode";
import { exportQrCodeUri } from "./importExport";
@ -1634,6 +1635,28 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
new Setting(basicDiv)
.setName(t("settings_encryptionmethod"))
.setDesc(t("settings_encryptionmethod_desc"))
.addDropdown((dropdown) => {
dropdown.addOption("rclone", t("settings_encryptionmethod_rclone"));
dropdown.addOption("openssl", t("settings_encryptionmethod_openssl"));
if (this.plugin.settings.encryptionMethod === "rclone-base64") {
dropdown.setValue("rclone");
} else if (this.plugin.settings.encryptionMethod === "openssl-base64") {
dropdown.setValue("openssl");
}
dropdown.onChange(async (val: string) => {
if (val === "rclone") {
this.plugin.settings.encryptionMethod = "rclone-base64";
} else if (val === "openssl") {
this.plugin.settings.encryptionMethod = "openssl-base64";
}
await this.plugin.saveSettings();
});
});
new Setting(basicDiv)
.setName(t("settings_autorun"))
.setDesc(t("settings_autorun_desc"))

View File

@ -1,6 +1,7 @@
import PQueue from "p-queue";
import XRegExp from "xregexp";
import type {
CipherMethodType,
ConflictActionType,
EmptyFolderCleanType,
Entity,
@ -22,14 +23,6 @@ import {
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE,
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2,
} from "./metadataOnRemote";
import {
MAGIC_ENCRYPTED_PREFIX_BASE32,
MAGIC_ENCRYPTED_PREFIX_BASE64URL,
decryptBase32ToString,
decryptBase64urlToString,
encryptStringToBase64url,
getSizeFromOrigToEnc,
} from "./encrypt";
import { RemoteClient } from "./remote";
import { Vault } from "obsidian";
@ -39,6 +32,7 @@ import {
clearPrevSyncRecordByVaultAndProfile,
upsertPrevSyncRecordByVaultAndProfile,
} from "./localdb";
import { Cipher } from "./encryptUnified";
export type SyncStatusType =
| "idle"
@ -55,19 +49,17 @@ export type SyncStatusType =
export interface PasswordCheckType {
ok: boolean;
reason:
| "ok"
| "empty_remote"
| "unknown_encryption_method"
| "remote_encrypted_local_no_password"
| "password_matched"
| "password_not_matched"
| "invalid_text_after_decryption"
| "remote_not_encrypted_local_has_password"
| "no_password_both_sides";
| "password_not_matched_or_remote_not_encrypted"
| "likely_no_password_both_sides";
}
export const isPasswordOk = async (
remote: Entity[],
password: string = ""
cipher: Cipher
): Promise<PasswordCheckType> => {
if (remote === undefined || remote.length === 0) {
// remote empty
@ -77,81 +69,40 @@ export const isPasswordOk = async (
};
}
const santyCheckKey = remote[0].keyRaw;
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
// this is encrypted using old base32!
// try to decrypt it using the provided password.
if (password === "") {
if (cipher.isPasswordEmpty()) {
// TODO: no way to distinguish remote rclone encrypted
// if local has no password??
if (Cipher.isLikelyEncryptedName(santyCheckKey)) {
return {
ok: false,
reason: "remote_encrypted_local_no_password",
};
}
try {
const res = await decryptBase32ToString(santyCheckKey, password);
// additional test
// because iOS Safari bypasses decryption with wrong password!
if (isVaildText(res)) {
return {
ok: true,
reason: "password_matched",
};
} else {
return {
ok: false,
reason: "invalid_text_after_decryption",
};
}
} catch (error) {
} else {
return {
ok: false,
reason: "password_not_matched",
};
}
}
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL)) {
// this is encrypted using new base64url!
// try to decrypt it using the provided password.
if (password === "") {
return {
ok: false,
reason: "remote_encrypted_local_no_password",
};
}
try {
const res = await decryptBase64urlToString(santyCheckKey, password);
// additional test
// because iOS Safari bypasses decryption with wrong password!
if (isVaildText(res)) {
return {
ok: true,
reason: "password_matched",
};
} else {
return {
ok: false,
reason: "invalid_text_after_decryption",
};
}
} catch (error) {
return {
ok: false,
reason: "password_not_matched",
ok: true,
reason: "likely_no_password_both_sides",
};
}
} else {
// it is not encrypted!
if (password !== "") {
if (cipher.method === "unknown") {
return {
ok: false,
reason: "remote_not_encrypted_local_has_password",
reason: "unknown_encryption_method",
};
}
try {
await cipher.decryptName(santyCheckKey);
return {
ok: true,
reason: "password_matched",
};
} catch (error) {
return {
ok: false,
reason: "password_not_matched_or_remote_not_encrypted",
};
}
return {
ok: true,
reason: "no_password_both_sides",
};
}
};
@ -231,12 +182,9 @@ const copyEntityAndFixTimeFormat = (
/**
* Inplace, no copy again.
* @param remote
* @param password
* @returns
*/
const decryptRemoteEntityInplace = async (remote: Entity, password: string) => {
if (password == undefined || password === "") {
const decryptRemoteEntityInplace = async (remote: Entity, cipher: Cipher) => {
if (cipher?.isPasswordEmpty()) {
remote.key = remote.keyRaw;
remote.keyEnc = remote.keyRaw;
remote.size = remote.sizeRaw;
@ -244,19 +192,9 @@ const decryptRemoteEntityInplace = async (remote: Entity, password: string) => {
return remote;
}
if (remote.keyRaw.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
remote.keyEnc = remote.keyRaw;
remote.key = await decryptBase32ToString(remote.keyEnc, password);
remote.sizeEnc = remote.sizeRaw;
} else if (remote.keyRaw.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL)) {
remote.keyEnc = remote.keyRaw;
remote.key = await decryptBase64urlToString(remote.keyEnc, password);
remote.sizeEnc = remote.sizeRaw;
} else {
throw Error(
`unexpected key to decrypt: ${JSON.stringify(remote, null, 2)}`
);
}
remote.keyEnc = remote.keyRaw;
remote.key = await cipher.decryptName(remote.keyEnc);
remote.sizeEnc = remote.sizeRaw;
// TODO
// remote.size = getSizeFromEncToOrig(remote.sizeEnc, password);
@ -309,13 +247,10 @@ const ensureMTimeOfRemoteEntityValid = (remote: Entity) => {
/**
* Inplace, no copy again.
* @param local
* @param password
* @returns
*/
const encryptLocalEntityInplace = async (
local: Entity,
password: string,
cipher: Cipher,
remoteKeyEnc: string | undefined
) => {
// console.debug(
@ -333,7 +268,7 @@ const encryptLocalEntityInplace = async (
throw Error(`local ${local.keyRaw} is abnormal without key`);
}
if (password === undefined || password === "") {
if (cipher.isPasswordEmpty()) {
local.sizeEnc = local.sizeRaw; // if no enc, the remote file has the same size
local.keyEnc = local.keyRaw;
return local;
@ -344,7 +279,7 @@ const encryptLocalEntityInplace = async (
// it's not filled yet, we fill it
// local.size is possibly undefined if it's "prevSync" Entity
// but local.key should always have value
local.sizeEnc = getSizeFromOrigToEnc(local.size);
local.sizeEnc = cipher.getSizeFromOrigToEnc(local.size);
}
if (local.keyEnc === undefined || local.keyEnc === "") {
@ -357,10 +292,7 @@ const encryptLocalEntityInplace = async (
local.keyEnc = remoteKeyEnc;
} else {
// we assign a new encrypted key because of no remote
// the old version uses base32
// local.keyEnc = await encryptStringToBase32(local.key, password);
// the new version users base64url
local.keyEnc = await encryptStringToBase64url(local.key, password);
local.keyEnc = await cipher.encryptName(local.key);
}
}
return local;
@ -377,7 +309,7 @@ export const ensembleMixedEnties = async (
configDir: string,
syncUnderscoreItems: boolean,
ignorePaths: string[],
password: string,
cipher: Cipher,
serviceType: SUPPORTED_SERVICES_TYPE
): Promise<SyncPlanType> => {
const finalMappings: SyncPlanType = {};
@ -387,7 +319,7 @@ export const ensembleMixedEnties = async (
const remoteCopied = ensureMTimeOfRemoteEntityValid(
await decryptRemoteEntityInplace(
copyEntityAndFixTimeFormat(remote, serviceType),
password
cipher
)
);
@ -436,14 +368,14 @@ export const ensembleMixedEnties = async (
if (finalMappings.hasOwnProperty(key)) {
const prevSyncCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(prevSync, serviceType),
password,
cipher,
finalMappings[key].remote?.keyEnc
);
finalMappings[key].prevSync = prevSyncCopied;
} else {
const prevSyncCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(prevSync, serviceType),
password,
cipher,
undefined
);
finalMappings[key] = {
@ -474,14 +406,14 @@ export const ensembleMixedEnties = async (
if (finalMappings.hasOwnProperty(key)) {
const localCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(local, serviceType),
password,
cipher,
finalMappings[key].remote?.keyEnc
);
finalMappings[key].local = localCopied;
} else {
const localCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(local, serviceType),
password,
cipher,
undefined
);
finalMappings[key] = {
@ -1017,7 +949,7 @@ const dispatchOperationToActualV3 = async (
db: InternalDBs,
vault: Vault,
localDeleteFunc: any,
password: string
cipher: Cipher
) => {
// console.debug(
// `inside dispatchOperationToActualV3, key=${key}, r=${JSON.stringify(
@ -1045,7 +977,7 @@ const dispatchOperationToActualV3 = async (
if (
client.serviceType === "onedrive" &&
r.local!.size === 0 &&
password === ""
cipher.isPasswordEmpty()
) {
// special treatment for empty files for OneDrive
// TODO: it's ugly, any other way?
@ -1057,10 +989,10 @@ const dispatchOperationToActualV3 = async (
r.key,
vault,
false,
password,
cipher,
r.local!.keyEnc
);
await decryptRemoteEntityInplace(entity, password);
await decryptRemoteEntityInplace(entity, cipher);
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
await upsertPrevSyncRecordByVaultAndProfile(
db,
@ -1081,7 +1013,7 @@ const dispatchOperationToActualV3 = async (
r.key,
vault,
r.remote!.mtimeCli!,
password,
cipher,
r.remote!.keyEnc
);
await upsertPrevSyncRecordByVaultAndProfile(
@ -1092,7 +1024,7 @@ const dispatchOperationToActualV3 = async (
);
} else if (r.decision === "local_is_deleted_thus_also_delete_remote") {
// local is deleted, we need to delete remote now
await client.deleteFromRemote(r.key, password, r.remote!.keyEnc);
await client.deleteFromRemote(r.key, cipher, r.remote!.keyEnc);
await clearPrevSyncRecordByVaultAndProfile(
db,
vaultRandomID,
@ -1119,11 +1051,11 @@ const dispatchOperationToActualV3 = async (
r.key,
vault,
false,
password,
cipher,
r.local!.keyEnc
);
// we need to decrypt the key!!!
await decryptRemoteEntityInplace(entity, password);
await decryptRemoteEntityInplace(entity, cipher);
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
await upsertPrevSyncRecordByVaultAndProfile(
db,
@ -1133,7 +1065,7 @@ const dispatchOperationToActualV3 = async (
);
} else if (r.decision === "folder_to_be_deleted") {
await localDeleteFunc(r.key);
await client.deleteFromRemote(r.key, password, r.remote!.keyEnc);
await client.deleteFromRemote(r.key, cipher, r.remote!.keyEnc);
await clearPrevSyncRecordByVaultAndProfile(
db,
vaultRandomID,
@ -1151,7 +1083,7 @@ export const doActualSync = async (
vaultRandomID: string,
profileID: string,
vault: Vault,
password: string,
cipher: Cipher,
concurrency: number,
localDeleteFunc: any,
protectModifyPercentage: number,
@ -1252,7 +1184,7 @@ export const doActualSync = async (
db,
vault,
localDeleteFunc,
password
cipher
);
console.debug(`finished ${key}`);

View File

@ -10,13 +10,13 @@ import {
encryptStringToBase64url,
getSizeFromEncToOrig,
getSizeFromOrigToEnc,
} from "../src/encrypt";
} from "../src/encryptOpenSSL";
import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc";
chai.use(chaiAsPromised);
const expect = chai.expect;
describe("Encryption tests", () => {
describe("Encryption OpenSSL tests", () => {
beforeEach(function () {
global.window = {
crypto: require("crypto").webcrypto,