Compare commits

...
8 Commits
Author SHA1 Message Date
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
9 changed files with 90 additions and 14 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsdian-save-remote",
"name": "Save remote",
"version": "0.0.11",
"version": "0.0.13",
"minAppVersion": "0.12.15",
"description": "This is yet another plugin allowing users to sync notes between local device and the cloud.",
"author": "fyears",
+5 -2
View File
@@ -1,6 +1,6 @@
{
"name": "obsidian-save-remote",
"version": "0.0.11",
"version": "0.0.13",
"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",
@@ -29,7 +31,8 @@
"typescript": "^4.4.4",
"webdav-server": "^2.6.2",
"webpack": "^5.58.2",
"webpack-cli": "^4.9.1"
"webpack-cli": "^4.9.1",
"xregexp": "^5.1.0"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.37.0",
+7 -3
View File
@@ -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
View File
@@ -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
);
};
+4 -1
View File
@@ -98,7 +98,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
@@ -149,6 +149,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;
+21 -5
View File
@@ -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);
return {
ok: true,
reason: "password_matched",
} as PasswordCheckType;
// 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
View File
@@ -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`";
+19
View File
@@ -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
View File
@@ -1,3 +1,3 @@
{
"0.0.11": "0.12.15"
"0.0.13": "0.12.15"
}