Compare commits

..
22 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
fyears fde4c9a294 0.0.11 2021-11-08 00:40:45 +08:00
fyears 7fc38a942a check password 2021-11-08 00:39:17 +08:00
fyears cc0c4fe224 second confirm for password changes 2021-11-08 00:17:42 +08:00
fyears 2ab5640c12 list all objects 2021-11-07 17:15:06 +08:00
fyears b7a0376a0f allow clearing sync mappings 2021-11-07 16:02:03 +08:00
fyears d7d1c47e2a long desc for debug 2021-11-07 15:44:14 +08:00
fyears a65f5eb004 head bucket 2021-11-07 15:43:57 +08:00
fyears 2ce34379ad format doc 2021-11-07 15:27:41 +08:00
14 changed files with 400 additions and 47 deletions
+1
View File
@@ -38,3 +38,4 @@ jobs:
path: | path: |
main.js main.js
manifest.json manifest.json
styles.css
+13 -13
View File
@@ -14,16 +14,16 @@ Assuming all sources are reliable.
We list all combinations mutually exclusive and collectively exhaustive. We list all combinations mutually exclusive and collectively exhaustive.
| ID | Remote Files | Local files | Local delete rename history | Extra | Decision | | ID | Remote Files | Local files | Local delete rename history | Extra | Decision |
| ---- | ------------ | ----------- | --------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | --- | ------------ | ----------- | --------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| 1 | exist | exist | ignore | mtime_remote > mtime_local | download remote file, create local folder if not exists, clear local history if exists | | 1 | exist | exist | ignore | mtime_remote > mtime_local | download remote file, create local folder if not exists, clear local history if exists |
| 2 | exist | exist | ignore | mtime_remote < mtime_local | upload local file, create remote folder if not exists, clear local history if exists | | 2 | exist | exist | ignore | mtime_remote < mtime_local | upload local file, create remote folder if not exists, clear local history if exists |
| 3 | exist | exist | ignore | mtime_remote === mtime_local && password === "" && size_remote === size_local | clear local history if exists (the file was synced and no changes after last sync) | | 3 | exist | exist | ignore | mtime_remote === mtime_local && password === "" && size_remote === size_local | clear local history if exists (the file was synced and no changes after last sync) |
| 4 | exist | exist | ignore | mtime_remote === mtime_local && password === "" && size_remote !== size_local | upload local file, clear local history if exists (we always prefer local to remote) | | 4 | exist | exist | ignore | mtime_remote === mtime_local && password === "" && size_remote !== size_local | upload local file, clear local history if exists (we always prefer local to remote) |
| 5 | exist | exist | ignore | mtime_remote === mtime_local && password !== "" | clear local history if exists (in encryption mode, file sizes are unequal. we can only rely on mtime(s)) | | 5 | exist | exist | ignore | mtime_remote === mtime_local && password !== "" | clear local history if exists (in encryption mode, file sizes are unequal. we can only rely on mtime(s)) |
| 6 | exist | exist | ignore | If local is a folder. mtime_local === undefined | clear local history if exists. TODO: what if a folder and a previous file share the same name? | | 6 | exist | exist | ignore | If local is a folder. mtime_local === undefined | clear local history if exists. TODO: what if a folder and a previous file share the same name? |
| 7 | exist | not exist | exist | mtime_remote >= delete_time_local | download remote file, create folder if not exists | | 7 | exist | not exist | exist | mtime_remote >= delete_time_local | download remote file, create folder if not exists |
| 8 | exist | not exist | exist | mtime_remote < delete_time_local | delete remote file, clear local history | | 8 | exist | not exist | exist | mtime_remote < delete_time_local | delete remote file, clear local history |
| 9 | exist | not exist | not exist | | download remote file, create folder if not exists | | 9 | exist | not exist | not exist | | download remote file, create folder if not exists |
| 10 | not exist | exist | ignore | local may be folder or file | upload local files recursively, create remote folder if not exists, clear local history if exists | | 10 | not exist | exist | ignore | local may be folder or file | upload local files recursively, create remote folder if not exists, clear local history if exists |
| 11 | not exist | not exist | ignore | | clear local history if exists | | 11 | not exist | not exist | ignore | | clear local history if exists |
+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.10", "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.10", "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"
} }
} }
+3
View File
@@ -8,6 +8,9 @@ import {
const DEFAULT_ITER = 10000; const DEFAULT_ITER = 10000;
// base32.stringify(Buffer.from('Salted__'))
export const MAGIC_ENCRYPTED_PREFIX_BASE32 = "KNQWY5DFMRPV";
const getKeyIVFromPassword = async ( const getKeyIVFromPassword = async (
salt: Uint8Array, salt: Uint8Array,
password: string, password: string,
+5
View File
@@ -276,6 +276,11 @@ export const getSyncMetaMappingByRemoteKeyS3 = async (
throw Error("something bad in sync meta mapping!"); throw Error("something bad in sync meta mapping!");
}; };
export const clearAllSyncMetaMapping = async (db: lf.DatabaseConnection) => {
const tbl = db.getSchema().table(DEFAULT_TBL_SYNC_MAPPING);
await db.delete().from(tbl).exec();
};
export const insertSyncPlanRecord = async ( export const insertSyncPlanRecord = async (
db: lf.DatabaseConnection, db: lf.DatabaseConnection,
syncPlan: SyncPlanType syncPlan: SyncPlanType
+147 -12
View File
@@ -11,7 +11,11 @@ import {
TFolder, TFolder,
} from "obsidian"; } from "obsidian";
import * as CodeMirror from "codemirror"; import * as CodeMirror from "codemirror";
import { clearAllSyncPlanRecords, DatabaseConnection } from "./localdb"; import {
clearAllSyncPlanRecords,
clearAllSyncMetaMapping,
DatabaseConnection,
} from "./localdb";
import { import {
prepareDBs, prepareDBs,
destroyDBs, destroyDBs,
@@ -22,9 +26,15 @@ import {
insertSyncPlanRecord, insertSyncPlanRecord,
} from "./localdb"; } from "./localdb";
import type { SyncStatusType } from "./sync"; import type { SyncStatusType, PasswordCheckType } from "./sync";
import { getSyncPlan, doActualSync } from "./sync"; import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
import { DEFAULT_S3_CONFIG, getS3Client, listFromRemote, S3Config } from "./s3"; import {
DEFAULT_S3_CONFIG,
getS3Client,
listFromRemote,
S3Config,
checkS3Connectivity,
} from "./s3";
import { exportSyncPlansToFiles } from "./debugMode"; import { exportSyncPlansToFiles } from "./debugMode";
interface SaveRemotePluginSettings { interface SaveRemotePluginSettings {
@@ -71,6 +81,7 @@ export default class SaveRemotePlugin extends Plugin {
} }
try { try {
//console.log(`huh ${this.settings.password}`)
new Notice("1/6 Save Remote Sync Preparing"); new Notice("1/6 Save Remote Sync Preparing");
this.syncStatus = "preparing"; this.syncStatus = "preparing";
@@ -87,7 +98,18 @@ export default class SaveRemotePlugin extends Plugin {
// console.log(local); // console.log(local);
// console.log(localHistory); // console.log(localHistory);
new Notice("4/6 Starting to generate sync plan."); new Notice("4/7 Checking password correct or not.");
this.syncStatus = "checking_password";
const passwordCheckResult = await isPasswordOk(
remoteRsp.Contents,
this.settings.password
);
if (!passwordCheckResult.ok) {
new Notice("something goes wrong while checking password");
throw Error(passwordCheckResult.reason);
}
new Notice("5/7 Starting to generate sync plan.");
this.syncStatus = "generating_plan"; this.syncStatus = "generating_plan";
const syncPlan = await getSyncPlan( const syncPlan = await getSyncPlan(
remoteRsp.Contents, remoteRsp.Contents,
@@ -102,7 +124,7 @@ export default class SaveRemotePlugin extends Plugin {
// The operations above are read only and kind of safe. // The operations above are read only and kind of safe.
// The operations below begins to write or delete (!!!) something. // The operations below begins to write or delete (!!!) something.
new Notice("5/6 Save Remote Sync data exchanging!"); new Notice("6/7 Save Remote Sync data exchanging!");
this.syncStatus = "syncing"; this.syncStatus = "syncing";
await doActualSync( await doActualSync(
@@ -114,7 +136,7 @@ export default class SaveRemotePlugin extends Plugin {
this.settings.password this.settings.password
); );
new Notice("6/6 Save Remote finish!"); new Notice("7/7 Save Remote finish!");
this.syncStatus = "finish"; this.syncStatus = "finish";
this.syncStatus = "idle"; this.syncStatus = "idle";
} catch (error) { } catch (error) {
@@ -122,7 +144,7 @@ export default class SaveRemotePlugin extends Plugin {
console.log(msg); console.log(msg);
console.log(error); console.log(error);
new Notice(msg); new Notice(msg);
new Notice(error); new Notice(error.message);
this.syncStatus = "idle"; this.syncStatus = "idle";
} }
}); });
@@ -165,6 +187,62 @@ export default class SaveRemotePlugin extends Plugin {
} }
} }
export class PasswordModal extends Modal {
plugin: SaveRemotePlugin;
newPassword: string;
constructor(app: App, plugin: SaveRemotePlugin, newPassword: string) {
super(app);
this.plugin = plugin;
this.newPassword = newPassword;
}
onOpen() {
let { contentEl } = this;
// contentEl.setText("Add Or change password.");
contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." });
contentEl.createEl("p", {
text: "This password allows you encrypt your files before sending to remote services.",
});
contentEl.createEl("p", { text: "Empty means no password." });
contentEl.createEl("p", {
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/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/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)
.addButton((button) => {
button.setButtonText("The Second Confirm to change password.");
button.onClick(async () => {
this.plugin.settings.password = this.newPassword;
await this.plugin.saveSettings();
new Notice("New password saved!");
this.close();
});
button.setClass("password_second_confirm");
})
.addButton((button) => {
button.setButtonText("Cancel (password not changed.)");
button.onClick(() => {
this.close();
});
});
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
class SaveRemoteSettingTab extends PluginSettingTab { class SaveRemoteSettingTab extends PluginSettingTab {
plugin: SaveRemotePlugin; plugin: SaveRemotePlugin;
@@ -247,22 +325,59 @@ class SaveRemoteSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerEl)
.setName("check connectivity")
.setDesc("check connectivity")
.addButton(async (button) => {
button.setButtonText("Check");
button.onClick(async () => {
new Notice("Checking...");
const s3Client = getS3Client(this.plugin.settings.s3);
const res = await checkS3Connectivity(
s3Client,
this.plugin.settings.s3
);
if (res) {
new Notice("Great! The bucket can be accessed.");
} else {
new Notice("The S3 bucket cannot be reached.");
}
});
});
containerEl.createEl("h2", { text: "General" }); containerEl.createEl("h2", { text: "General" });
let newPassword = `${this.plugin.settings.password}`;
new Setting(containerEl) new Setting(containerEl)
.setName("encryption password") .setName("encryption password")
.setDesc("Password for E2E encryption. Empty for no password.") .setDesc(
'Password for E2E encryption. Empty for no password. You need to click "Confirm".'
)
.addText((text) => .addText((text) =>
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(`${this.plugin.settings.password}`) .setValue(`${this.plugin.settings.password}`)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.password = value.trim(); newPassword = value.trim();
await this.plugin.saveSettings();
}) })
); )
.addButton(async (button) => {
button.setButtonText("Confirm");
button.onClick(async () => {
new PasswordModal(this.app, this.plugin, newPassword).open();
});
});
containerEl.createEl("h2", { text: "Debug" }); containerEl.createEl("h2", { text: "Debug" });
const syncPlanDiv = containerEl.createEl("div");
syncPlanDiv.createEl("p", {
text: "Sync plans are created every time after you trigger sync and before the actual sync.",
});
syncPlanDiv.createEl("p", {
text: "They are useful to know what would actually happen in those sync.",
});
new Setting(containerEl) new Setting(containerEl)
.setName("export sync plans") .setName("export sync plans")
.setDesc("export sync plans") .setDesc("export sync plans")
@@ -284,5 +399,25 @@ class SaveRemoteSettingTab extends PluginSettingTab {
new Notice("sync plans history (in db) deleted"); new Notice("sync plans history (in db) deleted");
}); });
}); });
const syncMappingDiv = containerEl.createEl("div");
syncMappingDiv.createEl("p", {
text: "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects.",
});
syncMappingDiv.createEl("p", {
text: "If the sync mappings history are deleted, unnecessary data exchanges may occur in next-time syncing, because whether a remote object and local object with same name are equivalent or not could not be determined correctly by comparing last modified times.",
});
new Setting(containerEl)
.setName("delete sync mappings history in db")
.setDesc("delete sync mappings history in db")
.addButton(async (button) => {
button.setButtonText("Delete Sync Mappings");
button.onClick(async () => {
await clearAllSyncMetaMapping(this.plugin.db);
new Notice("sync mappings history (in local db) deleted");
});
});
} }
} }
+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
);
};
+84 -14
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,
@@ -10,6 +11,9 @@ import {
GetObjectCommand, GetObjectCommand,
DeleteObjectCommand, DeleteObjectCommand,
HeadObjectCommand, HeadObjectCommand,
HeadBucketCommand,
ListObjectsV2CommandInput,
ListObjectsV2CommandOutput,
} from "@aws-sdk/client-s3"; } from "@aws-sdk/client-s3";
import type { _Object } from "@aws-sdk/client-s3"; import type { _Object } from "@aws-sdk/client-s3";
@@ -95,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
@@ -112,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);
} }
}; };
@@ -129,17 +143,45 @@ export const listFromRemote = async (
s3Config: S3Config, s3Config: S3Config,
prefix?: string prefix?: string
) => { ) => {
const confCmd = {
Bucket: s3Config.s3BucketName,
} as ListObjectsV2CommandInput;
if (prefix !== undefined) { if (prefix !== undefined) {
return await s3Client.send( confCmd.Prefix = prefix;
new ListObjectsV2Command({
Bucket: s3Config.s3BucketName,
Prefix: prefix,
})
);
} }
return await s3Client.send(
new ListObjectsV2Command({ Bucket: s3Config.s3BucketName }) const contents = [] as _Object[];
);
let isTruncated = true;
let continuationToken = "";
do {
const rsp = await s3Client.send(new ListObjectsV2Command(confCmd));
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;
confCmd.ContinuationToken = rsp.NextContinuationToken;
if (
isTruncated &&
(continuationToken === undefined || continuationToken === "")
) {
throw Error("isTruncated is true but no continuationToken provided");
}
} while (isTruncated);
// ensemble fake rsp
return {
"$.metadata": {
httpStatusCode: 200,
},
Contents: contents,
};
}; };
/** /**
@@ -266,3 +308,31 @@ export const deleteFromRemote = async (
// pass // pass
} }
}; };
/**
* Check the config of S3 by heading bucket
* https://stackoverflow.com/questions/50842835
* @param s3Client
* @param s3Config
* @returns
*/
export const checkS3Connectivity = async (
s3Client: S3Client,
s3Config: S3Config
) => {
try {
const results = await s3Client.send(
new HeadBucketCommand({ Bucket: s3Config.s3BucketName })
);
if (
results === undefined ||
results.$metadata === undefined ||
results.$metadata.httpStatusCode === undefined
) {
return false;
}
return results.$metadata.httpStatusCode === 200;
} catch (err) {
return false;
}
};
+83 -2
View File
@@ -16,14 +16,24 @@ import {
deleteFromRemote, deleteFromRemote,
downloadFromRemote, downloadFromRemote,
} from "./s3"; } from "./s3";
import { mkdirpInVault, SUPPORTED_SERVICES_TYPE, isHiddenPath } from "./misc"; import {
import { decryptBase32ToString, encryptStringToBase32 } from "./encrypt"; mkdirpInVault,
SUPPORTED_SERVICES_TYPE,
isHiddenPath,
isVaildText,
} from "./misc";
import {
decryptBase32ToString,
encryptStringToBase32,
MAGIC_ENCRYPTED_PREFIX_BASE32,
} from "./encrypt";
export type SyncStatusType = export type SyncStatusType =
| "idle" | "idle"
| "preparing" | "preparing"
| "getting_remote_meta" | "getting_remote_meta"
| "getting_local_meta" | "getting_local_meta"
| "checking_password"
| "generating_plan" | "generating_plan"
| "syncing" | "syncing"
| "finish"; | "finish";
@@ -61,6 +71,77 @@ export interface SyncPlanType {
mixedStates: Record<string, FileOrFolderMixedState>; mixedStates: Record<string, FileOrFolderMixedState>;
} }
export interface PasswordCheckType {
ok: boolean;
reason:
| "ok"
| "empty_remote"
| "remote_encrypted_local_no_password"
| "password_matched"
| "password_not_matched"
| "invalid_text_after_decryption"
| "remote_not_encrypted_local_has_password"
| "no_password_both_sides";
}
export const isPasswordOk = async (
remote: S3ObjectType[],
password: string = ""
) => {
if (remote === undefined || remote.length === 0) {
// remote empty
return {
ok: true,
reason: "empty_remote",
} as PasswordCheckType;
}
const santyCheckKey = remote[0].Key;
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
// this is encrypted!
// try to decrypt it using the provided password.
if (password === "") {
return {
ok: false,
reason: "remote_encrypted_local_no_password",
} as PasswordCheckType;
}
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,
reason: "password_not_matched",
} as PasswordCheckType;
}
} else {
// it is not encrypted!
if (password !== "") {
return {
ok: false,
reason: "remote_not_encrypted_local_has_password",
} as PasswordCheckType;
}
return {
ok: true,
reason: "no_password_both_sides",
} as PasswordCheckType;
}
};
const ensembleMixedStates = async ( const ensembleMixedStates = async (
remote: S3ObjectType[], remote: S3ObjectType[],
local: TAbstractFile[], local: TAbstractFile[],
+4
View File
@@ -1 +1,5 @@
/* set the styles */ /* set the styles */
.password_second_confirm {
font-weight: bold;
}
+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.10": "0.12.15" "0.0.14": "0.12.15"
} }