diff --git a/src/encrypt.ts b/src/encrypt.ts index 097621e..0a6885d 100644 --- a/src/encrypt.ts +++ b/src/encrypt.ts @@ -1,4 +1,4 @@ -import { base32, base64 } from "rfc4648"; +import { base32, base64url } from "rfc4648"; import { bufferToArrayBuffer, arrayBufferToBuffer, @@ -10,6 +10,8 @@ const DEFAULT_ITER = 20000; // base32.stringify(Buffer.from('Salted__')) export const MAGIC_ENCRYPTED_PREFIX_BASE32 = "KNQWY5DFMRPV"; +// base64.stringify(Buffer.from('Salted__')) +export const MAGIC_ENCRYPTED_PREFIX_BASE64URL = "U2FsdGVkX"; const getKeyIVFromPassword = async ( salt: Uint8Array, @@ -120,7 +122,7 @@ export const encryptStringToBase32 = async ( rounds, saltHex ); - return base32.stringify(new Uint8Array(enc)); + return base32.stringify(new Uint8Array(enc), { pad: false }); }; export const decryptBase32ToString = async ( @@ -130,7 +132,36 @@ export const decryptBase32ToString = async ( ) => { return new TextDecoder().decode( await decryptArrayBuffer( - bufferToArrayBuffer(base32.parse(text)), + bufferToArrayBuffer(base32.parse(text, { loose: true })), + password, + rounds + ) + ); +}; + +export const encryptStringToBase64url = async ( + text: string, + password: string, + rounds: number = DEFAULT_ITER, + saltHex: string = "" +) => { + const enc = await encryptArrayBuffer( + bufferToArrayBuffer(new TextEncoder().encode(text)), + password, + rounds, + saltHex + ); + return base64url.stringify(new Uint8Array(enc), { pad: false }); +}; + +export const decryptBase64urlToString = async ( + text: string, + password: string, + rounds: number = DEFAULT_ITER +) => { + return new TextDecoder().decode( + await decryptArrayBuffer( + bufferToArrayBuffer(base64url.parse(text, { loose: true })), password, rounds ) diff --git a/src/misc.ts b/src/misc.ts index 56bb0bb..6d80b7c 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,7 +1,7 @@ import { Vault } from "obsidian"; import * as path from "path"; -import { base32 } from "rfc4648"; +import { base32, base64url } from "rfc4648"; import XRegExp from "xregexp"; /** diff --git a/src/sync.ts b/src/sync.ts index d7eeda6..405d222 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -14,6 +14,9 @@ import { decryptBase32ToString, encryptStringToBase32, MAGIC_ENCRYPTED_PREFIX_BASE32, + decryptBase64urlToString, + encryptStringToBase64url, + MAGIC_ENCRYPTED_PREFIX_BASE64URL, } from "./encrypt"; export type SyncStatusType = @@ -85,7 +88,7 @@ export const isPasswordOk = async ( } const santyCheckKey = remote[0].key; if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) { - // this is encrypted! + // this is encrypted using old base32! // try to decrypt it using the provided password. if (password === "") { return { @@ -96,6 +99,38 @@ 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, + reason: "password_not_matched", + } as PasswordCheckType; + } + } + if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL)) { + // this is encrypted using new base64url! + // 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 decryptBase64urlToString(santyCheckKey, password); + // additional test // because iOS Safari bypasses decryption with wrong password! if (isVaildText(res)) { @@ -145,7 +180,15 @@ const ensembleMixedStates = async ( const remoteEncryptedKey = entry.key; let key = remoteEncryptedKey; if (password !== "") { - key = await decryptBase32ToString(remoteEncryptedKey, password); + if (remoteEncryptedKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) { + key = await decryptBase32ToString(remoteEncryptedKey, password); + } else if ( + remoteEncryptedKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL) + ) { + key = await decryptBase64urlToString(remoteEncryptedKey, password); + } else { + throw Error(`unexpected key=${remoteEncryptedKey}`); + } } const backwardMapping = await getSyncMetaMappingByRemoteKey( remoteType, @@ -435,7 +478,10 @@ const dispatchOperationToActual = async ( if (password !== "") { remoteEncryptedKey = state.remote_encrypted_key; if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") { - remoteEncryptedKey = await encryptStringToBase32(key, password); + // the old version uses base32 + // remoteEncryptedKey = await encryptStringToBase32(key, password); + // the new version users base64url + remoteEncryptedKey = await encryptStringToBase64url(key, password); } }