Skip to content

Commit 4593ab3

Browse files
committed
enhance(e2ee): store encrypted password in keychain on mobile
1 parent 6ed01df commit 4593ab3

File tree

13 files changed

+183
-72
lines changed

13 files changed

+183
-72
lines changed

android/app/capacitor.build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ android {
99

1010
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
1111
dependencies {
12+
implementation project(':aparajita-capacitor-secure-storage')
1213
implementation project(':capacitor-community-safe-area')
1314
implementation project(':capacitor-action-sheet')
1415
implementation project(':capacitor-app')

android/app/src/main/assets/capacitor.plugins.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
[
2+
{
3+
"pkg": "@aparajita/capacitor-secure-storage",
4+
"classpath": "com.aparajita.capacitor.securestorage.SecureStorage"
5+
},
26
{
37
"pkg": "@capacitor-community/safe-area",
48
"classpath": "com.getcapacitor.community.safearea.SafeAreaPlugin"

android/capacitor.settings.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
include ':capacitor-android'
33
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
44

5+
include ':aparajita-capacitor-secure-storage'
6+
project(':aparajita-capacitor-secure-storage').projectDir = new File('../node_modules/@aparajita/capacitor-secure-storage/android')
7+
58
include ':capacitor-community-safe-area'
69
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/@capacitor-community/safe-area/android')
710

ios/App/Podfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
1111
def capacitor_pods
1212
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
1313
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
14+
pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/@aparajita/capacitor-secure-storage'
1415
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/@capacitor-community/safe-area'
1516
pod 'CapacitorActionSheet', :path => '../../node_modules/@capacitor/action-sheet'
1617
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'

ios/App/Podfile.lock

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
PODS:
2+
- AparajitaCapacitorSecureStorage (7.1.6):
3+
- Capacitor
4+
- KeychainSwift (~> 21.0)
25
- Capacitor (7.2.0):
36
- CapacitorCordova
47
- CapacitorActionSheet (7.0.1):
@@ -32,12 +35,14 @@ PODS:
3235
- Capacitor
3336
- JcesarmobileSslSkip (0.4.0):
3437
- Capacitor
38+
- KeychainSwift (21.0.0)
3539
- SendIntent (7.0.0):
3640
- Capacitor
3741
- StayLiquid (0.1.0):
3842
- Capacitor (>= 6.0.0)
3943

4044
DEPENDENCIES:
45+
- "AparajitaCapacitorSecureStorage (from `../../node_modules/@aparajita/capacitor-secure-storage`)"
4146
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
4247
- "CapacitorActionSheet (from `../../node_modules/@capacitor/action-sheet`)"
4348
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
@@ -58,7 +63,13 @@ DEPENDENCIES:
5863
- SendIntent (from `../../node_modules/send-intent`)
5964
- StayLiquid (from `../../node_modules/stay-liquid`)
6065

66+
SPEC REPOS:
67+
trunk:
68+
- KeychainSwift
69+
6170
EXTERNAL SOURCES:
71+
AparajitaCapacitorSecureStorage:
72+
:path: "../../node_modules/@aparajita/capacitor-secure-storage"
6273
Capacitor:
6374
:path: "../../node_modules/@capacitor/ios"
6475
CapacitorActionSheet:
@@ -99,6 +110,7 @@ EXTERNAL SOURCES:
99110
:path: "../../node_modules/stay-liquid"
100111

101112
SPEC CHECKSUMS:
113+
AparajitaCapacitorSecureStorage: 502bff73187cf9d0164459458ccf47ec65d5895a
102114
Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7
103115
CapacitorActionSheet: 4213427449132ae4135674d93010cb011725647e
104116
CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
@@ -116,9 +128,10 @@ SPEC CHECKSUMS:
116128
CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f
117129
CapgoCapacitorNavigationBar: 067b1c1d1ede5ce96200a730ce7fd498e9641509
118130
JcesarmobileSslSkip: 5fa98636a64c36faa50f32ab4daf34e38f4d45b9
131+
KeychainSwift: 4a71a45c802fd9e73906457c2dcbdbdc06c9419d
119132
SendIntent: 8a6f646a4489f788d253ffbd1082a98ea388d870
120133
StayLiquid: dac4b6cd7761472754f97d367ba4651ca79fcd2e
121134

122-
PODFILE CHECKSUM: 858411d5b4560fd80593ea76b0fd6359bccabec3
135+
PODFILE CHECKSUM: 3223217c556441dd921e66e1c855a0e8ee98159c
123136

124137
COCOAPODS: 1.16.2

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"postinstall": "yarn tldraw:build && yarn ui:build"
109109
},
110110
"dependencies": {
111+
"@aparajita/capacitor-secure-storage": "^7.1.6",
111112
"@capacitor-community/safe-area": "7.0.0-alpha.1",
112113
"@capacitor/action-sheet": "7.0.1",
113114
"@capacitor/android": "7.2.0",

src/electron/electron/handler.cljs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -618,14 +618,14 @@
618618
(defmethod handle :cancel-all-requests [_ args]
619619
(apply rsapi/cancel-all-requests (rest args)))
620620

621-
(defmethod handle :keychain/save-e2ee-password [_window [_ refresh-token encrypted-text]]
622-
(keychain/<set-password! refresh-token encrypted-text))
621+
(defmethod handle :keychain/save-e2ee-password [_window [_ key encrypted-text]]
622+
(keychain/<set-password! key encrypted-text))
623623

624-
(defmethod handle :keychain/get-e2ee-password [_window [_ refresh-token]]
625-
(keychain/<get-password refresh-token))
624+
(defmethod handle :keychain/get-e2ee-password [_window [_ key]]
625+
(keychain/<get-password key))
626626

627-
(defmethod handle :keychain/delete-e2ee-password [_window [_ refresh-token]]
628-
(keychain/<delete-password! refresh-token))
627+
(defmethod handle :keychain/delete-e2ee-password [_window [_ key]]
628+
(keychain/<delete-password! key))
629629

630630
(defmethod handle :default [args]
631631
(logger/error "Error: no ipc handler for:" args))

src/electron/electron/keychain.cljs

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
(ns electron.keychain
22
"Helper functions for storing E2EE secrets inside the OS keychain."
3-
(:require ["crypto" :as crypto]
4-
["electron" :refer [app]]
3+
(:require ["electron" :refer [app]]
54
["keytar" :as keytar]
65
[clojure.string :as string]
76
[electron.logger :as logger]
@@ -19,26 +18,14 @@
1918
[]
2019
(str (force service-name) " E2EE"))
2120

22-
(defn- normalize-account
23-
[refresh-token]
24-
(when (and (string? refresh-token)
25-
(not (string/blank? refresh-token)))
26-
(try
27-
(let [hash (.createHash crypto "sha256")]
28-
(.update hash refresh-token)
29-
(.digest hash "hex"))
30-
(catch :default e
31-
(logger/error ::normalize-account {:error e})
32-
nil))))
33-
3421
(defn supported?
3522
[]
3623
(boolean keytar))
3724

3825
(defn <set-password!
3926
"Persist `encrypted-text` for the `refresh-token` entry."
40-
[refresh-token encrypted-text]
41-
(if-let [account (and (supported?) (normalize-account refresh-token))]
27+
[key encrypted-text]
28+
(if-let [account (and (supported?) key)]
4229
(-> (p/let [_ (.setPassword keytar (keychain-service) account encrypted-text)]
4330
true)
4431
(p/catch (fn [e]
@@ -48,8 +35,8 @@
4835

4936
(defn <get-password
5037
"Fetch encrypted text stored for `refresh-token`."
51-
[refresh-token]
52-
(if-let [account (and (supported?) (normalize-account refresh-token))]
38+
[key]
39+
(if-let [account (and (supported?) key)]
5340
(-> (p/let [password (.getPassword keytar (keychain-service) account)]
5441
password)
5542
(p/catch (fn [e]
@@ -58,8 +45,8 @@
5845
(p/resolved nil)))
5946

6047
(defn <delete-password!
61-
[refresh-token]
62-
(if-let [account (and (supported?) (normalize-account refresh-token))]
48+
[key]
49+
(if-let [account (and (supported?) key)]
6350
(-> (p/let [_ (.deletePassword keytar (keychain-service) account)]
6451
true)
6552
(p/catch (fn [e]

src/main/frontend/handler/e2ee.cljs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
(:require [electron.ipc :as ipc]
44
[frontend.common.crypt :as crypt]
55
[frontend.common.thread-api :refer [def-thread-api]]
6+
[frontend.mobile.secure-storage :as secure-storage]
67
[frontend.state :as state]
78
[frontend.util :as util]
89
[lambdaisland.glogi :as log]
@@ -13,30 +14,39 @@
1314
(def ^:private delete-op :keychain/delete-e2ee-password)
1415

1516
(defn- <keychain-save!
16-
[refresh-token encrypted-text]
17-
(if (util/electron?)
18-
(-> (ipc/ipc save-op refresh-token encrypted-text)
19-
(p/catch (fn [e]
20-
(log/error :keychain-save-failed e)
21-
(throw e))))
17+
[key encrypted-text]
18+
(cond
19+
(util/electron?)
20+
(ipc/ipc save-op key encrypted-text)
21+
22+
(util/capacitor?)
23+
(secure-storage/<set-item! key encrypted-text)
24+
25+
:else
2226
(p/resolved nil)))
2327

2428
(defn- <keychain-get
25-
[refresh-token]
26-
(if (util/electron?)
27-
(-> (ipc/ipc get-op refresh-token)
28-
(p/catch (fn [e]
29-
(log/error :keychain-get-failed e)
30-
(throw e))))
29+
[key]
30+
(cond
31+
(util/electron?)
32+
(ipc/ipc get-op key)
33+
34+
(util/capacitor?)
35+
(secure-storage/<get-item key)
36+
37+
:else
3138
(p/resolved nil)))
3239

3340
(defn- <keychain-delete!
34-
[refresh-token]
35-
(if (util/electron?)
36-
(-> (ipc/ipc delete-op refresh-token)
37-
(p/catch (fn [e]
38-
(log/error :keychain-delete-failed e)
39-
(throw e))))
41+
[key]
42+
(cond
43+
(util/electron?)
44+
(ipc/ipc delete-op key)
45+
46+
(util/capacitor?)
47+
(secure-storage/<remove-item! key)
48+
49+
:else
4050
(p/resolved nil)))
4151

4252
(def-thread-api :thread-api/request-e2ee-password
@@ -59,14 +69,14 @@
5969
[encrypted-private-key]
6070
(<decrypt-user-e2ee-private-key encrypted-private-key))
6171

62-
(def-thread-api :thread-api/electron-save-e2ee-password
63-
[refresh-token encrypted-text]
64-
(<keychain-save! refresh-token encrypted-text))
72+
(def-thread-api :thread-api/native-save-e2ee-password
73+
[encrypted-text]
74+
(<keychain-save! "logseq-encrypted-password" encrypted-text))
6575

66-
(def-thread-api :thread-api/electron-get-e2ee-password
67-
[refresh-token]
68-
(<keychain-get refresh-token))
76+
(def-thread-api :thread-api/native-get-e2ee-password
77+
[]
78+
(<keychain-get "logseq-encrypted-password"))
6979

70-
(def-thread-api :thread-api/electron-delete-e2ee-password
71-
[refresh-token]
72-
(<keychain-delete! refresh-token))
80+
(def-thread-api :thread-api/native-delete-e2ee-password
81+
[]
82+
(<keychain-delete! "logseq-encrypted-password"))
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
(ns frontend.mobile.secure-storage
2+
"Wrapper around the Capacitor secure storage plugin."
3+
(:require ["@aparajita/capacitor-secure-storage" :refer [SecureStorage]]
4+
[frontend.mobile.util :as mobile-util]
5+
[lambdaisland.glogi :as log]
6+
[promesa.core :as p]))
7+
8+
(defonce ^:private *initialized? (atom false))
9+
(def ^:private key-prefix "logseq.e2ee.")
10+
11+
(defn- <ensure-initialized!
12+
[]
13+
(cond
14+
(not (mobile-util/native-platform?))
15+
(p/resolved false)
16+
17+
@*initialized?
18+
(p/resolved true)
19+
20+
:else
21+
(-> (p/let [_ (.setKeyPrefix SecureStorage key-prefix)]
22+
(reset! *initialized? true))
23+
(p/catch (fn [e]
24+
(log/error ::init {:error e})
25+
(throw e)))))) ;; propagate so callers can fallback if needed
26+
27+
(defn <set-item!
28+
[key value]
29+
(if (mobile-util/native-platform?)
30+
(-> (p/let [_ (<ensure-initialized!)
31+
_ (.setItem SecureStorage key value)]
32+
true)
33+
(p/catch (fn [e]
34+
(log/error ::set-item {:error e})
35+
(throw e))))
36+
(p/resolved false)))
37+
38+
(defn <get-item
39+
[key]
40+
(if (mobile-util/native-platform?)
41+
(-> (p/let [_ (<ensure-initialized!)]
42+
(.getItem SecureStorage key))
43+
(p/catch (fn [e]
44+
(log/error ::get-item {:error e})
45+
(throw e))))
46+
(p/resolved nil)))
47+
48+
(defn <remove-item!
49+
[key]
50+
(if (mobile-util/native-platform?)
51+
(-> (p/let [_ (<ensure-initialized!)
52+
_ (.removeItem SecureStorage key)]
53+
true)
54+
(p/catch (fn [e]
55+
(log/error ::remove-item {:error e})
56+
(throw e))))
57+
(p/resolved false)))

0 commit comments

Comments
 (0)