on master help sign in
added editing
commit59c98543b645c60321c2462f4c9c49cc29d73695 authoriko <ilyakooo0@gmail.com> dateThu 11 Aug 2022, 14:59 (4 years ago) parent971e20e
22 files changed+510−88
backend/BW.js+1−1 backend/BW.purs+2−1 backend/BW/Logic.purs+175−5 backend/BW/Types.purs+4−4 backend/Bridge.purs+6−16 backend/Data/EncString.js+4 backend/Data/EncString.purs+2 backend/Main.purs+40−15 bridge.yaml+6−3 frontend/Bridge.elm+19−24 frontend/GlobalEvents.elm+7 frontend/Logic/Cipher.elm+77 frontend/Main.elm+97−8 frontend/Page.elm+2 frontend/Pages/Captcha.elm+1 frontend/Pages/Cipher.elm+24−8 frontend/Pages/Ciphers.elm+17 frontend/Pages/EditCipher.elm+15−1 frontend/Pages/Login.elm+1−1 frontend/Pages/MasterPassword.elm+1 frontend/Pages/Navigation.elm+4−1 frontend/Types.elm+5
backend/BW.js+1−1
@@ -76,7 +76,7 @@ export function getServices() {
7676 ),
7777 getSync: () => api.getSync().then(sanitize),
7878 postCipher: (cipher) => api.postCipher(makeCipherRequest(cipher)),
79− putCipher: (id) => (cipher) => api.putCipher(id, makeCipherRequest(cipher)),
79+ putCipher: (cipher) => api.putCipher(cipher.id, makeCipherRequest(cipher)),
8080 }
8181 },
8282 crypto: bg.cryptoService,
backend/BW.purs+2−1
@@ -20,6 +20,7 @@ type CryptoService
2020 , hashPassword :: Fn3 Password SymmetricCryptoKey (JNullable HashPurpose) (Promise StringHash)
2121 , decryptToUtf8 :: Fn2 EncString SymmetricCryptoKey (Promise String)
2222 , decryptToBytes :: Fn2 EncString SymmetricCryptoKey (Promise ArrayBuffer)
23+ , encrypt :: Fn2 String SymmetricCryptoKey (Promise EncString)
2324 , stretchKey :: SymmetricCryptoKey -> Promise SymmetricCryptoKey
2425 }
2526
@@ -30,7 +31,7 @@ type ApiService
3031 , postIdentityToken :: PasswordTokenRequest -> Promise (IdentityCaptchaResponse |+| IdentityTokenResponse)
3132 , getSync :: Unit -> Promise SyncResponse
3233 , postCipher :: CipherResponse -> Promise CipherResponse
33− , putCipher :: CipherId -> CipherResponse -> Promise CipherResponse
34+ , putCipher :: CipherResponse -> Promise CipherResponse
3435 }
3536
3637cryptoFunctionsTypeSha1 = "sha1" :: CryptoFunctionsType
backend/BW/Logic.purs+175−5
@@ -11,13 +11,14 @@ import Data.EncString as EncString
1111import Data.Function.Uncurried (runFn2, runFn3, runFn4)
1212import Data.JNullable (JNullable, fromJNullable, jnull, nullify)
1313import Data.JNullable as JNullable
14−import Data.JOpt (fromJOpt)
14+import Data.JOpt (JOpt(..), fromJOpt)
1515import Data.Maybe (Maybe(..), fromMaybe)
16−import Data.Newtype (class Newtype, wrap)
16+import Data.Newtype (class Newtype, unwrap, wrap)
1717import Data.String as String
1818import Data.SymmetricCryptoKey (SymmetricCryptoKey)
1919import Data.SymmetricCryptoKey as SymmetricCryptoKey
2020import Data.Traversable (traverse)
21+import Data.Undefined.NoProblem (opt, undefined)
2122import Effect (Effect)
2223import Effect.Aff (Aff)
2324import Effect.Aff.Class (class MonadAff, liftAff)
@@ -147,9 +148,159 @@ decrypt input = do
147148 key <- askAt (Proxy :: _ "key")
148149 liftPromise $ runFn2 crypto.decryptToUtf8 (EncString.fromString input) key
149150
151+encrypt ::
152+ forall r.
153+ String ->
154+ Run
155+ ( key :: Reader SymmetricCryptoKey
156+ , crypto :: Reader CryptoService
157+ , aff :: Aff
158+ , effect :: Effect
159+ | r
160+ )
161+ EncryptedString
162+encrypt input = do
163+ crypto <- askAt (Proxy :: _ "crypto")
164+ key <- askAt (Proxy :: _ "key")
165+ map EncString.toString $ liftPromise $ runFn2 crypto.encrypt input key
166+
150167liftPromise ∷ forall m a. MonadAff m ⇒ Promise a → m a
151168liftPromise = liftAff <<< Promise.toAff
152169
170+encodeCipher ::
171+ forall r.
172+ Bridge.FullCipher ->
173+ Run
174+ ( key :: Reader SymmetricCryptoKey
175+ , crypto :: Reader CryptoService
176+ , aff :: Aff
177+ , effect :: Effect
178+ | r
179+ )
180+ CipherResponse
181+encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
182+ let
183+ f :: CipherResponse -> Run _ CipherResponse
184+ f x = case cipher of
185+ Bridge.CardCipher (Bridge.Cipher_CardCipher c) -> do
186+ cardholderName <- encryptNullable c.cardholderName
187+ brand <- encryptNullable c.brand
188+ number <- encryptNullable c.number
189+ expMonth <- encryptNullable c.expMonth
190+ expYear <- encryptNullable c.expYear
191+ code <- encryptNullable c.code
192+ pure
193+ x
194+ { card =
195+ nullify
196+ { cardholderName
197+ , brand
198+ , number
199+ , expMonth
200+ , expYear
201+ , code
202+ }
203+ , type = cipherTypeCard
204+ }
205+ Bridge.IdentityCipher (Bridge.Cipher_IdentityCipher identity) -> do
206+ address1 <- encryptNullable identity.address1
207+ address2 <- encryptNullable identity.address2
208+ address3 <- encryptNullable identity.address3
209+ city <- encryptNullable identity.city
210+ company <- encryptNullable identity.company
211+ country <- encryptNullable identity.country
212+ email <- encryptNullable identity.email
213+ firstName <- encryptNullable identity.firstName
214+ lastName <- encryptNullable identity.lastName
215+ licenseNumber <- encryptNullable identity.licenseNumber
216+ middleName <- encryptNullable identity.middleName
217+ passportNumber <- encryptNullable identity.passportNumber
218+ phone <- encryptNullable identity.phone
219+ postalCode <- encryptNullable identity.postalCode
220+ ssn <- encryptNullable identity.ssn
221+ state <- encryptNullable identity.state
222+ title <- encryptNullable identity.title
223+ username <- encryptNullable identity.username
224+ pure
225+ x
226+ { identity =
227+ nullify
228+ { address1
229+ , address2
230+ , address3
231+ , city
232+ , company
233+ , country
234+ , email
235+ , firstName
236+ , lastName
237+ , licenseNumber
238+ , middleName
239+ , passportNumber
240+ , phone
241+ , postalCode
242+ , ssn
243+ , state
244+ , title
245+ , username
246+ }
247+ , type = cipherTypeIdentity
248+ }
249+ Bridge.LoginCipher (Bridge.Cipher_LoginCipher login) -> do
250+ password <- encryptNullable login.password
251+ uris <-
252+ traverse
253+ ( \uri -> do
254+ uriEnc <- encrypt uri
255+ pure { uri: uriEnc, match: jnull }
256+ )
257+ (unwrap login.uris)
258+ username <- encryptNullable login.username
259+ pure
260+ x
261+ { login =
262+ nullify
263+ { password
264+ , uris: JOpt $ opt uris
265+ , username
266+ , passwordRevisionDate: jnull
267+ , totp: jnull
268+ , autofillOnPageLoad: JOpt undefined
269+ }
270+ , type = cipherTypeLogin
271+ }
272+ Bridge.NoteCipher note -> do
273+ encNote <- encrypt note
274+ pure
275+ x
276+ { notes = nullify encNote
277+ , type = cipherTypeSecureNote
278+ }
279+ encName <- encrypt name
280+ f
281+ { id
282+ , organizationId: jnull
283+ , folderId: jnull
284+ , type: 0
285+ , name: encName
286+ , notes: jnull
287+ , fields: jnull
288+ , login: jnull
289+ , card: jnull
290+ , identity: jnull
291+ , secureNote: jnull
292+ , favorite: favorite
293+ , edit: jnull
294+ , viewPassword: jnull
295+ , organizationUseTotp: jnull
296+ , revisionDate: jnull
297+ , attachments: jnull
298+ , passwordHistory: jnull
299+ , collectionIds: jnull
300+ , deletedDate: jnull
301+ , reprompt
302+ }
303+
153304decodeCipher ::
154305 forall r.
155306 CipherResponse ->
@@ -160,7 +311,7 @@ decodeCipher ::
160311 , effect :: Effect
161312 | r
162313 )
163− Bridge.Sub_LoadCipher
314+ Bridge.FullCipher
164315decodeCipher cipher = do
165316 name <- decrypt cipher.name
166317 cipherType <- case cipher.type of
@@ -242,9 +393,12 @@ decodeCipher cipher = do
242393 }
243394 n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
244395 pure
245− $ Bridge.Sub_LoadCipher
246− { cipher: Bridge.FullCipher { cipher: cipherType, name }
396+ $ wrap
397+ { cipher: cipherType
398+ , name
247399 , id: cipher.id
400+ , favorite: cipher.favorite
401+ , reprompt: cipher.reprompt
248402 }
249403
250404decryptNullable ::
@@ -260,3 +414,19 @@ decryptNullable ::
260414 )
261415 x
262416decryptNullable x = wrap <<< fromJNullable Nothing <<< map Just <$> (traverse decrypt x)
417+
418+encryptNullable ::
419+ forall x r.
420+ Newtype x (Maybe String) =>
421+ x ->
422+ Run
423+ ( key :: Reader SymmetricCryptoKey
424+ , crypto :: Reader CryptoService
425+ , aff :: Aff
426+ , effect :: Effect
427+ | r
428+ )
429+ (JNullable EncryptedString)
430+encryptNullable x = case unwrap x of
431+ Nothing -> pure jnull
432+ Just y -> nullify <$> encrypt y
backend/BW/Types.purs+4−4
@@ -401,10 +401,10 @@ type CipherResponse
401401 , identity :: JNullable IdentityApi
402402 , secureNote :: JNullable SecureNoteApi
403403 , favorite :: Boolean
404− , edit :: Boolean
405− , viewPassword :: Boolean
406− , organizationUseTotp :: Boolean
407− , revisionDate :: Timestamp
404+ , edit :: JNullable Boolean
405+ , viewPassword :: JNullable Boolean
406+ , organizationUseTotp :: JNullable Boolean
407+ , revisionDate :: JNullable Timestamp
408408 , attachments :: JNullable (Array AttachmentResponse)
409409 , passwordHistory :: JNullable (Array PasswordHistoryResponse)
410410 , collectionIds :: JNullable (Array String)
backend/Bridge.purs+6−16
@@ -456,6 +456,7 @@ data Cmd =
456456 | Open String
457457 | RequestCipher String
458458 | SendMasterPassword String
459+ | UpdateCipher FullCipher
459460
460461instance encodeJsonCmd :: EncodeJson Cmd where
461462 encodeJson = genericEncodeAeson Argonaut.defaultOptions
@@ -468,7 +469,10 @@ derive instance ordCmd :: Ord Cmd
468469newtype FullCipher =
469470 FullCipher {
470471 cipher :: Cipher
472+ , favorite :: Boolean
473+ , id :: String
471474 , name :: String
475+ , reprompt :: Int
472476 }
473477
474478derive instance newtypeFullCipher :: Newtype FullCipher _
@@ -480,21 +484,6 @@ derive instance genericFullCipher :: Generic FullCipher _
480484derive instance eqFullCipher :: Eq FullCipher
481485derive instance ordFullCipher :: Ord FullCipher
482486
483−newtype Sub_LoadCipher =
484− Sub_LoadCipher {
485− cipher :: FullCipher
486− , id :: String
487− }
488−
489−derive instance newtypeSub_LoadCipher :: Newtype Sub_LoadCipher _
490−instance encodeJsonSub_LoadCipher :: EncodeJson Sub_LoadCipher where
491− encodeJson = genericEncodeAeson Argonaut.defaultOptions
492−instance decodeJsonSub_LoadCipher :: DecodeJson Sub_LoadCipher where
493− decodeJson = genericDecodeAeson Argonaut.defaultOptions
494−derive instance genericSub_LoadCipher :: Generic Sub_LoadCipher _
495−derive instance eqSub_LoadCipher :: Eq Sub_LoadCipher
496−derive instance ordSub_LoadCipher :: Ord Sub_LoadCipher
497−
498487newtype Sub_LoadCiphers =
499488 Sub_LoadCiphers {
500489 cipherType :: CipherType
@@ -541,8 +530,9 @@ derive instance ordSub_NeedsMasterPassword :: Ord Sub_NeedsMasterPassword
541530
542531data Sub =
543532 CaptchaDone
533+ | CipherChanged FullCipher
544534 | Error String
545− | LoadCipher Sub_LoadCipher
535+ | LoadCipher FullCipher
546536 | LoadCiphers Sub_LoadCiphers_List
547537 | LoginSuccessful
548538 | NeedsCaptcha String
backend/Data/EncString.js+4
@@ -6,6 +6,10 @@ export function fromString(str) {
66 return x
77}
88
9+export function toString(encStr) {
10+ return encStr.encryptedString
11+}
12+
913export function encryptionType(encStr) {
1014 return encStr.encryptionType
1115}
backend/Data/EncString.purs+2
@@ -25,3 +25,5 @@ type EncryptionType
2525 = Int
2626
2727foreign import fromString :: EncryptedString -> EncString
28+
29+foreign import toString :: EncString -> EncryptedString
backend/Main.purs+40−15
@@ -3,9 +3,9 @@ module Main
33 ) where
44
55import Prelude
6−import BW (ApiService, CryptoService, Services, CryptoFunctions)
6+import BW (ApiService, CryptoFunctions, Services, CryptoService)
77import BW as WB
8−import BW.Logic (decodeCipher, decrypt, hashPassword, liftPromise)
8+import BW.Logic (decodeCipher, decrypt, encodeCipher, hashPassword, liftPromise)
99import BW.Logic as Logic
1010import BW.Types (CipherResponse, Email(..), Password(..), Urls, cipherTypeCard, cipherTypeIdentity, cipherTypeLogin, cipherTypeSecureNote)
1111import Bridge as Bridge
@@ -15,9 +15,12 @@ import Data.Array as Array
1515import Data.Clipboard as Clipboard
1616import Data.DateTime (DateTime)
1717import Data.Either (Either(..))
18+import Data.Function.Uncurried (runFn2)
1819import Data.HCaptcha (bindHCaptchToken)
1920import Data.JNullable (jnull, nullify)
20−import Data.Maybe (Maybe(..))
21+import Data.JNullable as JNullable
22+import Data.Maybe (Maybe(..), maybe)
23+import Data.Newtype (unwrap)
2124import Data.OpenURL (openURL)
2225import Data.Ord.Down (Down(..))
2326import Data.SymmetricCryptoKey (SymmetricCryptoKey)
@@ -139,16 +142,7 @@ main = do
139142 Storage.store storage SyncKey sync
140143 Storage.store storage TokenKey token
141144 send Bridge.LoginSuccessful
142− Bridge.NeedCiphersList ->
143− runWithDecryptionKey do
144− sync <- getOrReset SyncKey
145− ciphers <- traverse processCipher sync.ciphers
146− let
147− sortedCiphers = Array.sortWith (_.date >>> Down) ciphers
148− send $ Bridge.LoadCiphers $ Bridge.Sub_LoadCiphers_List $ map _.cipher sortedCiphers
149− api <- getAuthedApi
150− newSync <- liftPromise $ api.getSync unit
151− liftEffect $ Storage.store storage SyncKey newSync
145+ Bridge.NeedCiphersList -> runWithDecryptionKey performSync
152146 Bridge.NeedsReset -> do
153147 WebStorage.clear storage
154148 Elm.send app Bridge.Reset
@@ -190,6 +184,14 @@ main = do
190184 runWithDecryptionKey do
191185 liftPromise $ Clipboard.clipboard text
192186 Bridge.Open uri -> openURL uri
187+ Bridge.UpdateCipher fullCipher ->
188+ runWithDecryptionKey do
189+ api <- getAuthedApi
190+ cipher <- encodeCipher fullCipher
191+ newCipher <- liftPromise (api.putCipher cipher) >>= decodeCipher
192+ send $ Bridge.CipherChanged newCipher
193+ performSync
194+ pure unit
193195
194196processCipher ::
195197 forall r.
@@ -214,12 +216,12 @@ processCipher cipher = do
214216 n
215217 | n == cipherTypeIdentity -> pure Bridge.IdentityType
216218 n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
217− date <- liftEffect $ Timestamp.toDateTime cipher.revisionDate
219+ date <- liftEffect $ maybe (pure bottom) (Timestamp.toDateTime) $ JNullable.toMaybe cipher.revisionDate
218220 pure
219221 $ { cipher:
220222 Bridge.Sub_LoadCiphers
221223 { name: name
222− , date: Timestamp.toLocalDateTimeString cipher.revisionDate
224+ , date: maybe "" Timestamp.toLocalDateTimeString $ JNullable.toMaybe cipher.revisionDate
223225 , id: cipher.id
224226 , cipherType
225227 }
@@ -321,3 +323,26 @@ send ::
321323send sub = do
322324 app <- askAt (Proxy :: _ "app")
323325 liftEffect $ Elm.send app sub
326+
327+performSync ::
328+ forall r.
329+ Run
330+ ( storage :: Reader Storage
331+ , app :: Reader Elm
332+ , aff :: Aff
333+ , effect :: Effect
334+ , services :: Reader Services
335+ , crypto :: Reader CryptoService
336+ , key :: Reader SymmetricCryptoKey
337+ | r
338+ )
339+ Unit
340+performSync = do
341+ api <- getAuthedApi
342+ storage <- askAt (Proxy :: _ "storage")
343+ newSync <- liftPromise $ api.getSync unit
344+ liftEffect $ Storage.store storage SyncKey newSync
345+ ciphers <- traverse processCipher newSync.ciphers
346+ let
347+ sortedCiphers = Array.sortWith (_.date >>> Down) ciphers
348+ send $ Bridge.LoadCiphers $ Bridge.Sub_LoadCiphers_List $ map _.cipher sortedCiphers
bridge.yaml+6−3
@@ -14,10 +14,9 @@ Sub:
1414 server: String
1515 login: String
1616- Reset
17−- LoadCipher:
18− id: String
19− cipher: FullCipher
17+- LoadCipher: FullCipher
2018- RecieveEmail: String
19+- CipherChanged: FullCipher
2120
2221Cmd:
2322- Init
@@ -32,8 +31,12 @@ Cmd:
3231- NeedEmail
3332- Copy: String
3433- Open: String
34+- UpdateCipher: FullCipher
3535
3636FullCipher:
37+ reprompt: Int
38+ favorite: Bool
39+ id: String
3740 name: String
3841 cipher: Cipher
3942
frontend/Bridge.elm+19−24
@@ -510,6 +510,7 @@ type Cmd =
510510 | Open String
511511 | RequestCipher String
512512 | SendMasterPassword String
513+ | UpdateCipher FullCipher
513514
514515jsonDecCmd : Json.Decode.Decoder ( Cmd )
515516jsonDecCmd =
@@ -523,6 +524,7 @@ jsonDecCmd =
523524 , ("Open", Json.Decode.lazy (\_ -> Json.Decode.map Open (Json.Decode.string)))
524525 , ("RequestCipher", Json.Decode.lazy (\_ -> Json.Decode.map RequestCipher (Json.Decode.string)))
525526 , ("SendMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map SendMasterPassword (Json.Decode.string)))
527+ , ("UpdateCipher", Json.Decode.lazy (\_ -> Json.Decode.map UpdateCipher (jsonDecFullCipher)))
526528 ]
527529 jsonDecObjectSetCmd = Set.fromList []
528530 in decodeSumTaggedObject "Cmd" "tag" "contents" jsonDecDictCmd jsonDecObjectSetCmd
@@ -539,46 +541,36 @@ jsonEncCmd val =
539541 Open v1 -> ("Open", encodeValue (Json.Encode.string v1))
540542 RequestCipher v1 -> ("RequestCipher", encodeValue (Json.Encode.string v1))
541543 SendMasterPassword v1 -> ("SendMasterPassword", encodeValue (Json.Encode.string v1))
544+ UpdateCipher v1 -> ("UpdateCipher", encodeValue (jsonEncFullCipher v1))
542545 in encodeSumTaggedObject "tag" "contents" keyval val
543546
544547
545548
546549type alias FullCipher =
547550 { cipher: Cipher
551+ , favorite: Bool
552+ , id: String
548553 , name: String
554+ , reprompt: Int
549555 }
550556
551557jsonDecFullCipher : Json.Decode.Decoder ( FullCipher )
552558jsonDecFullCipher =
553− Json.Decode.succeed (\pcipher pname -> {cipher = pcipher, name = pname})
559+ Json.Decode.succeed (\pcipher pfavorite pid pname preprompt -> {cipher = pcipher, favorite = pfavorite, id = pid, name = pname, reprompt = preprompt})
554560 |> required "cipher" (jsonDecCipher)
561+ |> required "favorite" (Json.Decode.bool)
562+ |> required "id" (Json.Decode.string)
555563 |> required "name" (Json.Decode.string)
564+ |> required "reprompt" (Json.Decode.int)
556565
557566jsonEncFullCipher : FullCipher -> Value
558567jsonEncFullCipher val =
559568 Json.Encode.object
560569 [ ("cipher", jsonEncCipher val.cipher)
561− , ("name", Json.Encode.string val.name)
562− ]
563−
564−
565−
566−type alias Sub_LoadCipher =
567− { cipher: FullCipher
568− , id: String
569− }
570−
571−jsonDecSub_LoadCipher : Json.Decode.Decoder ( Sub_LoadCipher )
572−jsonDecSub_LoadCipher =
573− Json.Decode.succeed (\pcipher pid -> {cipher = pcipher, id = pid})
574− |> required "cipher" (jsonDecFullCipher)
575− |> required "id" (Json.Decode.string)
576−
577−jsonEncSub_LoadCipher : Sub_LoadCipher -> Value
578−jsonEncSub_LoadCipher val =
579− Json.Encode.object
580− [ ("cipher", jsonEncFullCipher val.cipher)
570+ , ("favorite", Json.Encode.bool val.favorite)
581571 , ("id", Json.Encode.string val.id)
572+ , ("name", Json.Encode.string val.name)
573+ , ("reprompt", Json.Encode.int val.reprompt)
582574 ]
583575
584576
@@ -642,8 +634,9 @@ jsonEncSub_NeedsMasterPassword val =
642634
643635type Sub =
644636 CaptchaDone
637+ | CipherChanged FullCipher
645638 | Error String
646− | LoadCipher Sub_LoadCipher
639+ | LoadCipher FullCipher
647640 | LoadCiphers Sub_LoadCiphers_List
648641 | LoginSuccessful
649642 | NeedsCaptcha String
@@ -656,8 +649,9 @@ jsonDecSub : Json.Decode.Decoder ( Sub )
656649jsonDecSub =
657650 let jsonDecDictSub = Dict.fromList
658651 [ ("CaptchaDone", Json.Decode.lazy (\_ -> Json.Decode.succeed CaptchaDone))
652+ , ("CipherChanged", Json.Decode.lazy (\_ -> Json.Decode.map CipherChanged (jsonDecFullCipher)))
659653 , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
660− , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecSub_LoadCipher)))
654+ , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecFullCipher)))
661655 , ("LoadCiphers", Json.Decode.lazy (\_ -> Json.Decode.map LoadCiphers (jsonDecSub_LoadCiphers_List)))
662656 , ("LoginSuccessful", Json.Decode.lazy (\_ -> Json.Decode.succeed LoginSuccessful))
663657 , ("NeedsCaptcha", Json.Decode.lazy (\_ -> Json.Decode.map NeedsCaptcha (Json.Decode.string)))
@@ -673,8 +667,9 @@ jsonEncSub : Sub -> Value
673667jsonEncSub val =
674668 let keyval v = case v of
675669 CaptchaDone -> ("CaptchaDone", encodeValue (Json.Encode.list identity []))
670+ CipherChanged v1 -> ("CipherChanged", encodeValue (jsonEncFullCipher v1))
676671 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
677− LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncSub_LoadCipher v1))
672+ LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncFullCipher v1))
678673 LoadCiphers v1 -> ("LoadCiphers", encodeValue (jsonEncSub_LoadCiphers_List v1))
679674 LoginSuccessful -> ("LoginSuccessful", encodeValue (Json.Encode.list identity []))
680675 NeedsCaptcha v1 -> ("NeedsCaptcha", encodeValue (Json.Encode.string v1))
frontend/GlobalEvents.elmnew file+7
@@ -0,0 +1,7 @@
1+module GlobalEvents exposing (..)
2+
3+import Bridge
4+
5+
6+type Event
7+ = UpdateCipher Bridge.FullCipher
frontend/Logic/Cipher.elmnew file+77
@@ -0,0 +1,77 @@
1+module Logic.Cipher exposing (normalizeCipher)
2+
3+import Bridge
4+
5+
6+normalizeCipher : Bridge.Cipher -> Bridge.Cipher
7+normalizeCipher x =
8+ case x of
9+ Bridge.CardCipher c ->
10+ Bridge.CardCipher (normalizeCardCipher c)
11+
12+ Bridge.IdentityCipher c ->
13+ Bridge.IdentityCipher (normalizeIdentityCipher c)
14+
15+ Bridge.LoginCipher c ->
16+ Bridge.LoginCipher (normalizeLoginCipher c)
17+
18+ Bridge.NoteCipher c ->
19+ Bridge.NoteCipher c
20+
21+
22+normalizeCardCipher : Bridge.Cipher_CardCipher -> Bridge.Cipher_CardCipher
23+normalizeCardCipher { brand, cardholderName, code, expMonth, expYear, number } =
24+ { brand = normalizeStringMaybe brand
25+ , cardholderName = normalizeStringMaybe cardholderName
26+ , code = normalizeStringMaybe code
27+ , expMonth = normalizeStringMaybe expMonth
28+ , expYear = normalizeStringMaybe expYear
29+ , number = normalizeStringMaybe number
30+ }
31+
32+
33+normalizeIdentityCipher : Bridge.Cipher_IdentityCipher -> Bridge.Cipher_IdentityCipher
34+normalizeIdentityCipher { address1, address2, address3, city, company, country, email, firstName, lastName, licenseNumber, middleName, passportNumber, phone, postalCode, ssn, state, title, username } =
35+ { address1 = normalizeStringMaybe address1
36+ , address2 = normalizeStringMaybe address2
37+ , address3 = normalizeStringMaybe address3
38+ , city = normalizeStringMaybe city
39+ , company = normalizeStringMaybe company
40+ , country = normalizeStringMaybe country
41+ , email = normalizeStringMaybe email
42+ , firstName = normalizeStringMaybe firstName
43+ , lastName = normalizeStringMaybe lastName
44+ , licenseNumber = normalizeStringMaybe licenseNumber
45+ , middleName = normalizeStringMaybe middleName
46+ , passportNumber = normalizeStringMaybe passportNumber
47+ , phone = normalizeStringMaybe phone
48+ , postalCode = normalizeStringMaybe postalCode
49+ , ssn = normalizeStringMaybe ssn
50+ , state = normalizeStringMaybe state
51+ , title = normalizeStringMaybe title
52+ , username = username
53+ }
54+
55+
56+normalizeLoginCipher : Bridge.Cipher_LoginCipher -> Bridge.Cipher_LoginCipher
57+normalizeLoginCipher { password, uris, username } =
58+ { password = password
59+ , uris = uris |> List.map String.trim |> List.filter String.isEmpty
60+ , username = username
61+ }
62+
63+
64+normalizeStringMaybe : Maybe String -> Maybe String
65+normalizeStringMaybe =
66+ Maybe.andThen
67+ (\x ->
68+ let
69+ y =
70+ String.trim x
71+ in
72+ if String.isEmpty y then
73+ Nothing
74+
75+ else
76+ Just y
77+ )
frontend/Main.elm+97−8
@@ -3,6 +3,7 @@ module Main exposing (..)
33import Bridge
44import Browser
55import FFI exposing (getBridge, sendBridge)
6+import GlobalEvents exposing (Event)
67import Html exposing (..)
78import Html.Lazy as Lazy
89import List.Nonempty as Nonempty exposing (Nonempty(..))
@@ -17,6 +18,7 @@ import Pages.MasterPassword as MasterPassword
1718import Pages.Navigation as Navigation exposing (TopButton(..))
1819import Task
1920import Time
21+import Types exposing (CipherId)
2022import Utils exposing (..)
2123
2224
@@ -30,6 +32,7 @@ type Msg
3032 | RecieveMessage String
3133 | ShowLoginPage
3234 | ShowError String
35+ | ShowInfo String String
3336 | CloseNotification
3437 | SubmitLogin { email : String, password : String, server : String }
3538 | LoginMsg Login.Msg
@@ -42,7 +45,7 @@ type Msg
4245 | SendMasterPassword { password : String }
4346 | ShowMasterPasswordPage { server : String, login : String }
4447 | CipherMsg Cipher.Msg
45− | ShowCipherPage Bridge.Sub_LoadCipher
48+ | ShowCipherPage Bridge.FullCipher
4649 | RequestCipher CipherId
4750 | PopView
4851 | RecieveEmail String
@@ -54,10 +57,8 @@ type Msg
5457 | ClearNotification { currentTime : Time.Posix }
5558 | UpdateLastNotificatioTime Time.Posix
5659 | EditCipher Bridge.FullCipher
57−
58−
59−type alias CipherId =
60− String
60+ | UpdateCipher Bridge.FullCipher
61+ | FireGlobalEvent Event
6162
6263
6364type alias Model =
@@ -213,6 +214,9 @@ subscriptions model =
213214
214215 Bridge.CaptchaDone ->
215216 PopView
217+
218+ Bridge.CipherChanged c ->
219+ ShowInfo "Entry updated" ("The entry \"" ++ c.name ++ "\" has been successfully updated.")
216220 )
217221 ]
218222 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
@@ -304,6 +308,14 @@ update msg model =
304308 }
305309 model
306310
311+ ShowInfo title inf ->
312+ appendNotification
313+ { title = title
314+ , message = inf
315+ , severity = Notification.Info
316+ }
317+ model
318+
307319 CloseNotification ->
308320 ( { model | notifications = tailEmpty model.notifications }
309321 , Cmd.none
@@ -335,9 +347,45 @@ update msg model =
335347 |> Tuple.mapFirst (\pageModel -> appendPageStack <| LoginModel pageModel)
336348
337349 LoadCiphers ciphers ->
338− (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
339− |> Tuple.mapFirst (\pageModel -> appendPageStack <| CiphersModel pageModel)
350+ let
351+ ciphersShown =
352+ model.pageStack
353+ |> Nonempty.toList
354+ |> List.any
355+ (\x ->
356+ case x of
357+ CiphersModel _ ->
358+ True
359+
360+ _ ->
361+ False
362+ )
363+
364+ ( pageModel, cmd ) =
365+ (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
366+ in
367+ ( if ciphersShown then
368+ { model
369+ | pageStack =
370+ model.pageStack
371+ |> Nonempty.map
372+ (\x ->
373+ case x of
374+ CiphersModel _ ->
375+ CiphersModel pageModel
376+
377+ other ->
378+ other
379+ )
380+ }
340381
382+ else
383+ appendPageStack <| CiphersModel pageModel
384+ , cmd
385+ )
386+
387+ -- (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
388+ -- |> Tuple.mapFirst (\pageModel -> appendPageStack <| CiphersModel pageModel)
341389 OpenCiphersScreen ->
342390 ( appendPageStack <| LoadingPage, Cmd.batch [ sendBridge Bridge.NeedCiphersList, sendBridge Bridge.NeedEmail ] )
343391
@@ -430,6 +478,47 @@ update msg model =
430478 (EditCipher.page editCipherCallbacks EditCipherMsg).init cipher
431479 |> Tuple.mapFirst (\pageModel -> keepStackWith <| EditCipherModel pageModel)
432480
481+ UpdateCipher cipher ->
482+ ( model
483+ , Cmd.batch
484+ [ Bridge.UpdateCipher cipher |> sendBridge
485+ , cipher |> GlobalEvents.UpdateCipher |> FireGlobalEvent |> pureCmd
486+ , PopView |> pureCmd
487+ ]
488+ )
489+
490+ FireGlobalEvent ev ->
491+ ( { model
492+ | pageStack =
493+ model.pageStack
494+ |> Nonempty.map
495+ (\page ->
496+ case page of
497+ LoadingPage ->
498+ LoadingPage
499+
500+ LoginModel m ->
501+ (Login.page loginCallbacks LoginMsg).event m ev |> LoginModel
502+
503+ CiphersModel m ->
504+ (Ciphers.page ciphersCallbacks CiphersMsg).event m ev |> CiphersModel
505+
506+ MasterPasswordModel m ->
507+ (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).event m ev |> MasterPasswordModel
508+
509+ CipherModel m ->
510+ (Cipher.page cipherCallbacks CipherMsg).event m ev |> CipherModel
511+
512+ EditCipherModel m ->
513+ (EditCipher.page editCipherCallbacks EditCipherMsg).event m ev |> EditCipherModel
514+
515+ CaptchaModel m ->
516+ (Captcha.page captchaCallbacks CaptchaMsg).event m ev |> CaptchaModel
517+ )
518+ }
519+ , Cmd.none
520+ )
521+
433522
434523loginCallbacks : Login.Callbacks Msg
435524loginCallbacks =
@@ -448,7 +537,7 @@ cipherCallbacks =
448537
449538editCipherCallbacks : EditCipher.Callbacks Msg
450539editCipherCallbacks =
451− { save = always Noop }
540+ { save = UpdateCipher }
452541
453542
454543captchaCallbacks : Captcha.Callbacks
frontend/Page.elm+2
@@ -1,5 +1,6 @@
11module Page exposing (..)
22
3+import GlobalEvents exposing (Event)
34import Html exposing (..)
45import Html.Events exposing (..)
56
@@ -19,4 +20,5 @@ type alias InitializedPage init model imsg emsg =
1920 , update : imsg -> model -> ( Result String model, Cmd emsg )
2021 , subscriptions : model -> Sub emsg
2122 , title : model -> List (Html emsg)
23+ , event : model -> Event -> model
2224 }
frontend/Pages/Captcha.elm+1
@@ -30,6 +30,7 @@ page callbacks liftMsg =
3030 , update = \msg model -> update callbacks liftMsg msg model
3131 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
3232 , title = always [ text "Captcha" ]
33+ , event = \model _ -> model
3334 }
3435
3536
frontend/Pages/Cipher.elm+24−8
@@ -1,6 +1,7 @@
11module Pages.Cipher exposing (..)
22
33import Bridge
4+import GlobalEvents
45import Html exposing (..)
56import Html.Attributes as Attr
67import Html.Events as Ev
@@ -9,7 +10,7 @@ import Utils exposing (..)
910
1011
1112type alias Model =
12− { cipher : Bridge.Sub_LoadCipher
13+ { cipher : Bridge.FullCipher
1314 , passwordHidden : Bool
1415 , cvvHidden : Bool
1516 }
@@ -23,24 +24,39 @@ type Msg
2324
2425
2526type alias Callbacks msg =
26− { copy : String -> msg, open : String -> msg, edit : Bridge.FullCipher -> msg }
27+ { copy : String -> msg
28+ , open : String -> msg
29+ , edit : Bridge.FullCipher -> msg
30+ }
2731
2832
29−page : Callbacks emsg -> Page Bridge.Sub_LoadCipher Model Msg emsg
33+page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
3034page callbacks liftMsg =
31− { init = \cipher -> Tuple.mapSecond (Cmd.map liftMsg) (init cipher)
35+ { init = \data -> Tuple.mapSecond (Cmd.map liftMsg) (init data)
3236 , view = \model -> view model |> List.map (Html.map liftMsg)
3337 , update = \msg model -> update callbacks liftMsg msg model
3438 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
3539 , title =
3640 \{ cipher } ->
37− [ text cipher.cipher.name
38− , span [ Attr.class "u-float-right" ] [ iconButton "edit" (callbacks.edit cipher.cipher) ]
41+ [ text cipher.name
42+ , span [ Attr.class "u-float-right" ] [ iconButton "edit" (callbacks.edit cipher) ]
3943 ]
44+ , event =
45+ \model ev ->
46+ case ev of
47+ GlobalEvents.UpdateCipher c ->
48+ { model
49+ | cipher =
50+ if c.id == model.cipher.id then
51+ c
52+
53+ else
54+ model.cipher
55+ }
4056 }
4157
4258
43−init : Bridge.Sub_LoadCipher -> ( Model, Cmd Msg )
59+init : Bridge.FullCipher -> ( Model, Cmd Msg )
4460init cipher =
4561 ( { cipher = cipher
4662 , passwordHidden = True
@@ -83,7 +99,7 @@ row { name, value, nameIcon, icons } =
8399
84100view : Model -> List (Html Msg)
85101view { passwordHidden, cipher, cvvHidden } =
86− (case cipher.cipher.cipher of
102+ (case cipher.cipher of
87103 Bridge.LoginCipher { username, password, uris } ->
88104 maybeList username
89105 (\x ->
frontend/Pages/Ciphers.elm+17
@@ -1,6 +1,7 @@
11module Pages.Ciphers exposing (Callbacks, Model, Msg, menuConfig, page)
22
33import Bridge
4+import GlobalEvents
45import Html exposing (..)
56import Html.Attributes as Attr
67import Html.Events as Ev
@@ -118,6 +119,22 @@ page callbacks liftMsg =
118119 "Notes"
119120 )
120121 ]
122+ , event =
123+ \model ev ->
124+ case ev of
125+ GlobalEvents.UpdateCipher c ->
126+ { model
127+ | ciphers =
128+ model.ciphers
129+ |> List.map
130+ (\cipher ->
131+ if cipher.id == c.id then
132+ { cipher | name = c.name }
133+
134+ else
135+ cipher
136+ )
137+ }
121138 }
122139
123140
frontend/Pages/EditCipher.elm+15−1
@@ -1,10 +1,12 @@
11module Pages.EditCipher exposing (..)
22
33import Bridge
4+import GlobalEvents
45import Html exposing (..)
56import Html.Attributes as Attr
67import Html.Events as Ev
78import Page exposing (..)
9+import Types exposing (CipherId)
810import Utils exposing (..)
911
1012
@@ -23,7 +25,7 @@ type alias Callbacks msg =
2325
2426page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
2527page callbacks liftMsg =
26− { init = \cipher -> Tuple.mapSecond (Cmd.map liftMsg) (init cipher)
28+ { init = \data -> Tuple.mapSecond (Cmd.map liftMsg) (init data)
2729 , view = \model -> view model |> List.map (Html.map liftMsg)
2830 , update = \msg model -> update callbacks liftMsg msg model
2931 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
@@ -32,6 +34,18 @@ page callbacks liftMsg =
3234 [ text fullCipher.name
3335 , span [ Attr.class "u-float-right" ] [ iconButton "task-outstanding" (callbacks.save fullCipher) ]
3436 ]
37+ , event =
38+ \model ev ->
39+ case ev of
40+ GlobalEvents.UpdateCipher c ->
41+ { model
42+ | fullCipher =
43+ if c.id == model.fullCipher.id then
44+ c
45+
46+ else
47+ model.fullCipher
48+ }
3549 }
3650
3751
frontend/Pages/Login.elm+1−1
@@ -33,6 +33,7 @@ page callbacks liftMsg =
3333 , update = \msg model -> update callbacks liftMsg msg model
3434 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
3535 , title = always [ text "Log into bitwarden" ]
36+ , event = \model _ -> model
3637 }
3738
3839
@@ -98,7 +99,6 @@ view model =
9899 ]
99100 []
100101 ]
101− -- ++ maybeList model.siteKey (\siteKey -> iframe [ Attr.src ("https://iko.soy/warden/hcaptcha.html?" ++ siteKey) ] [])
102102 ++ [ alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ] ]
103103 )
104104 ]
frontend/Pages/MasterPassword.elm+1
@@ -33,6 +33,7 @@ page callbacks liftMsg =
3333 , update = \msg model -> update callbacks liftMsg msg model
3434 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
3535 , title = always [ text "Enter master password" ]
36+ , event = \model _ -> model
3637 }
3738
3839
frontend/Pages/Navigation.elm+4−1
@@ -142,7 +142,10 @@ pushView model stack =
142142
143143popView : PageStack model -> PageStack model
144144popView stack =
145− Nonempty.tail stack |> Nonempty.fromList |> Maybe.withDefault stack
145+ stack
146+ |> Nonempty.tail
147+ |> Nonempty.fromList
148+ |> Maybe.withDefault stack
146149
147150
148151showNavigationView :
frontend/Types.elmnew file+5
@@ -0,0 +1,5 @@
1+module Types exposing (..)
2+
3+
4+type alias CipherId =
5+ String