Skip to content

Commit 6b30f66

Browse files
Encrypt profiles and don't store passwords
1 parent f18bd1b commit 6b30f66

File tree

8 files changed

+149
-115
lines changed

8 files changed

+149
-115
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ALTER TABLE profile
2+
DROP COLUMN read_password;
3+
4+
--;;
5+
6+
ALTER TABLE profile
7+
ADD COLUMN edit_token TEXT;
8+
9+
--;;
10+
11+
ALTER TABLE profile
12+
ADD COLUMN is_public INTEGER;

src/flamebin/config.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
(mount/defstate config
8585
:start (do (init-config) (fn config-getter [& path] (apply cfg/get path))))
8686

87+
#_(mount/start #'config)
88+
8789
;;;; Misc initialization
8890

8991
(malli.registry/set-default-registry!

src/flamebin/core.clj

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,36 @@
1313
(@rl/global-saved-kbytes-limiter length-kb))
1414
(raise 429 "Upload saved bytes limit reached.")))
1515

16-
(defn save-profile [stream ip {:keys [profile-format type private?]
16+
(defn save-profile [stream ip {:keys [profile-format type public?]
1717
:as _upload-request}]
1818
(let [profile (case profile-format
1919
:collapsed (proc/collapsed-stacks-stream->dense-profile stream)
2020
:dense-edn (proc/dense-edn-stream->dense-profile stream))
21-
dpf-array (proc/freeze profile)
21+
edit-token (secret-token)
22+
read-token (when-not public? (secret-token))
23+
dpf-array (proc/freeze profile read-token)
2224
dpf-kb (quot (alength ^bytes dpf-array) 1024)
2325
id (db/new-unused-id)
2426
filename (format "%s.dpf" id)]
2527
(ensure-saved-limits ip dpf-kb)
2628
(storage/save-file dpf-array filename)
27-
(db/insert-profile
28-
;; TODO: replace IP with proper owner at some point
29-
(dto/->Profile id filename type (:total-samples profile) ip
30-
(when private? (secret-token)) (Instant/now)))))
29+
;; TODO: replace IP with proper owner at some point
30+
(-> (dto/->Profile id filename type (:total-samples profile) ip
31+
edit-token public? (Instant/now))
32+
db/insert-profile
33+
;; Attach read-token to the response here — it's not in the scheme
34+
;; because we don't store it in the DB.
35+
(assoc :read-token read-token))))
3136

32-
(defn render-profile [profile-id provided-read-password]
33-
(let [{:keys [read_password file_path] :as profile} (db/get-profile profile-id)]
37+
(defn render-profile [profile-id read-token]
38+
(let [{:keys [is_public file_path] :as profile} (db/get-profile profile-id)]
3439
;; Authorization
35-
(when read_password
36-
(when (nil? provided-read-password)
37-
(raise 403 "Required read-password to access this resource."))
38-
(when (not= read_password provided-read-password)
39-
(raise 403 "Incorrect read-password.")))
40+
(when-not is_public
41+
(when (nil? read-token)
42+
(raise 403 "Required read-token to access this resource.")))
4043

4144
(-> (storage/get-file file_path)
42-
proc/read-compressed-profile
45+
(proc/read-compressed-profile read-token)
4346
(render/render-html-flamegraph {}))))
4447

4548
(defn list-public-profiles []
@@ -48,4 +51,4 @@
4851
(comment
4952
(save-profile
5053
(clojure.java.io/input-stream (clojure.java.io/file "test/res/normal.txt"))
51-
"me" (dto/->UploadProfileRequest :collapsed :flamegraph "alloc" true)))
54+
"me" (dto/->UploadProfileRequest :collapsed :flamegraph "alloc" false)))

src/flamebin/db.clj

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
(defn insert-profile [profile]
5454
(m/assert Profile profile)
5555
(let [{:keys [id file_path profile_type sample_count owner upload_ts
56-
read_password]} profile]
56+
edit_token is_public]} profile]
5757
(log/infof "Inserting profile %s from %s" id owner)
5858
(with-locking db-lock
5959
(sql-helpers/insert! (db-options) :profile
@@ -62,29 +62,34 @@
6262
:profile_type (name profile_type)
6363
:upload_ts (str upload_ts)
6464
:sample_count sample_count
65-
:read_password read_password
65+
:is_public is_public
66+
:edit_token edit_token
6667
:owner owner}))
6768
profile))
6869

70+
#_(jdbc/execute! (db-options) ["SELECT * FROM profile"])
71+
72+
(defn- unqualify-keys [m] (update-keys m (comp keyword name)))
73+
6974
(defn list-profiles []
7075
(with-locking db-lock
71-
(->> (jdbc/execute! (db-options) ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts FROM profile"])
72-
(mapv #(-> (update-keys % (comp keyword name))
73-
(assoc :read_password nil)
76+
(->> (jdbc/execute! (db-options) ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, is_public FROM profile"])
77+
(mapv #(-> (unqualify-keys %)
78+
(assoc :edit_token nil)
7479
(coerce Profile))))))
7580

7681
(defn list-public-profiles [n]
7782
(with-locking db-lock
78-
(->> (jdbc/execute! (db-options) ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, read_password FROM profile
79-
WHERE read_password IS NULL ORDER BY upload_ts DESC LIMIT ?" n])
80-
(mapv #(-> (update-keys % (comp keyword name))
83+
(->> (jdbc/execute! (db-options) ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, is_public, edit_token FROM profile
84+
WHERE is_public = 1 ORDER BY upload_ts DESC LIMIT ?" n])
85+
(mapv #(-> (unqualify-keys %)
8186
(coerce Profile))))))
8287

8388
(defn get-profile [profile-id]
8489
(with-locking db-lock
85-
(let [q ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, read_password FROM profile WHERE id = ?" profile-id]
90+
(let [q ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, edit_token, is_public FROM profile WHERE id = ?" profile-id]
8691
row (some-> (jdbc/execute-one! (db-options) q)
87-
(update-keys (comp keyword name))
92+
unqualify-keys
8893
(coerce Profile))]
8994
(or row
9095
(let [msg (format "Profile with ID '%s' not found." profile-id)]
@@ -98,7 +103,9 @@ WHERE read_password IS NULL ORDER BY upload_ts DESC LIMIT ?" n])
98103

99104
(comment
100105
(clear-db)
101-
(insert-profile {:id (new-id) :file_path "no.txt" :profile_type :alloc :sample_count 100 :owner "me"} )
106+
(insert-profile {:id (new-id) :file_path "no.txt" :profile_type :alloc
107+
:sample_count 100 :owner "me" :upload_ts (java.time.Instant/now)
108+
:is_public true, :edit_token "sadhjflkaj"})
102109
(insert-profile {:id (new-id) :file_path "nilsamples" :profile_type "noexist" :sample_count nil :owner "me"})
103110
(find-ppf-file "xDRA4dpWFM")
104111
(let [p (malli.generator/generate Profile)]

src/flamebin/dto.clj

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,70 @@
44
[malli.core :as m]
55
[malli.experimental.lite :as mlite]
66
[malli.experimental.time.transform]
7-
[malli.transform :as mt])
7+
[malli.transform :as mt]
8+
[malli.util :as mu])
89
(:import java.time.Instant))
910

1011
config ;; Don't remove.
1112

13+
(def ^:private int-to-boolean
14+
{:decoders
15+
{:boolean #(cond (= % 1) true
16+
(= % 0) false
17+
:else %)}})
18+
1219
(def global-transformer
1320
(mt/transformer
1421
mt/string-transformer
22+
int-to-boolean
1523
malli.experimental.time.transform/time-transformer))
1624

1725
(defn coerce [value schema]
1826
(m/coerce schema value global-transformer))
1927

28+
(defmacro ^:private defschema-and-constructor [schema-name schema-val]
29+
(assert (= (first schema-val) '->))
30+
(assert (map? (second schema-val)))
31+
(let [ks (keys (second schema-val))]
32+
`(let [sch# ~schema-val]
33+
(def ~schema-name ~schema-val)
34+
(defn ~(symbol (str "->" schema-name)) ~(mapv symbol ks)
35+
(coerce ~(into {} (map #(vector % (symbol %)) ks)) ~schema-name)))))
36+
2037
;;;; Profile
2138

22-
(def Profile
23-
(mlite/schema
24-
{:id :nano-id
25-
:file_path [:and {:gen/fmap #(str % ".dpf")} :string]
26-
:profile_type :keyword
27-
:sample_count [:maybe nat-int?]
28-
:owner [:maybe :string]
29-
:read_password [:maybe :string]
30-
:upload_ts [:and {:gen/gen (gen/fmap Instant/ofEpochSecond
31-
(gen/choose 1500000000 1700000000))}
32-
:time/instant]}))
39+
(defschema-and-constructor Profile
40+
(-> {:id :nano-id
41+
:file_path [:and {:gen/fmap #(str % ".dpf")} :string]
42+
:profile_type :keyword
43+
:sample_count [:maybe nat-int?]
44+
:owner [:maybe :string]
45+
:edit_token [:maybe :string]
46+
:is_public :boolean
47+
:upload_ts [:and {:gen/gen (gen/fmap Instant/ofEpochSecond
48+
(gen/choose 1500000000 1700000000))}
49+
:time/instant]}
50+
mlite/schema))
3351

34-
(defn ->Profile
35-
[id file_path profile_type sample_count owner read_password upload_ts]
36-
(-> {:id id, :file_path file_path, :profile_type profile_type,
37-
:upload_ts upload_ts, :sample_count sample_count, :owner owner
38-
:read_password read_password}
39-
(coerce Profile)))
52+
#_((requiring-resolve 'malli.generator/sample) Profile)
4053

4154
;;;; DenseProfile
4255

43-
(def DenseProfile
44-
(m/schema [:map {:closed true}
45-
[:stacks [:vector [:tuple [:vector pos-int?] pos-int?]]]
46-
[:id->frame [:vector string?]]
47-
[:total-samples {:optional true} pos-int?]]))
48-
49-
#_((requiring-resolve 'malli.generator/sample) Profile)
56+
(defschema-and-constructor DenseProfile
57+
(-> {:stacks [:vector [:tuple [:vector pos-int?] pos-int?]]
58+
:id->frame [:vector string?]
59+
:total-samples pos-int?}
60+
mlite/schema
61+
(mu/optional-keys [:total-samples])
62+
mu/closed-schema))
5063

5164
;;;; UploadProfileRequest
5265

53-
(def UploadProfileRequest
54-
(mlite/schema
55-
{:profile-format [:enum :collapsed :dense-edn]
56-
:kind [:enum :flamegraph :diffgraph]
57-
:type [:re #"[\w\.]+"]
58-
:private? :boolean}))
59-
60-
(defn ->UploadProfileRequest [profile-format kind type private?]
61-
(-> {:profile-format profile-format, :kind kind, :type type, :private? private?}
62-
(coerce UploadProfileRequest)))
66+
(defschema-and-constructor UploadProfileRequest
67+
(-> {:profile-format [:enum :collapsed :dense-edn]
68+
:kind [:enum :flamegraph :diffgraph]
69+
:type [:re #"[\w\.]+"]
70+
:public? :boolean}
71+
mlite/schema))
6372

6473
#_(->UploadProfileRequest "collapsed" "diffgraph" "cpu" true)

src/flamebin/processing.clj

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
(ns flamebin.processing
2-
(:require [clj-async-profiler.post-processing :as pp]
2+
(:require [clojure.edn :as edn]
33
[clojure.java.io :as io]
44
[clojure.string :as str]
5-
[taoensso.nippy :as nippy]
65
[flamebin.dto :refer [DenseProfile]]
76
[flamebin.util :refer [raise]]
87
[malli.core :as m]
9-
[jsonista.core :as json]
10-
[flamebin.util :refer [raise]]
11-
[clojure.edn :as edn])
8+
[taoensso.nippy :as nippy])
129
(:import clj_async_profiler.Helpers
13-
(java.io BufferedReader InputStream)
14-
(java.util HashMap HashMap$Node Map$Entry)
15-
(java.util.function Consumer Function)
16-
java.io.PushbackReader))
10+
(java.io BufferedReader InputStream PushbackReader)
11+
(java.util HashMap Map$Entry)))
1712

1813
;; Collapsed stacks:
1914
;; a;b;c 10
@@ -88,16 +83,15 @@
8883
.stream
8984
(.sorted (Map$Entry/comparingByKey))
9085
(.forEach
91-
(reify Consumer
92-
(accept [_ entry]
93-
(let [stack (split-by-semicolon-and-transform-to-indices
94-
(.getKey ^Map$Entry entry) frame->id-map)
95-
value (.getValue ^Map$Entry entry)
96-
same (count-same stack (aget last-stack 0))
97-
dense-stack (into [same] (drop same stack))]
98-
(.add acc [dense-stack value])
99-
(aset last-stack 0 stack)
100-
(aset total-samples 0 (+ (aget total-samples 0) ^long value)))))))
86+
(fn [^Map$Entry entry]
87+
(let [stack (split-by-semicolon-and-transform-to-indices
88+
(.getKey entry) frame->id-map)
89+
value (.getValue entry)
90+
same (count-same stack (aget last-stack 0))
91+
dense-stack (into [same] (drop same stack))]
92+
(.add acc [dense-stack value])
93+
(aset last-stack 0 stack)
94+
(aset total-samples 0 (+ (aget total-samples 0) ^long value))))))
10195
id->frame-arr (object-array (.size frame->id-map))]
10296
(run! (fn [[k v]] (aset id->frame-arr v k)) frame->id-map)
10397
{:stacks (vec acc)
@@ -106,8 +100,9 @@
106100

107101
(def ^:private nippy-compressor nippy/zstd-compressor)
108102

109-
(defn freeze [object]
110-
(nippy/freeze object {:compressor nippy-compressor}))
103+
(defn freeze [object read-token]
104+
(nippy/freeze object {:compressor nippy-compressor
105+
:password (when read-token [:salted read-token])}))
111106

112107
(defn collapsed-stacks-stream->dense-profile [input-stream]
113108
(-> input-stream
@@ -122,8 +117,13 @@
122117
(update profile :total-samples
123118
#(or % (transduce (map second) + 0 (:stacks profile)))))))
124119

125-
(defn read-compressed-profile [source-file]
126-
(nippy/thaw-from-file source-file))
120+
(defn read-compressed-profile [source-file read-token]
121+
(try (nippy/thaw-from-file source-file {:password (when read-token
122+
[:salted read-token])})
123+
(catch clojure.lang.ExceptionInfo ex
124+
(if (str/includes? (ex-message ex) "decryption")
125+
(raise 403 "Failed to decrypt flamegraph, incorrect read-token.")
126+
(throw ex)))))
127127

128128
(comment
129129
(defn file-as-gzip-input-stream [file]
@@ -134,10 +134,13 @@
134134
(io/copy (io/file file) s))
135135
(.toByteArray baos)))))
136136

137-
(freeze
138-
(intermediate-profile->dense-profile
139-
(collapsed-stacks-stream->intermediate-profile
140-
(file-as-gzip-input-stream "test/res/huge.txt"))))
137+
(nippy/thaw
138+
(freeze
139+
(intermediate-profile->dense-profile
140+
(collapsed-stacks-stream->intermediate-profile
141+
(file-as-gzip-input-stream "test/res/normal.txt")))
142+
"key1")
143+
{:password [:salted "key2"]})
141144

142145
(with-open [w (io/writer (io/file "test/res/huge.edn"))]
143146
(binding [*out* w]

src/flamebin/web.clj

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,29 +106,29 @@
106106
(@rl/global-processed-kbytes-limiter length-kb))
107107
(raise 429 "Upload processed bytes limit reached.")))
108108

109-
(defn- profile-url [router profile-id read-password]
109+
(defn- profile-url [router profile-id read-token]
110110
(format "https://%s%s%s"
111111
(@config :server :host)
112112
(-> (r/match-by-name router ::profile-page {:profile-id profile-id})
113113
r/match->path)
114-
(if read-password
115-
(str "?read_password=" read-password)
114+
(if read-token
115+
(str "?read-token=" read-token)
116116
"")))
117117

118118
(defn $upload-profile [{:keys [remote-addr body query-params], router ::r/router
119119
:as req}]
120120
(let [length-kb (quot (ensure-content-length req) 1024)]
121121
(ensure-processed-limits remote-addr length-kb)
122122
;; TODO: probably better validate in routes coercion.
123-
(let [{:strs [kind type private], pformat "format"} query-params
123+
(let [{:strs [kind type public], pformat "format"} query-params
124124
req' (->UploadProfileRequest pformat (or kind :flamegraph) type
125-
(= private "true"))
125+
(= public "true"))
126126

127-
{:keys [id read_password] :as profile} (core/save-profile body remote-addr req')]
127+
{:keys [id read-token] :as profile} (core/save-profile body remote-addr req')]
128128
{:status 201
129-
:headers (cond-> {"Location" (profile-url router id read_password)
129+
:headers (cond-> {"Location" (profile-url router id read-token)
130130
"X-Created-ID" (str id)}
131-
read_password (assoc "X-Read-Password" read_password))
131+
read-token (assoc "X-Read-Token" read-token))
132132
:body profile})))
133133

134134
;; Endpoints: web pages
@@ -140,13 +140,13 @@
140140
(resp (pages/index-page)))
141141

142142
(defn $render-profile [{:keys [path-params query-params] :as req}]
143-
;; TODO: define read_password in routes.
143+
;; TODO: define read_token in routes.
144144
(let [{:keys [profile-id]} path-params]
145145
(when-not (valid-id? profile-id)
146146
(raise 404 (str "Invalid profile ID: " profile-id)))
147147
{:status 200
148148
:headers {"content-type" "text/html"}
149-
:body (core/render-profile profile-id (query-params "read_password"))}))
149+
:body (core/render-profile profile-id (query-params "read-token"))}))
150150

151151
(defn $public-resource [req]
152152
(ring.middleware.resource/resource-request req ""))

0 commit comments

Comments
 (0)