Compare commits

...
4 Commits
Author SHA1 Message Date
fyears dde4327249 bump to beta 0.4.9
Release A New Version / build (16.x) (push) Failing after 44s
2024-03-25 00:22:30 +08:00
fyears e283efc8f7 new encryption 2024-03-25 00:21:56 +08:00
fyears 6825241071 half way of encryption refactor 2024-03-23 16:38:58 +08:00
fyears 98380b6c92 remove loglevel and webdav-fs 2024-03-23 11:37:39 +08:00
31 changed files with 1015 additions and 235 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
- Webdav - Webdav
- [Here](./docs/services_connectable_or_not.md) shows more connectable (or not-connectable) services in details. - [Here](./docs/services_connectable_or_not.md) shows more connectable (or not-connectable) services in details.
- **Obsidian Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker". - **Obsidian Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password. - **[End-to-end encryption](./docs/encryption/README.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
- **Scheduled auto sync supported.** You can also manually trigger the sync using sidebar ribbon, or using the command from the command palette (or even bind the hot key combination to the command then press the hot key combination). - **Scheduled auto sync supported.** You can also manually trigger the sync using sidebar ribbon, or using the command from the command palette (or even bind the hot key combination to the command then press the hot key combination).
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).** - **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
- **Skip Large files** and **skip paths** by custom regex conditions! - **Skip Large files** and **skip paths** by custom regex conditions!
+8
View File
@@ -0,0 +1,8 @@
# Encryption
Currently (March 2024), Remotely Save supports two end to end encryption format:
1. [RClone Crypt](./rclone.md) format, which is the recommend way now.
2. [OpenSSL enc](./openssl.md) format
Here is also the [comparation](./comparation.md).
+23
View File
@@ -0,0 +1,23 @@
# Comparation Between Encryption Formats
## Warning
**ALWAYS BACKUP YOUR VAULT MANUALLY!!!**
If you switch between RClone Crypt format and OpenSSL enc format, you have to delete the cloud vault files **manually** and **fully**, so that the plugin can re-sync (i.e. re-upload) the newly encrypted versions to the cloud.
## The feature table
| | RClone Crypt | OpenSSL enc | comments |
| ------------------------ | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| key generation | scrypt with fixed salt | PBKDF2 with dynamic salt | scrypt is better than PBKDF2 from the algorithm aspect. But RClone uses fixed salt by default. Also the parameters might affect the result. |
| content encryption | XSalsa20Poly1305 on chunks | AES-256-CBC | XSalsa20Poly1305 is way better than AES-256-CBC. And encryption by chunks should require less resources. |
| file name encryption | EME on each segment of the path | AES-256-CBC on the whole path | RClone has the benefit as well as pitfall that the path structure is preserved. Maybe it's more of a design decision difference? No comment on EME and AES-256-CBC. |
| viewing decrypted result | RClone has command that can mount the encrypted vault as if the encryption is transparent. | No convenient way except writing some scripts we are aware of. | RClone is way more convenient. |
## Some notes
1. Anyway, security is a hard problem. The author of Remotely Save doesn't have sufficient knowledge to "judge" which one is the better format. **Use them at your own risk.**
2. Currently the RClone Crypt format is recommended by default in Remotely Save. Just because of the taste from the Remotely Save author, who likes RClone.
3. **Always use a long password.**
4. Both algorithms are selected deliberately to **be compatible with some well-known third-party tools** (instead of some home-made methods) and **have many tests to ensure the correctness**.
@@ -1,10 +1,22 @@
# Encryption # OpenSSL enc format
If a password is set, the files are encrypted before being sent to the cloud. If a password is set, the files are encrypted before being sent to the cloud.
The encryption algorithm is delibrately designed to be aligned with openssl format. ## Warning
1. The encryption algorithm is implemented using web-crypto. **ALWAYS BACKUP YOUR VAULT MANUALLY!!!**
If you switch between RClone Crypt format and OpenSSL enc format, you have to delete the cloud vault files **manually** and **fully**, so that the plugin can re-sync (i.e. re-upload) the newly encrypted versions to the cloud.
## Comparation between encryption formats
See the doc [Comparation](./comparation.md).
## Interoperability with official OpenSSL
This encryption algorithm is delibrately designed to be aligned with openssl format.
1. The encryption algorithm is implemented using web-crypto. Using AES-256-CBC.
2. The file content is encrypted using openssl format. Assuming a file named `sometext.txt`, a password `somepassword`, then the encryption is equivalent to the following command: 2. The file content is encrypted using openssl format. Assuming a file named `sometext.txt`, a password `somepassword`, then the encryption is equivalent to the following command:
```bash ```bash
+46
View File
@@ -0,0 +1,46 @@
# RClone Crypt format
The encryption is compatible with RClone Crypt with **base64** name encryption format.
It's developed based on another js project by the same author of Remotely Save: [`@fyears/rclone-crypt`](https://github.com/fyears/rclone-crypt), which is NOT an official library from RClone, and is NOT affiliated with RClone.
Reasonable tests are also ported from official RClone code, to ensure the compatibility and correctness of the encryption.
## Warning
**ALWAYS BACKUP YOUR VAULT MANUALLY!!!**
If you switch between RClone Crypt format and OpenSSL enc format, you have to delete the cloud vault files **manually** and **fully**, so that the plugin can re-sync (i.e. re-upload) the newly encrypted versions to the cloud.
## Comparation between encryption formats
See the doc [Comparation](./comparation.md).
## Interoperability with official RClone
Please pay attention that the plugin uses **base64** of encrypted file names, while official RClone by default uses **base32** file names. The intention is purely for potentially support longer file names.
You could set up the RClone profile by calling `rclone config`. You need to create two profiles, one for your original connection and the other for RClone Crypt.
Finally, a working config file should like this:
```ini
[webdav1]
type = webdav
url = https://example.com/sharefolder1/subfolder1 # the same as the web address in Remotely Save settings.
vendor = other
user = <some webdav username>
pass = <some webdav password, obfuscated>
[webdav1crypt]
type = crypt
remote = nas1test:vaultname # the same as your "Remote Base Directory" (usually the vault name) in Remotely Save settings
password = <some encryption password, obfuscated>
filename_encoding = base64 # don't forget this!!!
```
You can use the `mount` command to view and see the files in file explorer! On Windows, the command should like this (the remote vault is mounted to drive `X:`):
```bash
rclone mount webdav1crypt: X: --network-mode
```
+1
View File
@@ -11,3 +11,4 @@
- [x] sync direction: incremental pull only - [x] sync direction: incremental pull only
- [x] sync protection: warning based on the threshold - [x] sync protection: warning based on the threshold
- [ ] partial sync: better sync on save - [ ] partial sync: better sync on save
- [x] encrpytion: new encryption method, see [this](../../encryption/)
+2
View File
@@ -1,6 +1,7 @@
import dotenv from "dotenv/config"; import dotenv from "dotenv/config";
import esbuild from "esbuild"; import esbuild from "esbuild";
import process from "process"; import process from "process";
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
// import builtins from 'builtin-modules' // import builtins from 'builtin-modules'
const banner = `/* const banner = `/*
@@ -54,6 +55,7 @@ esbuild
"process.env.NODE_DEBUG": `undefined`, // ugly fix "process.env.NODE_DEBUG": `undefined`, // ugly fix
"process.env.DEBUG": `undefined`, // ugly fix "process.env.DEBUG": `undefined`, // ugly fix
}, },
plugins: [inlineWorkerPlugin()],
}) })
.then((context) => { .then((context) => {
if (process.argv.includes("--watch")) { if (process.argv.includes("--watch")) {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.4.8", "version": "0.4.9",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+5 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.4.8", "version": "0.4.9",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev2": "node esbuild.config.mjs --watch", "dev2": "node esbuild.config.mjs --watch",
@@ -39,6 +39,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"esbuild": "^0.19.9", "esbuild": "^0.19.9",
"esbuild-plugin-inline-worker": "^0.1.1",
"jsdom": "^23.0.1", "jsdom": "^23.0.1",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"npm-check-updates": "^16.14.12", "npm-check-updates": "^16.14.12",
@@ -50,7 +51,8 @@
"typescript": "^5.3.3", "typescript": "^5.3.3",
"webdav-server": "^2.6.2", "webdav-server": "^2.6.2",
"webpack": "^5.89.0", "webpack": "^5.89.0",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4",
"worker-loader": "^3.0.8"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.474.0", "@aws-sdk/client-s3": "^3.474.0",
@@ -58,6 +60,7 @@
"@aws-sdk/signature-v4-crt": "^3.474.0", "@aws-sdk/signature-v4-crt": "^3.474.0",
"@aws-sdk/types": "^3.468.0", "@aws-sdk/types": "^3.468.0",
"@azure/msal-node": "^2.6.0", "@azure/msal-node": "^2.6.0",
"@fyears/rclone-crypt": "^0.0.7",
"@fyears/tsqueue": "^1.0.1", "@fyears/tsqueue": "^1.0.1",
"@microsoft/microsoft-graph-client": "^3.0.7", "@microsoft/microsoft-graph-client": "^3.0.7",
"@smithy/fetch-http-handler": "^2.3.1", "@smithy/fetch-http-handler": "^2.3.1",
@@ -75,7 +78,6 @@
"http-status-codes": "^2.3.0", "http-status-codes": "^2.3.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"loglevel": "^1.8.1",
"lucide": "^0.298.0", "lucide": "^0.298.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"mustache": "^4.2.0", "mustache": "^4.2.0",
@@ -90,7 +92,6 @@
"url": "^0.11.3", "url": "^0.11.3",
"util": "^0.12.5", "util": "^0.12.5",
"webdav": "^5.3.1", "webdav": "^5.3.1",
"webdav-fs": "^4.0.1",
"xregexp": "^5.1.1" "xregexp": "^5.1.1"
} }
} }
+4
View File
@@ -88,6 +88,8 @@ export type SyncDirectionType =
| "incremental_pull_only" | "incremental_pull_only"
| "incremental_push_only"; | "incremental_push_only";
export type CipherMethodType = "rclone-base64" | "openssl-base64" | "unknown";
export interface RemotelySavePluginSettings { export interface RemotelySavePluginSettings {
s3: S3Config; s3: S3Config;
webdav: WebdavConfig; webdav: WebdavConfig;
@@ -119,6 +121,8 @@ export interface RemotelySavePluginSettings {
enableMobileStatusBar?: boolean; enableMobileStatusBar?: boolean;
encryptionMethod?: CipherMethodType;
/** /**
* @deprecated * @deprecated
*/ */
+251
View File
@@ -0,0 +1,251 @@
import {
Cipher as CipherRCloneCryptPack,
encryptedSize,
} from "@fyears/rclone-crypt";
// @ts-ignore
import EncryptWorker from "./encryptRClone.worker";
interface RecvMsg {
status: "ok" | "error";
outputName?: string;
outputContent?: ArrayBuffer;
error?: any;
}
export const getSizeFromOrigToEnc = encryptedSize;
export class CipherRclone {
readonly password: string;
readonly cipher: CipherRCloneCryptPack;
readonly workers: Worker[];
init: boolean;
workerIdx: number;
constructor(password: string, workerNum: number) {
this.password = password;
this.init = false;
this.workerIdx = 0;
// console.debug("begin creating CipherRCloneCryptPack");
this.cipher = new CipherRCloneCryptPack("base64");
// console.debug("finish creating CipherRCloneCryptPack");
// console.debug("begin creating EncryptWorker");
this.workers = [];
for (let i = 0; i < workerNum; ++i) {
this.workers.push(new (EncryptWorker as any)() as Worker);
}
// console.debug("finish creating EncryptWorker");
}
closeResources() {
for (let i = 0; i < this.workers.length; ++i) {
this.workers[i].terminate();
}
}
async prepareByCallingWorker(): Promise<void> {
if (this.init) {
return;
}
// console.debug("begin prepareByCallingWorker");
await this.cipher.key(this.password, "");
// console.debug("finish getting key");
const res: Promise<void>[] = [];
for (let i = 0; i < this.workers.length; ++i) {
res.push(
new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port2.onmessage = (event) => {
// console.debug("main: receiving msg in prepare");
const { status } = event.data as RecvMsg;
if (status === "ok") {
// console.debug("main: receiving init ok in prepare");
this.init = true;
resolve(); // return the class object itself
} else {
reject("error after prepareByCallingWorker");
}
};
channel.port2.onmessageerror = (event) => {
// console.debug("main: receiving error in prepare");
reject(event);
};
// console.debug("main: before postMessage in prepare");
this.workers[i].postMessage(
{
action: "prepare",
dataKeyBuf: this.cipher.dataKey.buffer,
nameKeyBuf: this.cipher.nameKey.buffer,
nameTweakBuf: this.cipher.nameTweak.buffer,
},
[channel.port1 /* buffer no transfered because we need to copy */]
);
})
);
}
await Promise.all(res);
}
async encryptNameByCallingWorker(inputName: string): Promise<string> {
// console.debug("main: start encryptNameByCallingWorker");
await this.prepareByCallingWorker();
// console.debug(
// "main: really start generate promise in encryptNameByCallingWorker"
// );
++this.workerIdx;
const whichWorker = this.workerIdx % this.workers.length;
return await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port2.onmessage = (event) => {
// console.debug("main: receiving msg in encryptNameByCallingWorker");
const { outputName } = event.data as RecvMsg;
if (outputName === undefined) {
reject("unknown outputName after encryptNameByCallingWorker");
} else {
resolve(outputName);
}
};
channel.port2.onmessageerror = (event) => {
// console.debug("main: receiving error in encryptNameByCallingWorker");
reject(event);
};
// console.debug("main: before postMessage in encryptNameByCallingWorker");
this.workers[whichWorker].postMessage(
{
action: "encryptName",
inputName: inputName,
},
[channel.port1]
);
});
}
async decryptNameByCallingWorker(inputName: string): Promise<string> {
await this.prepareByCallingWorker();
++this.workerIdx;
const whichWorker = this.workerIdx % this.workers.length;
return await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port2.onmessage = (event) => {
// console.debug("main: receiving msg in decryptNameByCallingWorker");
const { outputName, status } = event.data as RecvMsg;
if (status === "error") {
reject("error");
} else {
if (outputName === undefined) {
reject("unknown outputName after decryptNameByCallingWorker");
} else {
resolve(outputName);
}
}
};
channel.port2.onmessageerror = (event) => {
// console.debug("main: receiving error in decryptNameByCallingWorker");
reject(event);
channel;
};
// console.debug("main: before postMessage in decryptNameByCallingWorker");
this.workers[whichWorker].postMessage(
{
action: "decryptName",
inputName: inputName,
},
[channel.port1]
);
});
}
async encryptContentByCallingWorker(
input: ArrayBuffer
): Promise<ArrayBuffer> {
await this.prepareByCallingWorker();
++this.workerIdx;
const whichWorker = this.workerIdx % this.workers.length;
return await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port2.onmessage = (event) => {
// console.debug("main: receiving msg in encryptContentByCallingWorker");
const { outputContent } = event.data as RecvMsg;
if (outputContent === undefined) {
reject("unknown outputContent after encryptContentByCallingWorker");
} else {
resolve(outputContent);
}
};
channel.port2.onmessageerror = (event) => {
// console.debug("main: receiving error in encryptContentByCallingWorker");
reject(event);
};
// console.debug(
// "main: before postMessage in encryptContentByCallingWorker"
// );
this.workers[whichWorker].postMessage(
{
action: "encryptContent",
inputContent: input,
},
[channel.port1, input]
);
});
}
async decryptContentByCallingWorker(
input: ArrayBuffer
): Promise<ArrayBuffer> {
await this.prepareByCallingWorker();
++this.workerIdx;
const whichWorker = this.workerIdx % this.workers.length;
return await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port2.onmessage = (event) => {
// console.debug("main: receiving msg in decryptContentByCallingWorker");
const { outputContent, status } = event.data as RecvMsg;
if (status === "error") {
reject("error");
} else {
if (outputContent === undefined) {
reject("unknown outputContent after decryptContentByCallingWorker");
} else {
resolve(outputContent);
}
}
};
channel.port2.onmessageerror = (event) => {
// console.debug(
// "main: receiving onmessageerror in decryptContentByCallingWorker"
// );
reject(event);
};
// console.debug(
// "main: before postMessage in decryptContentByCallingWorker"
// );
this.workers[whichWorker].postMessage(
{
action: "decryptContent",
inputContent: input,
},
[channel.port1, input]
);
});
}
}
+184
View File
@@ -0,0 +1,184 @@
import { nanoid } from "nanoid";
import { Cipher as CipherRCloneCryptPack } from "@fyears/rclone-crypt";
const ctx: WorkerGlobalScope = self as any;
const workerNanoID = nanoid();
const cipher = new CipherRCloneCryptPack("base64");
// console.debug(`worker [${workerNanoID}]: cipher created`);
async function encryptNameStr(input: string) {
const res = await cipher.encryptFileName(input);
return res;
}
async function decryptNameStr(input: string) {
return await cipher.decryptFileName(input);
}
async function encryptContentBuf(input: ArrayBuffer) {
return (await cipher.encryptData(new Uint8Array(input), undefined)).buffer;
}
async function decryptContentBuf(input: ArrayBuffer) {
return (await cipher.decryptData(new Uint8Array(input))).buffer;
}
ctx.addEventListener("message", async (event: any) => {
const port: MessagePort = event.ports[0];
const {
action,
dataKeyBuf,
nameKeyBuf,
nameTweakBuf,
inputName,
inputContent,
} = event.data as {
action:
| "prepare"
| "encryptContent"
| "decryptContent"
| "encryptName"
| "decryptName";
dataKeyBuf?: ArrayBuffer;
nameKeyBuf?: ArrayBuffer;
nameTweakBuf?: ArrayBuffer;
inputName?: string;
inputContent?: ArrayBuffer;
};
// console.debug(`worker [${workerNanoID}]: receiving action=${action}`);
if (action === "prepare") {
// console.debug(`worker [${workerNanoID}]: prepare: start`);
try {
if (
dataKeyBuf === undefined ||
nameKeyBuf === undefined ||
nameTweakBuf === undefined
) {
// console.debug(`worker [${workerNanoID}]: prepare: no buffer??`);
throw Error(
`worker [${workerNanoID}]: prepare: internal keys not transferred to worker properly`
);
}
// console.debug(`worker [${workerNanoID}]: prepare: so we update`);
cipher.updateInternalKey(
new Uint8Array(dataKeyBuf),
new Uint8Array(nameKeyBuf),
new Uint8Array(nameTweakBuf)
);
port.postMessage({
status: "ok",
});
} catch (error) {
console.error(error);
port.postMessage({
status: "error",
error: error,
});
}
} else if (action === "encryptName") {
try {
if (inputName === undefined) {
throw Error(
`worker [${workerNanoID}]: encryptName: internal inputName not transferred to worker properly`
);
}
const outputName = await encryptNameStr(inputName);
// console.debug(
// `worker [${workerNanoID}]: after encryptNameStr, before postMessage`
// );
port.postMessage({
status: "ok",
outputName: outputName,
});
} catch (error) {
console.error(`worker [${workerNanoID}]: encryptName=${inputName}`);
console.error(error);
port.postMessage({
status: "error",
error: error,
});
}
} else if (action === "decryptName") {
try {
if (inputName === undefined) {
throw Error(
`worker [${workerNanoID}]: decryptName: internal inputName not transferred to worker properly`
);
}
const outputName = await decryptNameStr(inputName);
// console.debug(
// `worker [${workerNanoID}]: after decryptNameStr, before postMessage`
// );
port.postMessage({
status: "ok",
outputName: outputName,
});
} catch (error) {
console.error(`worker [${workerNanoID}]: decryptName=${inputName}`);
console.error(error);
port.postMessage({
status: "error",
error: error,
});
}
} else if (action === "encryptContent") {
try {
if (inputContent === undefined) {
throw Error(
`worker [${workerNanoID}]: encryptContent: internal inputContent not transferred to worker properly`
);
}
const outputContent = await encryptContentBuf(inputContent);
// console.debug(
// `worker [${workerNanoID}]: after encryptContentBuf, before postMessage`
// );
port.postMessage(
{
status: "ok",
outputContent: outputContent,
},
[outputContent]
);
} catch (error) {
console.error(error);
port.postMessage({
status: "error",
error: error,
});
}
} else if (action === "decryptContent") {
try {
if (inputContent === undefined) {
throw Error(
`worker [${workerNanoID}]: decryptContent: internal inputContent not transferred to worker properly`
);
}
const outputContent = await decryptContentBuf(inputContent);
// console.debug(
// `worker [${workerNanoID}]: after decryptContentBuf, before postMessage`
// );
port.postMessage(
{
status: "ok",
outputContent: outputContent,
},
[outputContent]
);
} catch (error) {
console.error(error);
port.postMessage({
status: "error",
error: error,
});
}
} else {
port.postMessage({
status: "error",
error: `worker [${workerNanoID}]: unknown action=${action}`,
});
}
});
+148
View File
@@ -0,0 +1,148 @@
import { CipherMethodType } from "./baseTypes";
import * as openssl from "./encryptOpenSSL";
import * as rclone from "./encryptRClone";
import { isVaildText } from "./misc";
export class Cipher {
readonly password: string;
readonly method: CipherMethodType;
cipherRClone?: rclone.CipherRclone;
constructor(password: string, method: CipherMethodType) {
this.password = password ?? "";
this.method = method;
if (method === "rclone-base64") {
this.cipherRClone = new rclone.CipherRclone(password, 5);
}
}
closeResources() {
if (this.method === "rclone-base64" && this.cipherRClone !== undefined) {
this.cipherRClone.closeResources();
}
}
isPasswordEmpty() {
return this.password === "";
}
isFolderAware() {
if (this.method === "openssl-base64") {
return false;
}
if (this.method === "rclone-base64") {
return true;
}
throw Error(`no idea about isFolderAware for method=${this.method}`);
}
async encryptContent(content: ArrayBuffer) {
// console.debug("start encryptContent");
if (this.password === "") {
return content;
}
if (this.method === "openssl-base64") {
return await openssl.encryptArrayBuffer(content, this.password);
} else if (this.method === "rclone-base64") {
return await this.cipherRClone!.encryptContentByCallingWorker(content);
} else {
throw Error(`not supported encrypt method=${this.method}`);
}
}
async decryptContent(content: ArrayBuffer) {
// console.debug("start decryptContent");
if (this.password === "") {
return content;
}
if (this.method === "openssl-base64") {
return await openssl.decryptArrayBuffer(content, this.password);
} else if (this.method === "rclone-base64") {
return await this.cipherRClone!.decryptContentByCallingWorker(content);
} else {
throw Error(`not supported decrypt method=${this.method}`);
}
}
async encryptName(name: string) {
// console.debug("start encryptName");
if (this.password === "") {
return name;
}
if (this.method === "openssl-base64") {
return await openssl.encryptStringToBase64url(name, this.password);
} else if (this.method === "rclone-base64") {
return await this.cipherRClone!.encryptNameByCallingWorker(name);
} else {
throw Error(`not supported encrypt method=${this.method}`);
}
}
async decryptName(name: string) {
// console.debug("start decryptName");
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") {
return await this.cipherRClone!.decryptNameByCallingWorker(name);
} else {
throw Error(`not supported decrypt 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") {
return rclone.getSizeFromOrigToEnc(x);
} 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;
}
}
+9 -2
View File
@@ -64,6 +64,8 @@
"modal_password_attn5": "Attention 5/5: The longer the password, the better.", "modal_password_attn5": "Attention 5/5: The longer the password, the better.",
"modal_password_secondconfirm": "The Second Confirm to change password.", "modal_password_secondconfirm": "The Second Confirm to change password.",
"modal_password_notice": "New password saved!", "modal_password_notice": "New password saved!",
"modal_encryptionmethod_title": "Hold on and PLEASE READ ON...",
"modal_encryptionmethod_shortdesc": "You are changing the encrpytion method but you have set the password before.\nAfter switching the method, you need to <b>manually</b> and <b>fully</b> delete every encrypted vault files in the remote and re-sync (so that re-upload) the newly encrypted files again.",
"modal_remotebasedir_title": "You are changing the remote base directory config", "modal_remotebasedir_title": "You are changing the remote base directory config",
"modal_remotebasedir_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the config would be reset to use the vault folder name (the default config).\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.", "modal_remotebasedir_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the config would be reset to use the vault folder name (the default config).\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.",
"modal_remotebasedir_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.", "modal_remotebasedir_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.",
@@ -109,7 +111,12 @@
"modal_sizesconflict_copynotice": "All the sizes conflicts info have been copied to the clipboard!", "modal_sizesconflict_copynotice": "All the sizes conflicts info have been copied to the clipboard!",
"settings_basic": "Basic Settings", "settings_basic": "Basic Settings",
"settings_password": "Encryption Password", "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 Crypt format is recommended but it doesn't encrypt path structure. OpenSSL enc is the legacy format of this plugin. <b>Both are not affliated with official RClone and OpenSSL product or community.</b> 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. More info in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/encryption'>online doc</a>.",
"settings_encryptionmethod_rclone": "RClone Crypt (recommended)",
"settings_encryptionmethod_openssl": "OpenSSL enc (legacy)",
"settings_autorun": "Schedule For Auto Run", "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_desc": "The plugin tries to schedule the running after every interval. Battery may be impacted.",
"settings_autorun_notset": "(not set)", "settings_autorun_notset": "(not set)",
@@ -303,7 +310,7 @@
"settings_resetcache_button": "Reset", "settings_resetcache_button": "Reset",
"settings_resetcache_notice": "Local internal cache/databases deleted. Please manually reload the plugin.", "settings_resetcache_notice": "Local internal cache/databases deleted. Please manually reload the plugin.",
"syncalgov3_title": "Remotely Save has HUGE updates on the sync algorithm", "syncalgov3_title": "Remotely Save has HUGE updates on the sync algorithm",
"syncalgov3_texts": "Welcome to use Remotely Save!\nFrom this version, a new algorithm has been developed:\n<ul><li>More robust deletion sync,</li><li>minimal conflict handling,</li><li>no meta data uploaded any more,</li><li>deletion / modification protection,</li><li>backup mode</li><li>...</li></ul>\nStay tune for more! A full introduction is in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>doc website</a>.\nIf you agree to use this, please read and check two checkboxes then click the \"Agree\" button, and enjoy the plugin!\nIf you do not agree, please click the \"Do Not Agree\" button, the plugin will unload itself.\nAlso, please consider <a href='https://github.com/remotely-save/remotely-save'>visit the GitHub repo and star ⭐ it</a>! Or even <a href='https://github.com/remotely-save/donation'>buy me a coffee</a>. Your support is very important to me! Thanks!", "syncalgov3_texts": "Welcome to use Remotely Save!\nFrom this version, a new algorithm has been developed:\n<ul><li>More robust deletion sync,</li><li>minimal conflict handling,</li><li>no meta data uploaded any more,</li><li>deletion / modification protection,</li><li>backup mode</li><li>new encryption method</li><li>...</li></ul>\nStay tune for more! A full introduction is in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>doc website</a>.\nIf you agree to use this, please read and check two checkboxes then click the \"Agree\" button, and enjoy the plugin!\nIf you do not agree, please click the \"Do Not Agree\" button, the plugin will unload itself.\nAlso, please consider <a href='https://github.com/remotely-save/remotely-save'>visit the GitHub repo and star ⭐ it</a>! Or even <a href='https://github.com/remotely-save/donation'>buy me a coffee</a>. Your support is very important to me! Thanks!",
"syncalgov3_checkbox_manual_backup": "I will backup my vault manually firstly.", "syncalgov3_checkbox_manual_backup": "I will backup my vault manually firstly.",
"syncalgov3_checkbox_requiremultidevupdate": "I understand I need to update the plugin ACROSS ALL DEVICES to make them work properly.", "syncalgov3_checkbox_requiremultidevupdate": "I understand I need to update the plugin ACROSS ALL DEVICES to make them work properly.",
"syncalgov3_button_agree": "Agree", "syncalgov3_button_agree": "Agree",
+8 -2
View File
@@ -64,6 +64,8 @@
"modal_password_attn5": "注意 5/5:密码越长越好。", "modal_password_attn5": "注意 5/5:密码越长越好。",
"modal_password_secondconfirm": "再次确认保存新密码", "modal_password_secondconfirm": "再次确认保存新密码",
"modal_password_notice": "新密码已保存!", "modal_password_notice": "新密码已保存!",
"modal_encryptionmethod_title": "稍等一下,请阅读下文:",
"modal_encryptionmethod_shortdesc": "您正在修改加密方式,但是您已经设置了密码。\n修改加密方式之后,您需要<b>手动</b>和<b>完全</b>删除在远端的之前加密过的库文件,然后重新同步(从而重新上传)新的加密文件。",
"modal_remotebasedir_title": "您正在修改远端基文件夹设置", "modal_remotebasedir_title": "您正在修改远端基文件夹设置",
"modal_remotebasedir_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置会被重设回库的文件夹名(默认设置)。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。", "modal_remotebasedir_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置会被重设回库的文件夹名(默认设置)。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。",
"modal_remotebasedir_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。", "modal_remotebasedir_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。",
@@ -109,7 +111,11 @@
"modal_sizesconflict_copynotice": "所有的文件大小冲突信息,已被复制到剪贴板!", "modal_sizesconflict_copynotice": "所有的文件大小冲突信息,已被复制到剪贴板!",
"settings_basic": "基本设置", "settings_basic": "基本设置",
"settings_password": "密码", "settings_password": "密码",
"settings_password_desc": "端到端加密的密码。不填写则代表没密码。您需要点击“确认”来修改。注意:密码和其它信息都会在本地保存。", "settings_password_desc": "端到端加密的密码。不填写则代表没密码。您需要点击“确认”来修改。注意:密码和其它信息都会在本地保存。如果您修改了密码,您需要手动删除远端的所有文件,重新同步(从而上传)加密文件。",
"settings_encryptionmethod": "加密方法",
"settings_encryptionmethod_desc": "端到端加密的方法。推荐选用 RClone Crypt 方法,但是它没有加密文件路径结构。OpenSSL enc 是本插件一开始就支持的方式。<b>两种方法都和 RClone、OpenSSL 官方产品和社区无利益相关。</b>如果您修改了加密方法,您需要手动删除远端的所有文件,重新同步(从而上传)加密文件。更多详细说明见<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/encryption'>在线文档</a>。",
"settings_encryptionmethod_rclone": "RClone Crypt(推荐)",
"settings_encryptionmethod_openssl": "OpenSSL enc(旧方法)",
"settings_autorun": "自动运行", "settings_autorun": "自动运行",
"settings_autorun_desc": "每隔一段时间,此插件尝试自动同步。会影响到电池用量。", "settings_autorun_desc": "每隔一段时间,此插件尝试自动同步。会影响到电池用量。",
"settings_autorun_notset": "(不设置)", "settings_autorun_notset": "(不设置)",
@@ -303,7 +309,7 @@
"settings_resetcache_button": "重设", "settings_resetcache_button": "重设",
"settings_resetcache_notice": "本地同步缓存和数据库已被删除。请手动重新载入此插件。", "settings_resetcache_notice": "本地同步缓存和数据库已被删除。请手动重新载入此插件。",
"syncalgov3_title": "Remotely Save 的同步算法有重大更新", "syncalgov3_title": "Remotely Save 的同步算法有重大更新",
"syncalgov3_texts": "欢迎使用 Remotely Save!\n从这个版本开始,插件更新了同步算法:\n<ul><li>更稳健的删除同步</li><li>引入冲突处理</li><li>避免上传元数据</li><li>修改删除保护</li><li>备份模式</li><li>……</li></ul>\n敬请期待更多更新!详细介绍请参阅<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文档网站</a>。\n如果您同意使用新版本,请阅读和勾选两个勾选框,然后点击“同意”按钮,开始使用插件吧!\n如果您不同意,请点击“不同意”按钮,插件将自动停止运行(unload)。\n此外,请考虑<a href='https://github.com/remotely-save/remotely-save'>访问 GitHub 页面然后点赞 ⭐</a>!您的支持对我十分重要!谢谢!", "syncalgov3_texts": "欢迎使用 Remotely Save!\n从这个版本开始,插件更新了同步算法:\n<ul><li>更稳健的删除同步</li><li>引入冲突处理</li><li>避免上传元数据</li><li>修改删除保护</li><li>备份模式</li><li>新的加密方式</li><li>……</li></ul>\n敬请期待更多更新!详细介绍请参阅<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文档网站</a>。\n如果您同意使用新版本,请阅读和勾选两个勾选框,然后点击“同意”按钮,开始使用插件吧!\n如果您不同意,请点击“不同意”按钮,插件将自动停止运行(unload)。\n此外,请考虑<a href='https://github.com/remotely-save/remotely-save'>访问 GitHub 页面然后点赞 ⭐</a>!您的支持对我十分重要!谢谢!",
"syncalgov3_checkbox_manual_backup": "我将会首先手动备份我的库(Vault)。", "syncalgov3_checkbox_manual_backup": "我将会首先手动备份我的库(Vault)。",
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有设备上都更新此插件使之正常运行。", "syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有设备上都更新此插件使之正常运行。",
"syncalgov3_button_agree": "同意", "syncalgov3_button_agree": "同意",
+8 -2
View File
@@ -64,6 +64,8 @@
"modal_password_attn5": "注意 5/5:密碼越長越好。", "modal_password_attn5": "注意 5/5:密碼越長越好。",
"modal_password_secondconfirm": "再次確認儲存新密碼", "modal_password_secondconfirm": "再次確認儲存新密碼",
"modal_password_notice": "新密碼已儲存!", "modal_password_notice": "新密碼已儲存!",
"modal_encryptionmethod_title": "稍等一下,請閱讀下文:",
"modal_encryptionmethod_shortdesc": "您正在修改加密方式,但是您已經設定了密碼。\n修改加密方式之後,您需要<b>手動</b>和<b>完全</b>刪除在遠端的之前加密過的庫檔案,然後重新同步(從而重新上傳)新的加密檔案。",
"modal_remotebasedir_title": "您正在修改遠端基資料夾設定", "modal_remotebasedir_title": "您正在修改遠端基資料夾設定",
"modal_remotebasedir_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定會被重設回庫的資料夾名(預設設定)。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。", "modal_remotebasedir_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定會被重設回庫的資料夾名(預設設定)。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。",
"modal_remotebasedir_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。", "modal_remotebasedir_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。",
@@ -109,7 +111,11 @@
"modal_sizesconflict_copynotice": "所有的檔案大小衝突資訊,已被複制到剪貼簿!", "modal_sizesconflict_copynotice": "所有的檔案大小衝突資訊,已被複制到剪貼簿!",
"settings_basic": "基本設定", "settings_basic": "基本設定",
"settings_password": "密碼", "settings_password": "密碼",
"settings_password_desc": "端到端加密的密碼。不填寫則代表沒密碼。您需要點選“確認”來修改。注意:密碼和其它資訊都會在本地儲存。", "settings_password_desc": "端到端加密的密碼。不填寫則代表沒密碼。您需要點選“確認”來修改。注意:密碼和其它資訊都會在本地儲存。如果您修改了密碼,您需要手動刪除遠端的所有檔案,重新同步(從而上傳)加密檔案。",
"settings_encryptionmethod": "加密方法",
"settings_encryptionmethod_desc": "端到端加密的方法。推薦選用 RClone Crypt 方法,但是它沒有加密檔案路徑結構。OpenSSL enc 是本外掛一開始就支援的方式。<b>兩種方法都和 RClone、OpenSSL 官方產品和社群無利益相關。</b>如果您修改了加密方法,您需要手動刪除遠端的所有檔案,重新同步(從而上傳)加密檔案。更多詳細說明見<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/encryption'>線上文件</a>。",
"settings_encryptionmethod_rclone": "RClone Crypt(推薦)",
"settings_encryptionmethod_openssl": "OpenSSL enc(舊方法)",
"settings_autorun": "自動執行", "settings_autorun": "自動執行",
"settings_autorun_desc": "每隔一段時間,此外掛嘗試自動同步。會影響到電池用量。", "settings_autorun_desc": "每隔一段時間,此外掛嘗試自動同步。會影響到電池用量。",
"settings_autorun_notset": "(不設定)", "settings_autorun_notset": "(不設定)",
@@ -303,7 +309,7 @@
"settings_resetcache_button": "重設", "settings_resetcache_button": "重設",
"settings_resetcache_notice": "本地同步快取和資料庫已被刪除。請手動重新載入此外掛。", "settings_resetcache_notice": "本地同步快取和資料庫已被刪除。請手動重新載入此外掛。",
"syncalgov3_title": "Remotely Save 的同步演算法有重大更新", "syncalgov3_title": "Remotely Save 的同步演算法有重大更新",
"syncalgov3_texts": "歡迎使用 Remotely Save!\n從這個版本開始,外掛更新了同步演算法:\n<ul><li>更穩健的刪除同步</li><li>引入衝突處理</li><li>避免上傳元資料</li><li>修改刪除保護</li><li>備份模式</li><li>……</li></ul>\n敬請期待更多更新!詳細介紹請參閱<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文件網站</a>。\n如果您同意使用新版本,請閱讀和勾選兩個勾選框,然後點選“同意”按鈕,開始使用外掛吧!\n如果您不同意,請點選“不同意”按鈕,外掛將自動停止執行(unload)。\n此外,請考慮<a href='https://github.com/remotely-save/remotely-save'>訪問 GitHub 頁面然後點贊 ⭐</a>!您的支援對我十分重要!謝謝!", "syncalgov3_texts": "歡迎使用 Remotely Save!\n從這個版本開始,外掛更新了同步演算法:\n<ul><li>更穩健的刪除同步</li><li>引入衝突處理</li><li>避免上傳元資料</li><li>修改刪除保護</li><li>備份模式</li><li>新的加密方式</li><li>……</li></ul>\n敬請期待更多更新!詳細介紹請參閱<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文件網站</a>。\n如果您同意使用新版本,請閱讀和勾選兩個勾選框,然後點選“同意”按鈕,開始使用外掛吧!\n如果您不同意,請點選“不同意”按鈕,外掛將自動停止執行(unload)。\n此外,請考慮<a href='https://github.com/remotely-save/remotely-save'>訪問 GitHub 頁面然後點贊 ⭐</a>!您的支援對我十分重要!謝謝!",
"syncalgov3_checkbox_manual_backup": "我將會首先手動備份我的庫(Vault)。", "syncalgov3_checkbox_manual_backup": "我將會首先手動備份我的庫(Vault)。",
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有裝置上都更新此外掛使之正常執行。", "syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有裝置上都更新此外掛使之正常執行。",
"syncalgov3_button_agree": "同意", "syncalgov3_button_agree": "同意",
+27 -5
View File
@@ -67,6 +67,7 @@ import { SyncAlgoV3Modal } from "./syncAlgoV3Notice";
import AggregateError from "aggregate-error"; import AggregateError from "aggregate-error";
import { exportVaultSyncPlansToFiles } from "./debugMode"; import { exportVaultSyncPlansToFiles } from "./debugMode";
import { changeMobileStatusBar, compareVersion } from "./misc"; import { changeMobileStatusBar, compareVersion } from "./misc";
import { Cipher } from "./encryptUnified";
const DEFAULT_SETTINGS: RemotelySavePluginSettings = { const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
s3: DEFAULT_S3_CONFIG, s3: DEFAULT_S3_CONFIG,
@@ -97,6 +98,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
syncDirection: "bidirectional", syncDirection: "bidirectional",
obfuscateSettingFile: true, obfuscateSettingFile: true,
enableMobileStatusBar: false, enableMobileStatusBar: false,
encryptionMethod: "unknown",
}; };
interface OAuth2Info { interface OAuth2Info {
@@ -254,10 +256,12 @@ export default class RemotelySavePlugin extends Plugin {
getNotice(t("syncrun_step3")); getNotice(t("syncrun_step3"));
} }
this.syncStatus = "checking_password"; this.syncStatus = "checking_password";
const passwordCheckResult = await isPasswordOk(
remoteEntityList, const cipher = new Cipher(
this.settings.password this.settings.password,
this.settings.encryptionMethod ?? "unknown"
); );
const passwordCheckResult = await isPasswordOk(remoteEntityList, cipher);
if (!passwordCheckResult.ok) { if (!passwordCheckResult.ok) {
getNotice(t("syncrun_passworderr")); getNotice(t("syncrun_passworderr"));
throw Error(passwordCheckResult.reason); throw Error(passwordCheckResult.reason);
@@ -306,7 +310,7 @@ export default class RemotelySavePlugin extends Plugin {
this.app.vault.configDir, this.app.vault.configDir,
this.settings.syncUnderscoreItems ?? false, this.settings.syncUnderscoreItems ?? false,
this.settings.ignorePaths ?? [], this.settings.ignorePaths ?? [],
this.settings.password, cipher,
this.settings.serviceType this.settings.serviceType
); );
mixedEntityMappings = await getSyncPlanInplace( mixedEntityMappings = await getSyncPlanInplace(
@@ -341,7 +345,7 @@ export default class RemotelySavePlugin extends Plugin {
this.vaultRandomID, this.vaultRandomID,
profileID, profileID,
this.app.vault, this.app.vault,
this.settings.password, cipher,
this.settings.concurrency ?? 5, this.settings.concurrency ?? 5,
(key: string) => self.trash(key), (key: string) => self.trash(key),
this.settings.protectModifyPercentage ?? 50, this.settings.protectModifyPercentage ?? 50,
@@ -385,6 +389,8 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
cipher.closeResources();
if (this.settings.currLogLevel === "info") { if (this.settings.currLogLevel === "info") {
getNotice(t("syncrun_shortstep2")); getNotice(t("syncrun_shortstep2"));
} else { } else {
@@ -911,6 +917,22 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.enableMobileStatusBar = false; 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(); await this.saveSettings();
} }
+6
View File
@@ -118,6 +118,12 @@ export const base64ToArrayBuffer = (b64text: string) => {
return bufferToArrayBuffer(Buffer.from(b64text, "base64")); return bufferToArrayBuffer(Buffer.from(b64text, "base64"));
}; };
export const copyArrayBuffer = (src: ArrayBuffer) => {
var dst = new ArrayBuffer(src.byteLength);
new Uint8Array(dst).set(new Uint8Array(src));
return dst;
};
/** /**
* https://stackoverflow.com/questions/43131242 * https://stackoverflow.com/questions/43131242
* @param hex * @param hex
+17 -16
View File
@@ -12,6 +12,7 @@ import * as dropbox from "./remoteForDropbox";
import * as onedrive from "./remoteForOnedrive"; import * as onedrive from "./remoteForOnedrive";
import * as s3 from "./remoteForS3"; import * as s3 from "./remoteForS3";
import * as webdav from "./remoteForWebdav"; import * as webdav from "./remoteForWebdav";
import { Cipher } from "./encryptUnified";
export class RemoteClient { export class RemoteClient {
readonly serviceType: SUPPORTED_SERVICES_TYPE; readonly serviceType: SUPPORTED_SERVICES_TYPE;
@@ -105,8 +106,8 @@ export class RemoteClient {
uploadToRemote = async ( uploadToRemote = async (
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault | undefined, vault: Vault | undefined,
isRecursively: boolean = false, isRecursively: boolean,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined, foldersCreatedBefore: Set<string> | undefined = undefined,
uploadRaw: boolean = false, uploadRaw: boolean = false,
@@ -119,7 +120,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
isRecursively, isRecursively,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
uploadRaw, uploadRaw,
rawContent rawContent
@@ -130,7 +131,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
isRecursively, isRecursively,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
uploadRaw, uploadRaw,
rawContent rawContent
@@ -141,7 +142,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
isRecursively, isRecursively,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
foldersCreatedBefore, foldersCreatedBefore,
uploadRaw, uploadRaw,
@@ -153,7 +154,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
isRecursively, isRecursively,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
foldersCreatedBefore, foldersCreatedBefore,
uploadRaw, uploadRaw,
@@ -185,7 +186,7 @@ export class RemoteClient {
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault, vault: Vault,
mtime: number, mtime: number,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
skipSaving: boolean = false skipSaving: boolean = false
) => { ) => {
@@ -196,7 +197,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
mtime, mtime,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
skipSaving skipSaving
); );
@@ -206,7 +207,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
mtime, mtime,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
skipSaving skipSaving
); );
@@ -216,7 +217,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
mtime, mtime,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
skipSaving skipSaving
); );
@@ -226,7 +227,7 @@ export class RemoteClient {
fileOrFolderPath, fileOrFolderPath,
vault, vault,
mtime, mtime,
password, cipher,
remoteEncryptedKey, remoteEncryptedKey,
skipSaving skipSaving
); );
@@ -237,7 +238,7 @@ export class RemoteClient {
deleteFromRemote = async ( deleteFromRemote = async (
fileOrFolderPath: string, fileOrFolderPath: string,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "" remoteEncryptedKey: string = ""
) => { ) => {
if (this.serviceType === "s3") { if (this.serviceType === "s3") {
@@ -245,28 +246,28 @@ export class RemoteClient {
s3.getS3Client(this.s3Config!), s3.getS3Client(this.s3Config!),
this.s3Config!, this.s3Config!,
fileOrFolderPath, fileOrFolderPath,
password, cipher,
remoteEncryptedKey remoteEncryptedKey
); );
} else if (this.serviceType === "webdav") { } else if (this.serviceType === "webdav") {
return await webdav.deleteFromRemote( return await webdav.deleteFromRemote(
this.webdavClient!, this.webdavClient!,
fileOrFolderPath, fileOrFolderPath,
password, cipher,
remoteEncryptedKey remoteEncryptedKey
); );
} else if (this.serviceType === "dropbox") { } else if (this.serviceType === "dropbox") {
return await dropbox.deleteFromRemote( return await dropbox.deleteFromRemote(
this.dropboxClient!, this.dropboxClient!,
fileOrFolderPath, fileOrFolderPath,
password, cipher,
remoteEncryptedKey remoteEncryptedKey
); );
} else if (this.serviceType === "onedrive") { } else if (this.serviceType === "onedrive") {
return await onedrive.deleteFromRemote( return await onedrive.deleteFromRemote(
this.onedriveClient!, this.onedriveClient!,
fileOrFolderPath, fileOrFolderPath,
password, cipher,
remoteEncryptedKey remoteEncryptedKey
); );
} else { } else {
+16 -15
View File
@@ -10,7 +10,6 @@ import {
OAUTH2_FORCE_EXPIRE_MILLISECONDS, OAUTH2_FORCE_EXPIRE_MILLISECONDS,
UploadedType, UploadedType,
} from "./baseTypes"; } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { import {
bufferToArrayBuffer, bufferToArrayBuffer,
getFolderLevels, getFolderLevels,
@@ -18,6 +17,7 @@ import {
headersToRecord, headersToRecord,
mkdirpInVault, mkdirpInVault,
} from "./misc"; } from "./misc";
import { Cipher } from "./encryptUnified";
export { Dropbox } from "dropbox"; export { Dropbox } from "dropbox";
@@ -451,8 +451,8 @@ export const uploadToRemote = async (
client: WrappedDropboxClient, client: WrappedDropboxClient,
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault | undefined, vault: Vault | undefined,
isRecursively: boolean = false, isRecursively: boolean,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined, foldersCreatedBefore: Set<string> | undefined = undefined,
uploadRaw: boolean = false, uploadRaw: boolean = false,
@@ -463,7 +463,7 @@ export const uploadToRemote = async (
await client.init(); await client.init();
let uploadFile = fileOrFolderPath; let uploadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") { if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error( throw Error(
`uploadToRemote(dropbox) you have password but remoteEncryptedKey is empty!` `uploadToRemote(dropbox) you have password but remoteEncryptedKey is empty!`
@@ -497,8 +497,8 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`); throw Error(`you specify uploadRaw, but you also provide a folder key!`);
} }
// folder // folder
if (password === "") { if (cipher.isPasswordEmpty() || cipher.isFolderAware()) {
// if not encrypted, mkdir a remote folder // if not encrypted, || encrypted isFolderAware, mkdir a remote folder
if (foldersCreatedBefore?.has(uploadFile)) { if (foldersCreatedBefore?.has(uploadFile)) {
// created, pass // created, pass
} else { } else {
@@ -530,7 +530,8 @@ export const uploadToRemote = async (
mtimeCli: mtime, mtimeCli: mtime,
}; };
} else { } else {
// if encrypted, upload a fake file with the encrypted file name // if encrypted && !isFolderAware(),
// upload a fake file with the encrypted file name
await retryReq( await retryReq(
() => () =>
client.dropbox.filesUpload({ client.dropbox.filesUpload({
@@ -564,8 +565,8 @@ export const uploadToRemote = async (
localContent = await vault.adapter.readBinary(fileOrFolderPath); localContent = await vault.adapter.readBinary(fileOrFolderPath);
} }
let remoteContent = localContent; let remoteContent = localContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await cipher.encryptContent(localContent);
} }
// in dropbox, we don't need to create folders before uploading! cool! // in dropbox, we don't need to create folders before uploading! cool!
// TODO: filesUploadSession for larger files (>=150 MB) // TODO: filesUploadSession for larger files (>=150 MB)
@@ -670,7 +671,7 @@ export const downloadFromRemote = async (
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault, vault: Vault,
mtime: number, mtime: number,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
skipSaving: boolean = false skipSaving: boolean = false
) => { ) => {
@@ -691,14 +692,14 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0); return new ArrayBuffer(0);
} else { } else {
let downloadFile = fileOrFolderPath; let downloadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir); downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile); const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
localContent = await decryptArrayBuffer(remoteContent, password); localContent = await cipher.decryptContent(remoteContent);
} }
if (!skipSaving) { if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, { await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@@ -712,14 +713,14 @@ export const downloadFromRemote = async (
export const deleteFromRemote = async ( export const deleteFromRemote = async (
client: WrappedDropboxClient, client: WrappedDropboxClient,
fileOrFolderPath: string, fileOrFolderPath: string,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "" remoteEncryptedKey: string = ""
) => { ) => {
if (fileOrFolderPath === "/") { if (fileOrFolderPath === "/") {
return; return;
} }
let remoteFileName = fileOrFolderPath; let remoteFileName = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir); remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir);
+21 -20
View File
@@ -17,13 +17,13 @@ import {
Entity, Entity,
UploadedType, UploadedType,
} from "./baseTypes"; } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { import {
bufferToArrayBuffer, bufferToArrayBuffer,
getRandomArrayBuffer, getRandomArrayBuffer,
getRandomIntInclusive, getRandomIntInclusive,
mkdirpInVault, mkdirpInVault,
} from "./misc"; } from "./misc";
import { Cipher } from "./encryptUnified";
const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"]; const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"];
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`; const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
@@ -550,7 +550,7 @@ export class WrappedOnedriveClient {
// 20220401: On Android, requestUrl has issue that text becomes base64. // 20220401: On Android, requestUrl has issue that text becomes base64.
// Use fetch everywhere instead! // Use fetch everywhere instead!
if (false /*VALID_REQURL*/) { if (false /*VALID_REQURL*/) {
await requestUrl({ const res = await requestUrl({
url: theUrl, url: theUrl,
method: "PUT", method: "PUT",
body: payload, body: payload,
@@ -560,8 +560,9 @@ export class WrappedOnedriveClient {
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
}, },
}); });
return res.json as DriveItem | UploadSession;
} else { } else {
await fetch(theUrl, { const res = await fetch(theUrl, {
method: "PUT", method: "PUT",
body: payload, body: payload,
headers: { headers: {
@@ -569,6 +570,7 @@ export class WrappedOnedriveClient {
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
}, },
}); });
return (await res.json()) as DriveItem | UploadSession;
} }
}; };
@@ -694,8 +696,8 @@ export const uploadToRemote = async (
client: WrappedOnedriveClient, client: WrappedOnedriveClient,
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault | undefined, vault: Vault | undefined,
isRecursively: boolean = false, isRecursively: boolean,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined, foldersCreatedBefore: Set<string> | undefined = undefined,
uploadRaw: boolean = false, uploadRaw: boolean = false,
@@ -704,7 +706,7 @@ export const uploadToRemote = async (
await client.init(); await client.init();
let uploadFile = fileOrFolderPath; let uploadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") { if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error( throw Error(
`uploadToRemote(onedrive) you have password but remoteEncryptedKey is empty!` `uploadToRemote(onedrive) you have password but remoteEncryptedKey is empty!`
@@ -734,8 +736,8 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`); throw Error(`you specify uploadRaw, but you also provide a folder key!`);
} }
// folder // folder
if (password === "") { if (cipher.isPasswordEmpty() || cipher.isFolderAware()) {
// if not encrypted, mkdir a remote folder // if not encrypted, || encrypted isFolderAware, mkdir a remote folder
if (foldersCreatedBefore?.has(uploadFile)) { if (foldersCreatedBefore?.has(uploadFile)) {
// created, pass // created, pass
} else { } else {
@@ -763,16 +765,15 @@ export const uploadToRemote = async (
mtimeCli: mtime, mtimeCli: mtime,
}; };
} else { } else {
// if encrypted, // if encrypted && !isFolderAware(),
// upload a fake, random-size file // upload a fake, random-size file
// with the encrypted file name // with the encrypted file name
const byteLengthRandom = getRandomIntInclusive( const byteLengthRandom = getRandomIntInclusive(
1, 1,
65536 /* max allowed */ 65536 /* max allowed */
); );
const arrBufRandom = await encryptArrayBuffer( const arrBufRandom = await cipher.encryptContent(
getRandomArrayBuffer(byteLengthRandom), getRandomArrayBuffer(byteLengthRandom)
password
); );
// an encrypted folder is always small, we just use put here // an encrypted folder is always small, we just use put here
@@ -816,8 +817,8 @@ export const uploadToRemote = async (
localContent = await vault.adapter.readBinary(fileOrFolderPath); localContent = await vault.adapter.readBinary(fileOrFolderPath);
} }
let remoteContent = localContent; let remoteContent = localContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await cipher.encryptContent(localContent);
} }
// no need to create parent folders firstly, cool! // no need to create parent folders firstly, cool!
@@ -930,7 +931,7 @@ export const downloadFromRemote = async (
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault, vault: Vault,
mtime: number, mtime: number,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
skipSaving: boolean = false skipSaving: boolean = false
) => { ) => {
@@ -948,14 +949,14 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0); return new ArrayBuffer(0);
} else { } else {
let downloadFile = fileOrFolderPath; let downloadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir); downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile); const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
localContent = await decryptArrayBuffer(remoteContent, password); localContent = await cipher.decryptContent(remoteContent);
} }
if (!skipSaving) { if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, { await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@@ -969,14 +970,14 @@ export const downloadFromRemote = async (
export const deleteFromRemote = async ( export const deleteFromRemote = async (
client: WrappedOnedriveClient, client: WrappedOnedriveClient,
fileOrFolderPath: string, fileOrFolderPath: string,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "" remoteEncryptedKey: string = ""
) => { ) => {
if (fileOrFolderPath === "/") { if (fileOrFolderPath === "/") {
return; return;
} }
let remoteFileName = fileOrFolderPath; let remoteFileName = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir); remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir);
+34 -20
View File
@@ -33,7 +33,6 @@ import {
UploadedType, UploadedType,
VALID_REQURL, VALID_REQURL,
} from "./baseTypes"; } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { import {
arrayBufferToBuffer, arrayBufferToBuffer,
bufferToArrayBuffer, bufferToArrayBuffer,
@@ -43,6 +42,7 @@ import {
export { S3Client } from "@aws-sdk/client-s3"; export { S3Client } from "@aws-sdk/client-s3";
import PQueue from "p-queue"; import PQueue from "p-queue";
import { Cipher } from "./encryptUnified";
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// special handler using Obsidian requestUrl // special handler using Obsidian requestUrl
@@ -233,7 +233,14 @@ const fromS3ObjectToEntity = (
if (x.Key! in mtimeRecords) { if (x.Key! in mtimeRecords) {
const m2 = mtimeRecords[x.Key!]; const m2 = mtimeRecords[x.Key!];
if (m2 !== 0) { if (m2 !== 0) {
// to be compatible with RClone, we read and store the time in seconds in new version!
if (m2 >= 1000000000000) {
// it's a millsecond, uploaded by old codes..
mtimeCli = m2; mtimeCli = m2;
} else {
// it's a second, uploaded by new codes of the plugin from March 24, 2024
mtimeCli = m2 * 1000;
}
} }
} }
const key = getLocalNoPrefixPath(x.Key!, remotePrefix); const key = getLocalNoPrefixPath(x.Key!, remotePrefix);
@@ -261,7 +268,14 @@ const fromS3HeadObjectToEntity = (
parseFloat(x.Metadata.mtime || x.Metadata.MTime || "0") parseFloat(x.Metadata.mtime || x.Metadata.MTime || "0")
); );
if (m2 !== 0) { if (m2 !== 0) {
// to be compatible with RClone, we read and store the time in seconds in new version!
if (m2 >= 1000000000000) {
// it's a millsecond, uploaded by old codes..
mtimeCli = m2; mtimeCli = m2;
} else {
// it's a second, uploaded by new codes of the plugin from March 24, 2024
mtimeCli = m2 * 1000;
}
} }
} }
// console.debug( // console.debug(
@@ -358,8 +372,8 @@ export const uploadToRemote = async (
s3Config: S3Config, s3Config: S3Config,
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault | undefined, vault: Vault | undefined,
isRecursively: boolean = false, isRecursively: boolean,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
uploadRaw: boolean = false, uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = "", rawContent: string | ArrayBuffer = "",
@@ -368,7 +382,7 @@ export const uploadToRemote = async (
): Promise<UploadedType> => { ): Promise<UploadedType> => {
console.debug(`uploading ${fileOrFolderPath}`); console.debug(`uploading ${fileOrFolderPath}`);
let uploadFile = fileOrFolderPath; let uploadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") { if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error( throw Error(
`uploadToRemote(s3) you have password but remoteEncryptedKey is empty!` `uploadToRemote(s3) you have password but remoteEncryptedKey is empty!`
@@ -402,8 +416,8 @@ export const uploadToRemote = async (
Body: "", Body: "",
ContentType: contentType, ContentType: contentType,
Metadata: { Metadata: {
MTime: `${mtime}`, MTime: `${mtime / 1000.0}`,
CTime: `${ctime}`, CTime: `${ctime / 1000.0}`,
}, },
}) })
); );
@@ -416,7 +430,7 @@ export const uploadToRemote = async (
// file // file
// we ignore isRecursively parameter here // we ignore isRecursively parameter here
let contentType = DEFAULT_CONTENT_TYPE; let contentType = DEFAULT_CONTENT_TYPE;
if (password === "") { if (cipher.isPasswordEmpty()) {
contentType = contentType =
mime.contentType( mime.contentType(
mime.lookup(fileOrFolderPath) || DEFAULT_CONTENT_TYPE mime.lookup(fileOrFolderPath) || DEFAULT_CONTENT_TYPE
@@ -447,8 +461,8 @@ export const uploadToRemote = async (
} }
} }
let remoteContent = localContent; let remoteContent = localContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await cipher.encryptContent(localContent);
} }
const bytesIn5MB = 5242880; const bytesIn5MB = 5242880;
@@ -465,8 +479,8 @@ export const uploadToRemote = async (
Body: body, Body: body,
ContentType: contentType, ContentType: contentType,
Metadata: { Metadata: {
MTime: `${mtime}`, MTime: `${mtime / 1000.0}`,
CTime: `${ctime}`, CTime: `${ctime / 1000.0}`,
}, },
}, },
}); });
@@ -645,8 +659,8 @@ export const downloadFromRemote = async (
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault, vault: Vault,
mtime: number, mtime: number,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string,
skipSaving: boolean = false skipSaving: boolean = false
) => { ) => {
const isFolder = fileOrFolderPath.endsWith("/"); const isFolder = fileOrFolderPath.endsWith("/");
@@ -664,7 +678,7 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0); return new ArrayBuffer(0);
} else { } else {
let downloadFile = fileOrFolderPath; let downloadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getRemoteWithPrefixPath( downloadFile = getRemoteWithPrefixPath(
@@ -677,8 +691,8 @@ export const downloadFromRemote = async (
downloadFile downloadFile
); );
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
localContent = await decryptArrayBuffer(remoteContent, password); localContent = await cipher.decryptContent(remoteContent);
} }
if (!skipSaving) { if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, { await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@@ -700,14 +714,14 @@ export const deleteFromRemote = async (
s3Client: S3Client, s3Client: S3Client,
s3Config: S3Config, s3Config: S3Config,
fileOrFolderPath: string, fileOrFolderPath: string,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "" remoteEncryptedKey: string = ""
) => { ) => {
if (fileOrFolderPath === "/") { if (fileOrFolderPath === "/") {
return; return;
} }
let remoteFileName = fileOrFolderPath; let remoteFileName = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getRemoteWithPrefixPath( remoteFileName = getRemoteWithPrefixPath(
@@ -721,7 +735,7 @@ export const deleteFromRemote = async (
}) })
); );
if (fileOrFolderPath.endsWith("/") && password === "") { if (fileOrFolderPath.endsWith("/") && cipher.isPasswordEmpty()) {
const x = await listFromRemoteRaw(s3Client, s3Config, remoteFileName); const x = await listFromRemoteRaw(s3Client, s3Config, remoteFileName);
x.forEach(async (element) => { x.forEach(async (element) => {
await s3Client.send( await s3Client.send(
@@ -731,7 +745,7 @@ export const deleteFromRemote = async (
}) })
); );
}); });
} else if (fileOrFolderPath.endsWith("/") && password !== "") { } else if (fileOrFolderPath.endsWith("/") && !cipher.isPasswordEmpty()) {
// TODO // TODO
} else { } else {
// pass // pass
+23 -17
View File
@@ -4,10 +4,11 @@ import { Platform, Vault, requestUrl } from "obsidian";
import { Queue } from "@fyears/tsqueue"; import { Queue } from "@fyears/tsqueue";
import chunk from "lodash/chunk"; import chunk from "lodash/chunk";
import flatten from "lodash/flatten"; import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";
import { getReasonPhrase } from "http-status-codes"; import { getReasonPhrase } from "http-status-codes";
import { Entity, UploadedType, VALID_REQURL, WebdavConfig } from "./baseTypes"; import { Entity, UploadedType, VALID_REQURL, WebdavConfig } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc"; import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
import { Cipher } from "./encryptUnified";
import type { import type {
FileStat, FileStat,
@@ -50,10 +51,15 @@ if (VALID_REQURL) {
const reqContentType = const reqContentType =
transformedHeaders["accept"] ?? transformedHeaders["content-type"]; transformedHeaders["accept"] ?? transformedHeaders["content-type"];
const retractedHeaders = { ...transformedHeaders };
if (retractedHeaders.hasOwnProperty("authorization")) {
retractedHeaders["authorization"] = "<retracted>";
}
console.debug(`before request:`); console.debug(`before request:`);
console.debug(`url: ${options.url}`); console.debug(`url: ${options.url}`);
console.debug(`method: ${options.method}`); console.debug(`method: ${options.method}`);
console.debug(`headers: ${JSON.stringify(transformedHeaders, null, 2)}`); console.debug(`headers: ${JSON.stringify(retractedHeaders, null, 2)}`);
console.debug(`reqContentType: ${reqContentType}`); console.debug(`reqContentType: ${reqContentType}`);
let r = await requestUrl({ let r = await requestUrl({
@@ -139,7 +145,6 @@ if (VALID_REQURL) {
// @ts-ignore // @ts-ignore
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js"; import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
import cloneDeep from "lodash/cloneDeep";
export type { WebDAVClient } from "webdav"; export type { WebDAVClient } from "webdav";
export const DEFAULT_WEBDAV_CONFIG = { export const DEFAULT_WEBDAV_CONFIG = {
@@ -316,15 +321,15 @@ export const uploadToRemote = async (
client: WrappedWebdavClient, client: WrappedWebdavClient,
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault | undefined, vault: Vault | undefined,
isRecursively: boolean = false, isRecursively: boolean,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
uploadRaw: boolean = false, uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = "" rawContent: string | ArrayBuffer = ""
): Promise<UploadedType> => { ): Promise<UploadedType> => {
await client.init(); await client.init();
let uploadFile = fileOrFolderPath; let uploadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") { if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
throw Error( throw Error(
`uploadToRemote(webdav) you have password but remoteEncryptedKey is empty!` `uploadToRemote(webdav) you have password but remoteEncryptedKey is empty!`
@@ -343,8 +348,8 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`); throw Error(`you specify uploadRaw, but you also provide a folder key!`);
} }
// folder // folder
if (password === "") { if (cipher.isPasswordEmpty() || cipher.isFolderAware()) {
// if not encrypted, mkdir a remote folder // if not encrypted, || encrypted isFolderAware, mkdir a remote folder
await client.client.createDirectory(uploadFile, { await client.client.createDirectory(uploadFile, {
recursive: true, recursive: true,
}); });
@@ -353,7 +358,8 @@ export const uploadToRemote = async (
entity: res, entity: res,
}; };
} else { } else {
// if encrypted, upload a fake file with the encrypted file name // if encrypted && !isFolderAware(),
// upload a fake file with the encrypted file name
await client.client.putFileContents(uploadFile, "", { await client.client.putFileContents(uploadFile, "", {
overwrite: true, overwrite: true,
onUploadProgress: (progress: any) => { onUploadProgress: (progress: any) => {
@@ -386,8 +392,8 @@ export const uploadToRemote = async (
mtimeCli = (await vault.adapter.stat(fileOrFolderPath))?.mtime; mtimeCli = (await vault.adapter.stat(fileOrFolderPath))?.mtime;
} }
let remoteContent = localContent; let remoteContent = localContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await cipher.encryptContent(localContent);
} }
// updated 20220326: the algorithm guarantee this // updated 20220326: the algorithm guarantee this
// // we need to create folders before uploading // // we need to create folders before uploading
@@ -491,7 +497,7 @@ export const downloadFromRemote = async (
fileOrFolderPath: string, fileOrFolderPath: string,
vault: Vault, vault: Vault,
mtime: number, mtime: number,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "", remoteEncryptedKey: string = "",
skipSaving: boolean = false skipSaving: boolean = false
) => { ) => {
@@ -512,15 +518,15 @@ export const downloadFromRemote = async (
return new ArrayBuffer(0); return new ArrayBuffer(0);
} else { } else {
let downloadFile = fileOrFolderPath; let downloadFile = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir); downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir);
// console.info(`downloadFile=${downloadFile}`); // console.info(`downloadFile=${downloadFile}`);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile); const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
localContent = await decryptArrayBuffer(remoteContent, password); localContent = await cipher.decryptContent(remoteContent);
} }
if (!skipSaving) { if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, { await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
@@ -534,14 +540,14 @@ export const downloadFromRemote = async (
export const deleteFromRemote = async ( export const deleteFromRemote = async (
client: WrappedWebdavClient, client: WrappedWebdavClient,
fileOrFolderPath: string, fileOrFolderPath: string,
password: string = "", cipher: Cipher,
remoteEncryptedKey: string = "" remoteEncryptedKey: string = ""
) => { ) => {
if (fileOrFolderPath === "/") { if (fileOrFolderPath === "/") {
return; return;
} }
let remoteFileName = fileOrFolderPath; let remoteFileName = fileOrFolderPath;
if (password !== "") { if (!cipher.isPasswordEmpty()) {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir); remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir);
+81
View File
@@ -22,6 +22,7 @@ import {
VALID_REQURL, VALID_REQURL,
WebdavAuthType, WebdavAuthType,
WebdavDepthType, WebdavDepthType,
CipherMethodType,
} from "./baseTypes"; } from "./baseTypes";
import { exportVaultSyncPlansToFiles } from "./debugMode"; import { exportVaultSyncPlansToFiles } from "./debugMode";
import { exportQrCodeUri } from "./importExport"; import { exportQrCodeUri } from "./importExport";
@@ -122,6 +123,60 @@ class PasswordModal extends Modal {
} }
} }
class EncryptionMethodModal extends Modal {
plugin: RemotelySavePlugin;
newEncryptionMethod: CipherMethodType;
constructor(
app: App,
plugin: RemotelySavePlugin,
newEncryptionMethod: CipherMethodType
) {
super(app);
this.plugin = plugin;
this.newEncryptionMethod = newEncryptionMethod;
}
onOpen() {
let { contentEl } = this;
const t = (x: TransItemType, vars?: any) => {
return this.plugin.i18n.t(x, vars);
};
// contentEl.setText("Add Or change password.");
contentEl.createEl("h2", { text: t("modal_encryptionmethod_title") });
t("modal_encryptionmethod_shortdesc")
.split("\n")
.forEach((val, idx) => {
contentEl.createEl("p", {
text: stringToFragment(val),
});
});
new Setting(contentEl)
.addButton((button) => {
button.setButtonText(t("confirm"));
button.onClick(async () => {
this.plugin.settings.encryptionMethod = this.newEncryptionMethod;
await this.plugin.saveSettings();
this.close();
});
button.setClass("encryptionmethod-second-confirm");
})
.addButton((button) => {
button.setButtonText(t("goback"));
button.onClick(() => {
this.close();
});
});
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
class ChangeRemoteBaseDirModal extends Modal { class ChangeRemoteBaseDirModal extends Modal {
readonly plugin: RemotelySavePlugin; readonly plugin: RemotelySavePlugin;
readonly newRemoteBaseDir: string; readonly newRemoteBaseDir: string;
@@ -1634,6 +1689,32 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
new Setting(basicDiv)
.setName(t("settings_encryptionmethod"))
.setDesc(stringToFragment(t("settings_encryptionmethod_desc")))
.addDropdown((dropdown) => {
dropdown.addOption(
"rclone-base64",
t("settings_encryptionmethod_rclone")
);
dropdown.addOption(
"openssl-base64",
t("settings_encryptionmethod_openssl")
);
dropdown.onChange(async (val: string) => {
if (this.plugin.settings.password === "") {
this.plugin.settings.encryptionMethod = val as CipherMethodType;
await this.plugin.saveSettings();
} else {
new EncryptionMethodModal(
this.app,
this.plugin,
val as CipherMethodType
).open();
}
});
});
new Setting(basicDiv) new Setting(basicDiv)
.setName(t("settings_autorun")) .setName(t("settings_autorun"))
.setDesc(t("settings_autorun_desc")) .setDesc(t("settings_autorun_desc"))
+49 -117
View File
@@ -1,6 +1,7 @@
import PQueue from "p-queue"; import PQueue from "p-queue";
import XRegExp from "xregexp"; import XRegExp from "xregexp";
import type { import type {
CipherMethodType,
ConflictActionType, ConflictActionType,
EmptyFolderCleanType, EmptyFolderCleanType,
Entity, Entity,
@@ -22,14 +23,6 @@ import {
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE, DEFAULT_FILE_NAME_FOR_METADATAONREMOTE,
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2, DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2,
} from "./metadataOnRemote"; } from "./metadataOnRemote";
import {
MAGIC_ENCRYPTED_PREFIX_BASE32,
MAGIC_ENCRYPTED_PREFIX_BASE64URL,
decryptBase32ToString,
decryptBase64urlToString,
encryptStringToBase64url,
getSizeFromOrigToEnc,
} from "./encrypt";
import { RemoteClient } from "./remote"; import { RemoteClient } from "./remote";
import { Vault } from "obsidian"; import { Vault } from "obsidian";
@@ -39,6 +32,7 @@ import {
clearPrevSyncRecordByVaultAndProfile, clearPrevSyncRecordByVaultAndProfile,
upsertPrevSyncRecordByVaultAndProfile, upsertPrevSyncRecordByVaultAndProfile,
} from "./localdb"; } from "./localdb";
import { Cipher } from "./encryptUnified";
export type SyncStatusType = export type SyncStatusType =
| "idle" | "idle"
@@ -55,19 +49,17 @@ export type SyncStatusType =
export interface PasswordCheckType { export interface PasswordCheckType {
ok: boolean; ok: boolean;
reason: reason:
| "ok"
| "empty_remote" | "empty_remote"
| "unknown_encryption_method"
| "remote_encrypted_local_no_password" | "remote_encrypted_local_no_password"
| "password_matched" | "password_matched"
| "password_not_matched" | "password_not_matched_or_remote_not_encrypted"
| "invalid_text_after_decryption" | "likely_no_password_both_sides";
| "remote_not_encrypted_local_has_password"
| "no_password_both_sides";
} }
export const isPasswordOk = async ( export const isPasswordOk = async (
remote: Entity[], remote: Entity[],
password: string = "" cipher: Cipher
): Promise<PasswordCheckType> => { ): Promise<PasswordCheckType> => {
if (remote === undefined || remote.length === 0) { if (remote === undefined || remote.length === 0) {
// remote empty // remote empty
@@ -77,82 +69,41 @@ export const isPasswordOk = async (
}; };
} }
const santyCheckKey = remote[0].keyRaw; const santyCheckKey = remote[0].keyRaw;
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
// this is encrypted using old base32! if (cipher.isPasswordEmpty()) {
// try to decrypt it using the provided password. // TODO: no way to distinguish remote rclone encrypted
if (password === "") { // if local has no password??
if (Cipher.isLikelyEncryptedName(santyCheckKey)) {
return { return {
ok: false, ok: false,
reason: "remote_encrypted_local_no_password", reason: "remote_encrypted_local_no_password",
}; };
} else {
return {
ok: true,
reason: "likely_no_password_both_sides",
};
}
} else {
if (cipher.method === "unknown") {
return {
ok: false,
reason: "unknown_encryption_method",
};
} }
try { try {
const res = await decryptBase32ToString(santyCheckKey, password); await cipher.decryptName(santyCheckKey);
// additional test
// because iOS Safari bypasses decryption with wrong password!
if (isVaildText(res)) {
return { return {
ok: true, ok: true,
reason: "password_matched", reason: "password_matched",
}; };
} else {
return {
ok: false,
reason: "invalid_text_after_decryption",
};
}
} catch (error) { } catch (error) {
return { return {
ok: false, ok: false,
reason: "password_not_matched", reason: "password_not_matched_or_remote_not_encrypted",
}; };
} }
} }
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",
};
}
} else {
// it is not encrypted!
if (password !== "") {
return {
ok: false,
reason: "remote_not_encrypted_local_has_password",
};
}
return {
ok: true,
reason: "no_password_both_sides",
};
}
}; };
const isSkipItemByName = ( const isSkipItemByName = (
@@ -231,12 +182,9 @@ const copyEntityAndFixTimeFormat = (
/** /**
* Inplace, no copy again. * Inplace, no copy again.
* @param remote
* @param password
* @returns
*/ */
const decryptRemoteEntityInplace = async (remote: Entity, password: string) => { const decryptRemoteEntityInplace = async (remote: Entity, cipher: Cipher) => {
if (password == undefined || password === "") { if (cipher?.isPasswordEmpty()) {
remote.key = remote.keyRaw; remote.key = remote.keyRaw;
remote.keyEnc = remote.keyRaw; remote.keyEnc = remote.keyRaw;
remote.size = remote.sizeRaw; remote.size = remote.sizeRaw;
@@ -244,19 +192,9 @@ const decryptRemoteEntityInplace = async (remote: Entity, password: string) => {
return remote; return remote;
} }
if (remote.keyRaw.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
remote.keyEnc = remote.keyRaw; remote.keyEnc = remote.keyRaw;
remote.key = await decryptBase32ToString(remote.keyEnc, password); remote.key = await cipher.decryptName(remote.keyEnc);
remote.sizeEnc = remote.sizeRaw; 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)}`
);
}
// TODO // TODO
// remote.size = getSizeFromEncToOrig(remote.sizeEnc, password); // remote.size = getSizeFromEncToOrig(remote.sizeEnc, password);
@@ -309,13 +247,10 @@ const ensureMTimeOfRemoteEntityValid = (remote: Entity) => {
/** /**
* Inplace, no copy again. * Inplace, no copy again.
* @param local
* @param password
* @returns
*/ */
const encryptLocalEntityInplace = async ( const encryptLocalEntityInplace = async (
local: Entity, local: Entity,
password: string, cipher: Cipher,
remoteKeyEnc: string | undefined remoteKeyEnc: string | undefined
) => { ) => {
// console.debug( // console.debug(
@@ -333,7 +268,7 @@ const encryptLocalEntityInplace = async (
throw Error(`local ${local.keyRaw} is abnormal without key`); 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.sizeEnc = local.sizeRaw; // if no enc, the remote file has the same size
local.keyEnc = local.keyRaw; local.keyEnc = local.keyRaw;
return local; return local;
@@ -344,7 +279,7 @@ const encryptLocalEntityInplace = async (
// it's not filled yet, we fill it // it's not filled yet, we fill it
// local.size is possibly undefined if it's "prevSync" Entity // local.size is possibly undefined if it's "prevSync" Entity
// but local.key should always have value // but local.key should always have value
local.sizeEnc = getSizeFromOrigToEnc(local.size); local.sizeEnc = cipher.getSizeFromOrigToEnc(local.size);
} }
if (local.keyEnc === undefined || local.keyEnc === "") { if (local.keyEnc === undefined || local.keyEnc === "") {
@@ -357,10 +292,7 @@ const encryptLocalEntityInplace = async (
local.keyEnc = remoteKeyEnc; local.keyEnc = remoteKeyEnc;
} else { } else {
// we assign a new encrypted key because of no remote // we assign a new encrypted key because of no remote
// the old version uses base32 local.keyEnc = await cipher.encryptName(local.key);
// local.keyEnc = await encryptStringToBase32(local.key, password);
// the new version users base64url
local.keyEnc = await encryptStringToBase64url(local.key, password);
} }
} }
return local; return local;
@@ -377,7 +309,7 @@ export const ensembleMixedEnties = async (
configDir: string, configDir: string,
syncUnderscoreItems: boolean, syncUnderscoreItems: boolean,
ignorePaths: string[], ignorePaths: string[],
password: string, cipher: Cipher,
serviceType: SUPPORTED_SERVICES_TYPE serviceType: SUPPORTED_SERVICES_TYPE
): Promise<SyncPlanType> => { ): Promise<SyncPlanType> => {
const finalMappings: SyncPlanType = {}; const finalMappings: SyncPlanType = {};
@@ -387,7 +319,7 @@ export const ensembleMixedEnties = async (
const remoteCopied = ensureMTimeOfRemoteEntityValid( const remoteCopied = ensureMTimeOfRemoteEntityValid(
await decryptRemoteEntityInplace( await decryptRemoteEntityInplace(
copyEntityAndFixTimeFormat(remote, serviceType), copyEntityAndFixTimeFormat(remote, serviceType),
password cipher
) )
); );
@@ -436,14 +368,14 @@ export const ensembleMixedEnties = async (
if (finalMappings.hasOwnProperty(key)) { if (finalMappings.hasOwnProperty(key)) {
const prevSyncCopied = await encryptLocalEntityInplace( const prevSyncCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(prevSync, serviceType), copyEntityAndFixTimeFormat(prevSync, serviceType),
password, cipher,
finalMappings[key].remote?.keyEnc finalMappings[key].remote?.keyEnc
); );
finalMappings[key].prevSync = prevSyncCopied; finalMappings[key].prevSync = prevSyncCopied;
} else { } else {
const prevSyncCopied = await encryptLocalEntityInplace( const prevSyncCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(prevSync, serviceType), copyEntityAndFixTimeFormat(prevSync, serviceType),
password, cipher,
undefined undefined
); );
finalMappings[key] = { finalMappings[key] = {
@@ -474,14 +406,14 @@ export const ensembleMixedEnties = async (
if (finalMappings.hasOwnProperty(key)) { if (finalMappings.hasOwnProperty(key)) {
const localCopied = await encryptLocalEntityInplace( const localCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(local, serviceType), copyEntityAndFixTimeFormat(local, serviceType),
password, cipher,
finalMappings[key].remote?.keyEnc finalMappings[key].remote?.keyEnc
); );
finalMappings[key].local = localCopied; finalMappings[key].local = localCopied;
} else { } else {
const localCopied = await encryptLocalEntityInplace( const localCopied = await encryptLocalEntityInplace(
copyEntityAndFixTimeFormat(local, serviceType), copyEntityAndFixTimeFormat(local, serviceType),
password, cipher,
undefined undefined
); );
finalMappings[key] = { finalMappings[key] = {
@@ -1017,7 +949,7 @@ const dispatchOperationToActualV3 = async (
db: InternalDBs, db: InternalDBs,
vault: Vault, vault: Vault,
localDeleteFunc: any, localDeleteFunc: any,
password: string cipher: Cipher
) => { ) => {
// console.debug( // console.debug(
// `inside dispatchOperationToActualV3, key=${key}, r=${JSON.stringify( // `inside dispatchOperationToActualV3, key=${key}, r=${JSON.stringify(
@@ -1045,7 +977,7 @@ const dispatchOperationToActualV3 = async (
if ( if (
client.serviceType === "onedrive" && client.serviceType === "onedrive" &&
r.local!.size === 0 && r.local!.size === 0 &&
password === "" cipher.isPasswordEmpty()
) { ) {
// special treatment for empty files for OneDrive // special treatment for empty files for OneDrive
// TODO: it's ugly, any other way? // TODO: it's ugly, any other way?
@@ -1057,10 +989,10 @@ const dispatchOperationToActualV3 = async (
r.key, r.key,
vault, vault,
false, false,
password, cipher,
r.local!.keyEnc r.local!.keyEnc
); );
await decryptRemoteEntityInplace(entity, password); await decryptRemoteEntityInplace(entity, cipher);
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli); await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
await upsertPrevSyncRecordByVaultAndProfile( await upsertPrevSyncRecordByVaultAndProfile(
db, db,
@@ -1081,7 +1013,7 @@ const dispatchOperationToActualV3 = async (
r.key, r.key,
vault, vault,
r.remote!.mtimeCli!, r.remote!.mtimeCli!,
password, cipher,
r.remote!.keyEnc r.remote!.keyEnc
); );
await upsertPrevSyncRecordByVaultAndProfile( await upsertPrevSyncRecordByVaultAndProfile(
@@ -1092,7 +1024,7 @@ const dispatchOperationToActualV3 = async (
); );
} else if (r.decision === "local_is_deleted_thus_also_delete_remote") { } else if (r.decision === "local_is_deleted_thus_also_delete_remote") {
// local is deleted, we need to delete remote now // 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( await clearPrevSyncRecordByVaultAndProfile(
db, db,
vaultRandomID, vaultRandomID,
@@ -1119,11 +1051,11 @@ const dispatchOperationToActualV3 = async (
r.key, r.key,
vault, vault,
false, false,
password, cipher,
r.local!.keyEnc r.local!.keyEnc
); );
// we need to decrypt the key!!! // we need to decrypt the key!!!
await decryptRemoteEntityInplace(entity, password); await decryptRemoteEntityInplace(entity, cipher);
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli); await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
await upsertPrevSyncRecordByVaultAndProfile( await upsertPrevSyncRecordByVaultAndProfile(
db, db,
@@ -1133,7 +1065,7 @@ const dispatchOperationToActualV3 = async (
); );
} else if (r.decision === "folder_to_be_deleted") { } else if (r.decision === "folder_to_be_deleted") {
await localDeleteFunc(r.key); await localDeleteFunc(r.key);
await client.deleteFromRemote(r.key, password, r.remote!.keyEnc); await client.deleteFromRemote(r.key, cipher, r.remote!.keyEnc);
await clearPrevSyncRecordByVaultAndProfile( await clearPrevSyncRecordByVaultAndProfile(
db, db,
vaultRandomID, vaultRandomID,
@@ -1151,7 +1083,7 @@ export const doActualSync = async (
vaultRandomID: string, vaultRandomID: string,
profileID: string, profileID: string,
vault: Vault, vault: Vault,
password: string, cipher: Cipher,
concurrency: number, concurrency: number,
localDeleteFunc: any, localDeleteFunc: any,
protectModifyPercentage: number, protectModifyPercentage: number,
@@ -1252,7 +1184,7 @@ export const doActualSync = async (
db, db,
vault, vault,
localDeleteFunc, localDeleteFunc,
password cipher
); );
console.debug(`finished ${key}`); console.debug(`finished ${key}`);
+6
View File
@@ -0,0 +1,6 @@
declare module "*.worker.ts" {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}
+4
View File
@@ -8,6 +8,10 @@
font-weight: bold; font-weight: bold;
} }
.encryptionmethod-second-confirm {
font-weight: bold;
}
.settings-auth-related { .settings-auth-related {
border-top: 1px solid var(--background-modifier-border); border-top: 1px solid var(--background-modifier-border);
padding-top: 18px; padding-top: 18px;
@@ -10,13 +10,13 @@ import {
encryptStringToBase64url, encryptStringToBase64url,
getSizeFromEncToOrig, getSizeFromEncToOrig,
getSizeFromOrigToEnc, getSizeFromOrigToEnc,
} from "../src/encrypt"; } from "../src/encryptOpenSSL";
import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc"; import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc";
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
const expect = chai.expect; const expect = chai.expect;
describe("Encryption tests", () => { describe("Encryption OpenSSL tests", () => {
beforeEach(function () { beforeEach(function () {
global.window = { global.window = {
crypto: require("crypto").webcrypto, crypto: require("crypto").webcrypto,
+1 -1
View File
@@ -14,7 +14,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"lib": ["dom", "es5", "scripthost", "es2015"] "lib": ["dom", "es5", "scripthost", "es2015", "webworker"]
}, },
"include": ["**/*.ts"] "include": ["**/*.ts"]
} }
+7
View File
@@ -32,6 +32,13 @@ module.exports = {
], ],
module: { module: {
rules: [ rules: [
{
test: /\.worker\.ts$/,
loader: "worker-loader",
options: {
inline: "no-fallback",
},
},
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: "ts-loader", use: "ts-loader",