Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be2b168c6c | ||
|
|
6b296d6110 | ||
|
|
6757348ac9 | ||
|
|
a7e6bed071 | ||
|
|
8b8c5bdb98 | ||
|
|
5b77b23e9f | ||
|
|
05313f9530 | ||
|
|
ce4e55fcc7 | ||
|
|
b26d6a7ffb | ||
|
|
ea12a14d7f | ||
|
|
3609a78465 | ||
|
|
7ec2db4006 | ||
|
|
5352c7b828 | ||
|
|
2a49bb53b2 |
@@ -38,3 +38,4 @@ jobs:
|
||||
path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsdian-save-remote",
|
||||
"id": "obsidian-save-remote",
|
||||
"name": "Save remote",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.14",
|
||||
"minAppVersion": "0.12.15",
|
||||
"description": "This is yet another plugin allowing users to sync notes between local device and the cloud.",
|
||||
"author": "fyears",
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-save-remote",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.14",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
@@ -15,10 +15,12 @@
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^14.14.37",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"mocha": "^9.1.3",
|
||||
"prettier": "^2.4.1",
|
||||
@@ -33,6 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.37.0",
|
||||
"@aws-sdk/lib-storage": "^3.40.1",
|
||||
"@aws-sdk/signature-v4-crt": "^3.37.0",
|
||||
"acorn": "^8.5.0",
|
||||
"aws-crt": "^1.10.1",
|
||||
@@ -47,6 +50,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"webdav": "^4.7.0",
|
||||
"webdav-fs": "^4.0.0"
|
||||
"webdav-fs": "^4.0.0",
|
||||
"xregexp": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -105,6 +105,7 @@ export default class SaveRemotePlugin extends Plugin {
|
||||
this.settings.password
|
||||
);
|
||||
if (!passwordCheckResult.ok) {
|
||||
new Notice("something goes wrong while checking password");
|
||||
throw Error(passwordCheckResult.reason);
|
||||
}
|
||||
|
||||
@@ -205,13 +206,16 @@ export class PasswordModal extends Modal {
|
||||
contentEl.createEl("p", { text: "Empty means no password." });
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: "Attention 1/3: The password setting itself is stored in PLAIN TEXT LOCALLY (because the plugin needs to use the password to encrypt the files) (and the password would not be sent to remote by this plugin).",
|
||||
text: "Attention 1/4: The password setting itself is stored in PLAIN TEXT LOCALLY (because the plugin needs to use the password to encrypt the files) (and the password would not be sent to remote by this plugin).",
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
text: "Attention 2/3: If you change the password. You should make sure the remote service (s3/webdav/...) IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD. OTHERWISE SOMETHING BAD WOULD HAPPEN!",
|
||||
text: "Attention 2/4: The file contents are encrypted using openssl format. BUT, some metadata such as file sizes and directory structures are not encrypted or can be easily guessed.",
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
text: "Attention 3/3: The longer the password, the better.",
|
||||
text: "Attention 3/4: If you change the password. You should make sure the remote service (s3/webdav/...) IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD. OTHERWISE SOMETHING BAD WOULD HAPPEN!",
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
text: "Attention 4/4: The longer the password, the better.",
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
|
||||
+19
@@ -2,6 +2,7 @@ import { Vault } from "obsidian";
|
||||
import * as path from "path";
|
||||
|
||||
import { base32 } from "rfc4648";
|
||||
import XRegExp from "xregexp";
|
||||
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "ftp";
|
||||
|
||||
@@ -111,3 +112,21 @@ export const hexStringToTypedArray = (hex: string) => {
|
||||
export const base64ToBase32 = (a: string) => {
|
||||
return base32.stringify(Buffer.from(a, "base64"));
|
||||
};
|
||||
|
||||
/**
|
||||
* iOS Safari could decrypt string with invalid password!
|
||||
* So we need an extra way to test the decrypted result.
|
||||
* One simple way is testing the result are "valid", printable chars or not.
|
||||
*
|
||||
* https://stackoverflow.com/questions/6198986
|
||||
* https://www.regular-expressions.info/unicode.html
|
||||
* Manual test shows that emojis like '🍎' match '\\p{Cs}',
|
||||
* so we need to write the regrex in a form that \p{C} minus \p{Cs}
|
||||
* @param a
|
||||
*/
|
||||
export const isVaildText = (a: string) => {
|
||||
// If the regex matches, the string is invalid.
|
||||
return !XRegExp("\\p{Cc}|\\p{Cf}|\\p{Co}|\\p{Cn}|\\p{Zl}|\\p{Zp}", "A").test(
|
||||
a
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Readable } from "stream";
|
||||
|
||||
import { Vault } from "obsidian";
|
||||
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
import {
|
||||
S3Client,
|
||||
ListObjectsV2Command,
|
||||
@@ -98,7 +99,7 @@ export const uploadToRemote = async (
|
||||
ContentType: contentType,
|
||||
})
|
||||
);
|
||||
return await getRemoteMeta(s3Client, s3Config, fileOrFolderPath);
|
||||
return await getRemoteMeta(s3Client, s3Config, uploadFile);
|
||||
} else {
|
||||
// file
|
||||
// we ignore isRecursively parameter here
|
||||
@@ -115,14 +116,24 @@ export const uploadToRemote = async (
|
||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
||||
}
|
||||
const body = arrayBufferToBuffer(remoteContent);
|
||||
await s3Client.send(
|
||||
new PutObjectCommand({
|
||||
|
||||
const upload = new Upload({
|
||||
client: s3Client,
|
||||
queueSize: 20, // concurrency
|
||||
partSize: 5242880, // minimal 5MB by default
|
||||
leavePartsOnError: false,
|
||||
params: {
|
||||
Bucket: s3Config.s3BucketName,
|
||||
Key: uploadFile,
|
||||
Body: body,
|
||||
ContentType: contentType,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
upload.on("httpUploadProgress", (progress) => {
|
||||
// console.log(progress);
|
||||
});
|
||||
await upload.done();
|
||||
|
||||
return await getRemoteMeta(s3Client, s3Config, uploadFile);
|
||||
}
|
||||
};
|
||||
@@ -149,6 +160,9 @@ export const listFromRemote = async (
|
||||
if (rsp.$metadata.httpStatusCode !== 200) {
|
||||
throw Error("some thing bad while listing remote!");
|
||||
}
|
||||
if (rsp.Contents === undefined) {
|
||||
break;
|
||||
}
|
||||
contents.push(...rsp.Contents);
|
||||
|
||||
isTruncated = rsp.IsTruncated;
|
||||
|
||||
+17
-1
@@ -16,7 +16,12 @@ import {
|
||||
deleteFromRemote,
|
||||
downloadFromRemote,
|
||||
} from "./s3";
|
||||
import { mkdirpInVault, SUPPORTED_SERVICES_TYPE, isHiddenPath } from "./misc";
|
||||
import {
|
||||
mkdirpInVault,
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
isHiddenPath,
|
||||
isVaildText,
|
||||
} from "./misc";
|
||||
import {
|
||||
decryptBase32ToString,
|
||||
encryptStringToBase32,
|
||||
@@ -74,6 +79,7 @@ export interface PasswordCheckType {
|
||||
| "remote_encrypted_local_no_password"
|
||||
| "password_matched"
|
||||
| "password_not_matched"
|
||||
| "invalid_text_after_decryption"
|
||||
| "remote_not_encrypted_local_has_password"
|
||||
| "no_password_both_sides";
|
||||
}
|
||||
@@ -101,10 +107,20 @@ export const isPasswordOk = async (
|
||||
}
|
||||
try {
|
||||
const res = await decryptBase32ToString(santyCheckKey, password);
|
||||
|
||||
// additional test
|
||||
// because iOS Safari bypasses decryption with wrong password!
|
||||
if (isVaildText(res)) {
|
||||
return {
|
||||
ok: true,
|
||||
reason: "password_matched",
|
||||
} as PasswordCheckType;
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "invalid_text_after_decryption",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
+13
-1
@@ -1,6 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { expect } from "chai";
|
||||
import * as chai from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
import { base64ToBase32, bufferToArrayBuffer } from "../src/misc";
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
@@ -9,6 +10,9 @@ import {
|
||||
encryptStringToBase32,
|
||||
} from "../src/encrypt";
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe("Encryption tests", () => {
|
||||
beforeEach(function () {
|
||||
global.window = {
|
||||
@@ -22,6 +26,14 @@ describe("Encryption tests", () => {
|
||||
expect(await encryptStringToBase32(k, password)).to.not.equal(k);
|
||||
});
|
||||
|
||||
it("should raise error using different password", async () => {
|
||||
const k = "secret text";
|
||||
const password = "hey";
|
||||
const password2 = "hey2";
|
||||
const enc = await encryptStringToBase32(k, password);
|
||||
await expect(decryptBase32ToString(enc, password2)).to.be.rejected;
|
||||
});
|
||||
|
||||
it("should encrypt and decrypt string and get the same result returned", async () => {
|
||||
const k = "jfkkjkjbce7983ycdeknkkjckooAIUHIDIBIE((*BII)njD/d/dd/d/sjxhux";
|
||||
const password = "hfiuibec989###oiu982bj1`";
|
||||
|
||||
@@ -70,3 +70,22 @@ describe("Misc: get folder levels", () => {
|
||||
expect(misc.getFolderLevels(item3)).to.deep.equal(res3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Misc: vaild file name tests", () => {
|
||||
it("should treat no ascii correctly", async () => {
|
||||
const x = misc.isVaildText("😄🍎 apple 苹果");
|
||||
// console.log(x)
|
||||
expect(x).to.be.true;
|
||||
});
|
||||
|
||||
it("should find not-printable chars correctly", async () => {
|
||||
const x = misc.isVaildText("😄🍎 apple 苹果\u0000");
|
||||
// console.log(x)
|
||||
expect(x).to.be.false;
|
||||
});
|
||||
|
||||
it("should allow spaces/slashes/...", async () => {
|
||||
const x = misc.isVaildText("😄🍎 apple 苹果/-_=/\\*%^&@#$`");
|
||||
expect(x).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"0.0.11": "0.12.15"
|
||||
"0.0.14": "0.12.15"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user