From 8de2542f8c65b60808b547d8f7882317988cae32 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 30 Dec 2025 21:02:56 +0800 Subject: [PATCH] use salted PBKDF2 hash --- deps/publish/src/logseq/publish/common.cljs | 112 +++++++++++++++----- deps/publish/src/logseq/publish/routes.cljs | 13 ++- src/main/frontend/handler/publish.cljs | 3 +- 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/deps/publish/src/logseq/publish/common.cljs b/deps/publish/src/logseq/publish/common.cljs index 10ed4a5d29..8955987064 100644 --- a/deps/publish/src/logseq/publish/common.cljs +++ b/deps/publish/src/logseq/publish/common.cljs @@ -120,6 +120,89 @@ digest (.digest js/crypto.subtle "SHA-256" data)] (to-hex digest))) +(def password-kdf-iterations 210000) + +(defn bytes->base64url [bytes] + (let [binary (apply str (map #(js/String.fromCharCode %) (array-seq bytes))) + b64 (js/btoa binary)] + (-> b64 + (string/replace #"\+" "-") + (string/replace #"/" "_") + (string/replace #"=+$" "")))) + +(defn hash-password [password] + (js-await [salt (doto (js/Uint8Array. 16) + (js/crypto.getRandomValues)) + crypto-key (.importKey js/crypto.subtle + "raw" + (.encode text-encoder password) + #js {:name "PBKDF2"} + false + #js ["deriveBits"]) + derived (.deriveBits js/crypto.subtle + #js {:name "PBKDF2" + :hash "SHA-256" + :salt salt + :iterations password-kdf-iterations} + crypto-key + 256) + derived-bytes (js/Uint8Array. derived) + salt-encoded (bytes->base64url salt) + hash-encoded (bytes->base64url derived-bytes)] + (str "pbkdf2$sha256$" + password-kdf-iterations + "$" + salt-encoded + "$" + hash-encoded))) + +(defn base64url->uint8array [input] + (let [pad (if (pos? (mod (count input) 4)) + (apply str (repeat (- 4 (mod (count input) 4)) "=")) + "") + base64 (-> (str input pad) + (string/replace "-" "+") + (string/replace "_" "/")) + raw (js/atob base64) + data (js/Uint8Array. (.-length raw))] + (dotimes [i (.-length raw)] + (aset data i (.charCodeAt raw i))) + data)) + +(defn verify-password [password stored-hash] + (let [parts (when (string? stored-hash) + (string/split stored-hash #"\$"))] + (if-not (and (= 5 (count parts)) + (= "pbkdf2" (nth parts 0)) + (= "sha256" (nth parts 1))) + false + (js-await [iterations (js/parseInt (nth parts 2)) + salt (base64url->uint8array (nth parts 3)) + expected (base64url->uint8array (nth parts 4)) + crypto-key (.importKey js/crypto.subtle + "raw" + (.encode text-encoder password) + #js {:name "PBKDF2"} + false + #js ["deriveBits"]) + derived (.deriveBits js/crypto.subtle + #js {:name "PBKDF2" + :hash "SHA-256" + :salt salt + :iterations iterations} + crypto-key + (* 8 (.-length expected))) + derived-bytes (js/Uint8Array. derived)] + (if (not= (.-length derived-bytes) (.-length expected)) + false + (let [mismatch (reduce (fn [acc idx] + (bit-or acc + (bit-xor (aget derived-bytes idx) + (aget expected idx)))) + 0 + (range (.-length expected)))] + (zero? mismatch))))))) + (defn hmac-sha256 [key message] (js-await [crypto-key (.importKey js/crypto.subtle "raw" @@ -194,22 +277,9 @@ signed-query (str canonical-query "&X-Amz-Signature=" signature)] (str "https://" host canonical-uri "?" signed-query))) -(defn base64url->uint8array [input] - (let [pad (if (pos? (mod (count input) 4)) - (apply str (repeat (- 4 (mod (count input) 4)) "=")) - "") - base64 (-> (str input pad) - (string/replace "-" "+") - (string/replace "_" "/")) - raw (js/atob base64) - bytes (js/Uint8Array. (.-length raw))] - (dotimes [i (.-length raw)] - (aset bytes i (.charCodeAt raw i))) - bytes)) - (defn decode-jwt-part [part] - (let [bytes (base64url->uint8array part)] - (js/JSON.parse (.decode text-decoder bytes)))) + (let [data (base64url->uint8array part)] + (js/JSON.parse (.decode text-decoder data)))) (defn import-rsa-key [jwk] (.importKey js/crypto.subtle @@ -254,17 +324,9 @@ (when etag (string/replace etag #"\"" ""))) -(defn bytes->base64url [bytes] - (let [binary (apply str (map #(js/String.fromCharCode %) (array-seq bytes))) - b64 (js/btoa binary)] - (-> b64 - (string/replace #"\+" "-") - (string/replace #"/" "_") - (string/replace #"=+$" "")))) - (defn short-id-for-page [graph-uuid page-uuid] (js-await [payload (.encode text-encoder (str graph-uuid ":" page-uuid)) digest (.digest js/crypto.subtle "SHA-256" payload)] - (let [bytes (js/Uint8Array. digest) - encoded (bytes->base64url bytes)] + (let [data (js/Uint8Array. digest) + encoded (bytes->base64url data)] (subs encoded 0 10)))) diff --git a/deps/publish/src/logseq/publish/routes.cljs b/deps/publish/src/logseq/publish/routes.cljs index 166482a5fb..3af0a7e1cb 100644 --- a/deps/publish/src/logseq/publish/routes.cljs +++ b/deps/publish/src/logseq/publish/routes.cljs @@ -39,8 +39,8 @@ {:allowed? true :provided? false} (let [provided (request-password request)] (if (string? provided) - (js-await [hashed (publish-common/sha256-hex provided)] - {:allowed? (= hashed stored-hash) :provided? true}) + (js-await [valid? (publish-common/verify-password provided stored-hash)] + {:allowed? valid? :provided? true}) {:allowed? false :provided? false}))))) (defn- auth-claims @@ -78,8 +78,8 @@ (publish-model/entity->title (get payload-entities page-eid)))) blocks (or (:blocks payload) (get payload "blocks")) - page-password-hash (or (:page-password-hash payload) - (get payload "page-password-hash")) + page-password (or (:page-password payload) + (get payload "page-password")) refs (when (and page-eid page-title) (publish-index/page-refs-from-payload payload page-eid page_uuid page-title graph)) tagged-nodes (when (and page-eid page-title) @@ -113,12 +113,15 @@ (throw (ex-info "owner sub or username is missing" {:owner-sub owner-sub :owner-username owner-username}))) + password-hash (when (and (string? page-password) + (not (string/blank? page-password))) + (publish-common/hash-password page-password)) payload (bean/->js {:page_uuid page_uuid :page_title page-title :page_tags (when page-tags (js/JSON.stringify (clj->js page-tags))) - :password_hash page-password-hash + :password_hash password-hash :graph graph-uuid :schema_version schema_version :block_count block_count diff --git a/src/main/frontend/handler/publish.cljs b/src/main/frontend/handler/publish.cljs index 4487bb05eb..e5e4be3323 100644 --- a/src/main/frontend/handler/publish.cljs +++ b/src/main/frontend/handler/publish.cljs @@ -321,9 +321,8 @@ page-password (when (and (string? page-password) (not (string/blank? page-password))) page-password) - page-password-hash (when page-password ( payload - page-password-hash (assoc :page-password-hash page-password-hash)) + page-password (assoc :page-password page-password)) body (ldb/write-transit-str payload) content-hash (