diff --git a/package.json b/package.json index 68cd97a..8c7ab88 100644 --- a/package.json +++ b/package.json @@ -31,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", diff --git a/src/misc.ts b/src/misc.ts index 7f344db..a2410c5 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -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 + ); +}; diff --git a/src/sync.ts b/src/sync.ts index 8507d71..9ab06cf 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -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, diff --git a/tests/misc.test.ts b/tests/misc.test.ts index 1ec97a5..de5363f 100644 --- a/tests/misc.test.ts +++ b/tests/misc.test.ts @@ -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; + }); +});