Compare commits

..
14 Commits
Author SHA1 Message Date
fyears be2b168c6c artifact has styles 2021-11-10 01:06:00 +08:00
fyears 6b296d6110 bump to 0.0.14 2021-11-10 01:04:09 +08:00
fyears 6757348ac9 use multipart-upload 2021-11-10 01:03:33 +08:00
fyears a7e6bed071 fix typo 2021-11-10 00:30:30 +08:00
fyears 8b8c5bdb98 fix letter 2021-11-10 00:30:11 +08:00
fyears 5b77b23e9f xregexp should be dep not dev-dep 2021-11-10 00:16:24 +08:00
fyears 05313f9530 0.0.13 2021-11-09 10:01:18 +08:00
fyears ce4e55fcc7 more tests 2021-11-09 10:00:44 +08:00
fyears b26d6a7ffb more clear notice 2021-11-09 10:00:14 +08:00
fyears ea12a14d7f 0.0.12 2021-11-08 10:00:40 +08:00
fyears 3609a78465 test enc 2021-11-08 10:00:06 +08:00
fyears 7ec2db4006 correctly fetch remote data 2021-11-08 09:38:39 +08:00
fyears 5352c7b828 check undefined 2021-11-08 09:37:51 +08:00
fyears 2a49bb53b2 more attention 2021-11-08 00:57:18 +08:00
10 changed files with 108 additions and 19 deletions
+1
View File
@@ -38,3 +38,4 @@ jobs:
path: | path: |
main.js main.js
manifest.json manifest.json
styles.css
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"id": "obsdian-save-remote", "id": "obsidian-save-remote",
"name": "Save remote", "name": "Save remote",
"version": "0.0.11", "version": "0.0.14",
"minAppVersion": "0.12.15", "minAppVersion": "0.12.15",
"description": "This is yet another plugin allowing users to sync notes between local device and the cloud.", "description": "This is yet another plugin allowing users to sync notes between local device and the cloud.",
"author": "fyears", "author": "fyears",
+6 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "obsidian-save-remote", "name": "obsidian-save-remote",
"version": "0.0.11", "version": "0.0.14",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev": "webpack --mode development --watch", "dev": "webpack --mode development --watch",
@@ -15,10 +15,12 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.22", "@types/chai": "^4.2.22",
"@types/chai-as-promised": "^7.1.4",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/node": "^14.14.37", "@types/node": "^14.14.37",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
@@ -33,6 +35,7 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.37.0", "@aws-sdk/client-s3": "^3.37.0",
"@aws-sdk/lib-storage": "^3.40.1",
"@aws-sdk/signature-v4-crt": "^3.37.0", "@aws-sdk/signature-v4-crt": "^3.37.0",
"acorn": "^8.5.0", "acorn": "^8.5.0",
"aws-crt": "^1.10.1", "aws-crt": "^1.10.1",
@@ -47,6 +50,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"webdav": "^4.7.0", "webdav": "^4.7.0",
"webdav-fs": "^4.0.0" "webdav-fs": "^4.0.0",
"xregexp": "^5.1.0"
} }
} }
+7 -3
View File
@@ -105,6 +105,7 @@ export default class SaveRemotePlugin extends Plugin {
this.settings.password this.settings.password
); );
if (!passwordCheckResult.ok) { if (!passwordCheckResult.ok) {
new Notice("something goes wrong while checking password");
throw Error(passwordCheckResult.reason); 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: "Empty means no password." });
contentEl.createEl("p", { 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", { 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", { 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) new Setting(contentEl)
+19
View File
@@ -2,6 +2,7 @@ import { Vault } from "obsidian";
import * as path from "path"; import * as path from "path";
import { base32 } from "rfc4648"; import { base32 } from "rfc4648";
import XRegExp from "xregexp";
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "ftp"; export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "ftp";
@@ -111,3 +112,21 @@ export const hexStringToTypedArray = (hex: string) => {
export const base64ToBase32 = (a: string) => { export const base64ToBase32 = (a: string) => {
return base32.stringify(Buffer.from(a, "base64")); 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
);
};
+19 -5
View File
@@ -3,6 +3,7 @@ import { Readable } from "stream";
import { Vault } from "obsidian"; import { Vault } from "obsidian";
import { Upload } from "@aws-sdk/lib-storage";
import { import {
S3Client, S3Client,
ListObjectsV2Command, ListObjectsV2Command,
@@ -98,7 +99,7 @@ export const uploadToRemote = async (
ContentType: contentType, ContentType: contentType,
}) })
); );
return await getRemoteMeta(s3Client, s3Config, fileOrFolderPath); return await getRemoteMeta(s3Client, s3Config, uploadFile);
} else { } else {
// file // file
// we ignore isRecursively parameter here // we ignore isRecursively parameter here
@@ -115,14 +116,24 @@ export const uploadToRemote = async (
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await encryptArrayBuffer(localContent, password);
} }
const body = arrayBufferToBuffer(remoteContent); 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, Bucket: s3Config.s3BucketName,
Key: uploadFile, Key: uploadFile,
Body: body, Body: body,
ContentType: contentType, ContentType: contentType,
}) },
); });
upload.on("httpUploadProgress", (progress) => {
// console.log(progress);
});
await upload.done();
return await getRemoteMeta(s3Client, s3Config, uploadFile); return await getRemoteMeta(s3Client, s3Config, uploadFile);
} }
}; };
@@ -149,6 +160,9 @@ export const listFromRemote = async (
if (rsp.$metadata.httpStatusCode !== 200) { if (rsp.$metadata.httpStatusCode !== 200) {
throw Error("some thing bad while listing remote!"); throw Error("some thing bad while listing remote!");
} }
if (rsp.Contents === undefined) {
break;
}
contents.push(...rsp.Contents); contents.push(...rsp.Contents);
isTruncated = rsp.IsTruncated; isTruncated = rsp.IsTruncated;
+21 -5
View File
@@ -16,7 +16,12 @@ import {
deleteFromRemote, deleteFromRemote,
downloadFromRemote, downloadFromRemote,
} from "./s3"; } from "./s3";
import { mkdirpInVault, SUPPORTED_SERVICES_TYPE, isHiddenPath } from "./misc"; import {
mkdirpInVault,
SUPPORTED_SERVICES_TYPE,
isHiddenPath,
isVaildText,
} from "./misc";
import { import {
decryptBase32ToString, decryptBase32ToString,
encryptStringToBase32, encryptStringToBase32,
@@ -74,6 +79,7 @@ export interface PasswordCheckType {
| "remote_encrypted_local_no_password" | "remote_encrypted_local_no_password"
| "password_matched" | "password_matched"
| "password_not_matched" | "password_not_matched"
| "invalid_text_after_decryption"
| "remote_not_encrypted_local_has_password" | "remote_not_encrypted_local_has_password"
| "no_password_both_sides"; | "no_password_both_sides";
} }
@@ -101,10 +107,20 @@ export const isPasswordOk = async (
} }
try { try {
const res = await decryptBase32ToString(santyCheckKey, password); const res = await decryptBase32ToString(santyCheckKey, password);
return {
ok: true, // additional test
reason: "password_matched", // because iOS Safari bypasses decryption with wrong password!
} as PasswordCheckType; if (isVaildText(res)) {
return {
ok: true,
reason: "password_matched",
} as PasswordCheckType;
} else {
return {
ok: false,
reason: "invalid_text_after_decryption",
} as PasswordCheckType;
}
} catch (error) { } catch (error) {
return { return {
ok: false, ok: false,
+13 -1
View File
@@ -1,6 +1,7 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; 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 { base64ToBase32, bufferToArrayBuffer } from "../src/misc";
import { import {
decryptArrayBuffer, decryptArrayBuffer,
@@ -9,6 +10,9 @@ import {
encryptStringToBase32, encryptStringToBase32,
} from "../src/encrypt"; } from "../src/encrypt";
chai.use(chaiAsPromised);
const expect = chai.expect;
describe("Encryption tests", () => { describe("Encryption tests", () => {
beforeEach(function () { beforeEach(function () {
global.window = { global.window = {
@@ -22,6 +26,14 @@ describe("Encryption tests", () => {
expect(await encryptStringToBase32(k, password)).to.not.equal(k); 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 () => { it("should encrypt and decrypt string and get the same result returned", async () => {
const k = "jfkkjkjbce7983ycdeknkkjckooAIUHIDIBIE((*BII)njD/d/dd/d/sjxhux"; const k = "jfkkjkjbce7983ycdeknkkjckooAIUHIDIBIE((*BII)njD/d/dd/d/sjxhux";
const password = "hfiuibec989###oiu982bj1`"; const password = "hfiuibec989###oiu982bj1`";
+19
View File
@@ -70,3 +70,22 @@ describe("Misc: get folder levels", () => {
expect(misc.getFolderLevels(item3)).to.deep.equal(res3); 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
View File
@@ -1,3 +1,3 @@
{ {
"0.0.11": "0.12.15" "0.0.14": "0.12.15"
} }