on master help sign in
Implemented TOTP (#12)

* totp * Got totp working

commit4f8e87887795d6348912c35628ebe6789b6efe00 authoriko <ilyakooo0@gmail.com> dateFri 26 Aug 2022, 16:16 (4 years ago) parent5342153
12 files changed+237−18
backend/BW.js+8−1 backend/BW.purs+6 backend/BW/Logic.purs+4−1 backend/Bridge.purs+31 backend/Main.purs+12−1 bridge.yaml+7 frontend/Bridge.elm+44−1 frontend/GlobalEvents.elm+1 frontend/Logic/Cipher.elm+2−1 frontend/Main.elm+36−1 frontend/Pages/Cipher.elm+72−11 frontend/Pages/EditCipher.elm+14−1
backend/BW.js+8−1
@@ -21,6 +21,7 @@ import { CipherCreateRequest } from "../../deps/bw/libs/common/src/models/reques
2121import { Cipher } from "../../deps/bw/libs/common/src/models/domain/cipher";
2222import { CipherData } from "../../deps/bw/libs/common/src/models/data/cipherData";
2323import { PasswordGenerationService } from "../../deps/bw/libs/common/src/services/passwordGeneration.service";
24+import { TotpService } from "../../deps/bw/libs/shared/dist/src/services/totp.service"
2425
2526function sanitize(obj) {
2627 return JSON.parse(JSON.stringify(obj));
@@ -84,7 +85,8 @@ export function getServices() {
8485 },
8586 crypto: bg.cryptoService,
8687 cryptoFunctions: bg.cryptoFunctionService,
87− passwordGeneration: bg.passwordGenerationService
88+ passwordGeneration: bg.passwordGenerationService,
89+ totpService: bg.totpService,
8890 }
8991}
9092
@@ -175,6 +177,11 @@ class MainBackground {
175177 this.policyService,
176178 this.stateService
177179 );
180+ this.totpService = new TotpService(
181+ this.cryptoFunctionService,
182+ this.logService,
183+ this.stateService
184+ );
178185 }
179186
180187 async bootstrap() {
backend/BW.purs+6
@@ -17,6 +17,11 @@ import Effect (Effect)
1717import Effect.Unsafe (unsafePerformEffect)
1818import Untagged.Union (type (|+|))
1919
20+type TotpService
21+ = { getCode :: String -> Promise String
22+ , getTimeInterval :: String -> Int
23+ }
24+
2025type CryptoService
2126 = { makeKey :: Fn4 Password String KDF Int (Promise SymmetricCryptoKey)
2227 , hashPassword :: Fn3 Password SymmetricCryptoKey (JNullable HashPurpose) (Promise StringHash)
@@ -85,6 +90,7 @@ type Services
8590 , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
8691 , cryptoFunctions :: CryptoFunctions
8792 , passwordGeneration :: PasswordGeneration
93+ , totpService :: TotpService
8894 }
8995
9096foreign import getServices :: Effect Services
backend/BW/Logic.purs+4−1
@@ -256,6 +256,7 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
256256 )
257257 (unwrap login.uris)
258258 username <- encryptNullable login.username
259+ totp <- encryptNullable login.totp
259260 pure
260261 x
261262 { login =
@@ -264,7 +265,7 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
264265 , uris: JOpt $ opt uris
265266 , username
266267 , passwordRevisionDate: jnull
267− , totp: jnull
268+ , totp: totp
268269 , autofillOnPageLoad: JOpt undefined
269270 }
270271 , type = cipherTypeLogin
@@ -326,12 +327,14 @@ decodeCipher cipher = do
326327 Just login -> do
327328 username <- decryptNullable login.username
328329 password <- decryptNullable login.password
330+ totp <- decryptNullable login.totp
329331 uris <- fromJOpt [] <$> ((traverse >>> traverse) (_.uri >>> decrypt) login.uris)
330332 pure $ Bridge.LoginCipher
331333 $ Bridge.Cipher_LoginCipher
332334 { username
333335 , password
334336 , uris: wrap uris
337+ , totp
335338 }
336339 n
337340 | cipherTypeCard == n -> do
backend/Bridge.purs+31
@@ -362,6 +362,18 @@ derive instance genericCipher_LoginCipher_password_Maybe :: Generic Cipher_Login
362362derive instance eqCipher_LoginCipher_password_Maybe :: Eq Cipher_LoginCipher_password_Maybe
363363derive instance ordCipher_LoginCipher_password_Maybe :: Ord Cipher_LoginCipher_password_Maybe
364364
365+newtype Cipher_LoginCipher_totp_Maybe =
366+ Cipher_LoginCipher_totp_Maybe (Maybe String)
367+
368+derive instance newtypeCipher_LoginCipher_totp_Maybe :: Newtype Cipher_LoginCipher_totp_Maybe _
369+instance encodeJsonCipher_LoginCipher_totp_Maybe :: EncodeJson Cipher_LoginCipher_totp_Maybe where
370+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
371+instance decodeJsonCipher_LoginCipher_totp_Maybe :: DecodeJson Cipher_LoginCipher_totp_Maybe where
372+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
373+derive instance genericCipher_LoginCipher_totp_Maybe :: Generic Cipher_LoginCipher_totp_Maybe _
374+derive instance eqCipher_LoginCipher_totp_Maybe :: Eq Cipher_LoginCipher_totp_Maybe
375+derive instance ordCipher_LoginCipher_totp_Maybe :: Ord Cipher_LoginCipher_totp_Maybe
376+
365377newtype Cipher_LoginCipher_uris_List =
366378 Cipher_LoginCipher_uris_List (Array String)
367379
@@ -389,6 +401,7 @@ derive instance ordCipher_LoginCipher_username_Maybe :: Ord Cipher_LoginCipher_u
389401newtype Cipher_LoginCipher =
390402 Cipher_LoginCipher {
391403 password :: Cipher_LoginCipher_password_Maybe
404+ , totp :: Cipher_LoginCipher_totp_Maybe
392405 , uris :: Cipher_LoginCipher_uris_List
393406 , username :: Cipher_LoginCipher_username_Maybe
394407 }
@@ -458,6 +471,7 @@ data Cmd =
458471 | NeedsReset
459472 | Open String
460473 | RequestCipher String
474+ | RequestTotp String
461475 | SendMasterPassword String
462476 | UpdateCipher FullCipher
463477
@@ -559,6 +573,22 @@ derive instance genericSub_NeedsMasterPassword :: Generic Sub_NeedsMasterPasswor
559573derive instance eqSub_NeedsMasterPassword :: Eq Sub_NeedsMasterPassword
560574derive instance ordSub_NeedsMasterPassword :: Ord Sub_NeedsMasterPassword
561575
576+newtype Sub_Totp =
577+ Sub_Totp {
578+ code :: String
579+ , interval :: Int
580+ , source :: String
581+ }
582+
583+derive instance newtypeSub_Totp :: Newtype Sub_Totp _
584+instance encodeJsonSub_Totp :: EncodeJson Sub_Totp where
585+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
586+instance decodeJsonSub_Totp :: DecodeJson Sub_Totp where
587+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
588+derive instance genericSub_Totp :: Generic Sub_Totp _
589+derive instance eqSub_Totp :: Eq Sub_Totp
590+derive instance ordSub_Totp :: Ord Sub_Totp
591+
562592data Sub =
563593 CaptchaDone
564594 | CipherChanged FullCipher
@@ -573,6 +603,7 @@ data Sub =
573603 | NeedsMasterPassword Sub_NeedsMasterPassword
574604 | RecieveEmail String
575605 | Reset
606+ | Totp Sub_Totp
576607 | WrongPassword
577608
578609instance encodeJsonSub :: EncodeJson Sub where
backend/Main.purs+12−1
@@ -206,13 +206,24 @@ main = do
206206 send $ Bridge.CipherChanged newCipher
207207 performSync
208208 pure unit
209− Bridge.DeleteCipher c@(Bridge.FullCipher { id, name }) ->
209+ Bridge.DeleteCipher c@(Bridge.FullCipher { id }) ->
210210 runWithDecryptionKey do
211211 api <- getAuthedApi
212212 liftPromise $ api.deleteCipher id
213213 send $ Bridge.CipherDeleted c
214214 performSync
215215 pure unit
216+ Bridge.RequestTotp totp ->
217+ runWithDecryptionKey do
218+ { totpService } <- askAt (Proxy :: _ "services")
219+ code <- liftPromise $ totpService.getCode totp
220+ let
221+ interval = totpService.getTimeInterval totp
222+ send $ Bridge.Totp
223+ $ Bridge.Sub_Totp
224+ { interval, code, source: totp
225+ }
226+ pure unit
216227
217228processCipher ::
218229 forall r.
bridge.yaml+7
@@ -20,6 +20,10 @@ Sub:
2020- CipherChanged: FullCipher
2121- GeneratedPassword: String
2222- CipherDeleted: FullCipher
23+- Totp:
24+ source: String
25+ code: String
26+ interval: Int
2327
2428Cmd:
2529- Init
@@ -38,6 +42,7 @@ Cmd:
3842- CreateCipher: FullCipher
3943- GeneratePassword: PasswordGeneratorConfig
4044- DeleteCipher: FullCipher
45+- RequestTotp: String
4146
4247FullCipher:
4348 reprompt: Int
@@ -53,6 +58,8 @@ Cipher:
5358 Maybe: String
5459 password:
5560 Maybe: String
61+ totp:
62+ Maybe: String
5663- NoteCipher: String
5764- CardCipher:
5865 cardholderName:
frontend/Bridge.elm+44−1
@@ -383,6 +383,17 @@ jsonEncCipher_LoginCipher_password_Maybe val = (maybeEncode (Json.Encode.string
383383
384384
385385
386+type alias Cipher_LoginCipher_totp_Maybe = (Maybe String)
387+
388+jsonDecCipher_LoginCipher_totp_Maybe : Json.Decode.Decoder ( Cipher_LoginCipher_totp_Maybe )
389+jsonDecCipher_LoginCipher_totp_Maybe =
390+ Json.Decode.maybe (Json.Decode.string)
391+
392+jsonEncCipher_LoginCipher_totp_Maybe : Cipher_LoginCipher_totp_Maybe -> Value
393+jsonEncCipher_LoginCipher_totp_Maybe val = (maybeEncode (Json.Encode.string)) val
394+
395+
396+
386397type alias Cipher_LoginCipher_uris_List = (List String)
387398
388399jsonDecCipher_LoginCipher_uris_List : Json.Decode.Decoder ( Cipher_LoginCipher_uris_List )
@@ -407,14 +418,16 @@ jsonEncCipher_LoginCipher_username_Maybe val = (maybeEncode (Json.Encode.string
407418
408419type alias Cipher_LoginCipher =
409420 { password: Cipher_LoginCipher_password_Maybe
421+ , totp: Cipher_LoginCipher_totp_Maybe
410422 , uris: Cipher_LoginCipher_uris_List
411423 , username: Cipher_LoginCipher_username_Maybe
412424 }
413425
414426jsonDecCipher_LoginCipher : Json.Decode.Decoder ( Cipher_LoginCipher )
415427jsonDecCipher_LoginCipher =
416− Json.Decode.succeed (\ppassword puris pusername -> {password = ppassword, uris = puris, username = pusername})
428+ Json.Decode.succeed (\ppassword ptotp puris pusername -> {password = ppassword, totp = ptotp, uris = puris, username = pusername})
417429 |> required "password" (jsonDecCipher_LoginCipher_password_Maybe)
430+ |> required "totp" (jsonDecCipher_LoginCipher_totp_Maybe)
418431 |> required "uris" (jsonDecCipher_LoginCipher_uris_List)
419432 |> required "username" (jsonDecCipher_LoginCipher_username_Maybe)
420433
@@ -422,6 +435,7 @@ jsonEncCipher_LoginCipher : Cipher_LoginCipher -> Value
422435jsonEncCipher_LoginCipher val =
423436 Json.Encode.object
424437 [ ("password", jsonEncCipher_LoginCipher_password_Maybe val.password)
438+ , ("totp", jsonEncCipher_LoginCipher_totp_Maybe val.totp)
425439 , ("uris", jsonEncCipher_LoginCipher_uris_List val.uris)
426440 , ("username", jsonEncCipher_LoginCipher_username_Maybe val.username)
427441 ]
@@ -512,6 +526,7 @@ type Cmd =
512526 | NeedsReset
513527 | Open String
514528 | RequestCipher String
529+ | RequestTotp String
515530 | SendMasterPassword String
516531 | UpdateCipher FullCipher
517532
@@ -529,6 +544,7 @@ jsonDecCmd =
529544 , ("NeedsReset", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsReset))
530545 , ("Open", Json.Decode.lazy (\_ -> Json.Decode.map Open (Json.Decode.string)))
531546 , ("RequestCipher", Json.Decode.lazy (\_ -> Json.Decode.map RequestCipher (Json.Decode.string)))
547+ , ("RequestTotp", Json.Decode.lazy (\_ -> Json.Decode.map RequestTotp (Json.Decode.string)))
532548 , ("SendMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map SendMasterPassword (Json.Decode.string)))
533549 , ("UpdateCipher", Json.Decode.lazy (\_ -> Json.Decode.map UpdateCipher (jsonDecFullCipher)))
534550 ]
@@ -549,6 +565,7 @@ jsonEncCmd val =
549565 NeedsReset -> ("NeedsReset", encodeValue (Json.Encode.list identity []))
550566 Open v1 -> ("Open", encodeValue (Json.Encode.string v1))
551567 RequestCipher v1 -> ("RequestCipher", encodeValue (Json.Encode.string v1))
568+ RequestTotp v1 -> ("RequestTotp", encodeValue (Json.Encode.string v1))
552569 SendMasterPassword v1 -> ("SendMasterPassword", encodeValue (Json.Encode.string v1))
553570 UpdateCipher v1 -> ("UpdateCipher", encodeValue (jsonEncFullCipher v1))
554571 in encodeSumTaggedObject "tag" "contents" keyval val
@@ -700,6 +717,29 @@ jsonEncSub_NeedsMasterPassword val =
700717
701718
702719
720+type alias Sub_Totp =
721+ { code: String
722+ , interval: Int
723+ , source: String
724+ }
725+
726+jsonDecSub_Totp : Json.Decode.Decoder ( Sub_Totp )
727+jsonDecSub_Totp =
728+ Json.Decode.succeed (\pcode pinterval psource -> {code = pcode, interval = pinterval, source = psource})
729+ |> required "code" (Json.Decode.string)
730+ |> required "interval" (Json.Decode.int)
731+ |> required "source" (Json.Decode.string)
732+
733+jsonEncSub_Totp : Sub_Totp -> Value
734+jsonEncSub_Totp val =
735+ Json.Encode.object
736+ [ ("code", Json.Encode.string val.code)
737+ , ("interval", Json.Encode.int val.interval)
738+ , ("source", Json.Encode.string val.source)
739+ ]
740+
741+
742+
703743type Sub =
704744 CaptchaDone
705745 | CipherChanged FullCipher
@@ -714,6 +754,7 @@ type Sub =
714754 | NeedsMasterPassword Sub_NeedsMasterPassword
715755 | RecieveEmail String
716756 | Reset
757+ | Totp Sub_Totp
717758 | WrongPassword
718759
719760jsonDecSub : Json.Decode.Decoder ( Sub )
@@ -732,6 +773,7 @@ jsonDecSub =
732773 , ("NeedsMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map NeedsMasterPassword (jsonDecSub_NeedsMasterPassword)))
733774 , ("RecieveEmail", Json.Decode.lazy (\_ -> Json.Decode.map RecieveEmail (Json.Decode.string)))
734775 , ("Reset", Json.Decode.lazy (\_ -> Json.Decode.succeed Reset))
776+ , ("Totp", Json.Decode.lazy (\_ -> Json.Decode.map Totp (jsonDecSub_Totp)))
735777 , ("WrongPassword", Json.Decode.lazy (\_ -> Json.Decode.succeed WrongPassword))
736778 ]
737779 jsonDecObjectSetSub = Set.fromList []
@@ -753,6 +795,7 @@ jsonEncSub val =
753795 NeedsMasterPassword v1 -> ("NeedsMasterPassword", encodeValue (jsonEncSub_NeedsMasterPassword v1))
754796 RecieveEmail v1 -> ("RecieveEmail", encodeValue (Json.Encode.string v1))
755797 Reset -> ("Reset", encodeValue (Json.Encode.list identity []))
798+ Totp v1 -> ("Totp", encodeValue (jsonEncSub_Totp v1))
756799 WrongPassword -> ("WrongPassword", encodeValue (Json.Encode.list identity []))
757800 in encodeSumTaggedObject "tag" "contents" keyval val
758801
frontend/GlobalEvents.elm+1
@@ -6,3 +6,4 @@ import Bridge
66type Event
77 = UpdateCipher Bridge.FullCipher
88 | GeneratedPassword String
9+ | DecodedTotp Bridge.Sub_Totp
frontend/Logic/Cipher.elm+2−1
@@ -54,9 +54,10 @@ normalizeIdentityCipher { address1, address2, address3, city, company, country,
5454
5555
5656normalizeLoginCipher : Bridge.Cipher_LoginCipher -> Bridge.Cipher_LoginCipher
57−normalizeLoginCipher { password, uris, username } =
57+normalizeLoginCipher { password, totp, uris, username } =
5858 { password = password
5959 , uris = uris |> List.map String.trim |> List.filter String.isEmpty
60+ , totp = normalizeStringMaybe totp
6061 , username = username
6162 }
6263
frontend/Main.elm+36−1
@@ -64,6 +64,7 @@ type Msg
6464 | OpenNewCipherEditPage Bridge.CipherType
6565 | WrongPassword
6666 | DeleteCipher Bridge.FullCipher
67+ | RequestTotp String
6768
6869
6970type alias Model =
@@ -231,9 +232,39 @@ subscriptions model =
231232
232233 Bridge.CipherDeleted c ->
233234 ShowInfo "Entry deleted" ("The entry “" ++ c.name ++ "” has been successfully deleted.")
235+
236+ Bridge.Totp totp ->
237+ GlobalEvents.DecodedTotp totp |> FireGlobalEvent
234238 )
235239 ]
236240 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
241+ ++ (model.pageStack
242+ |> Nonempty.toList
243+ |> List.map
244+ (\x ->
245+ case x of
246+ LoginModel m ->
247+ (Login.page loginCallbacks LoginMsg).subscriptions m
248+
249+ LoadingPage ->
250+ Sub.none
251+
252+ CiphersModel m ->
253+ (Ciphers.page ciphersCallbacks CiphersMsg).subscriptions m
254+
255+ EditCipherModel m ->
256+ (EditCipher.page EditCipherMsg).subscriptions m
257+
258+ MasterPasswordModel m ->
259+ (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).subscriptions m
260+
261+ CipherModel m ->
262+ (Cipher.page cipherCallbacks CipherMsg).subscriptions m
263+
264+ CaptchaModel m ->
265+ (Captcha.page captchaCallbacks CaptchaMsg).subscriptions m
266+ )
267+ )
237268 )
238269
239270
@@ -557,6 +588,7 @@ update msg model =
557588 { uris = []
558589 , username = Nothing
559590 , password = Nothing
591+ , totp = Nothing
560592 }
561593
562594 Bridge.NoteType ->
@@ -614,6 +646,9 @@ update msg model =
614646 ]
615647 )
616648
649+ RequestTotp totp ->
650+ ( model, FFI.sendBridge (Bridge.RequestTotp totp) )
651+
617652
618653loginCallbacks : Login.Callbacks Msg
619654loginCallbacks =
@@ -630,7 +665,7 @@ ciphersCallbacks =
630665
631666cipherCallbacks : Cipher.Callbacks Msg
632667cipherCallbacks =
633− { copy = Copy, open = Open, edit = EditCipher }
668+ { copy = Copy, open = Open, edit = EditCipher, needTotp = RequestTotp }
634669
635670
636671editCipherCallbacks : EditCipher.Callbacks Msg
frontend/Pages/Cipher.elm+72−11
@@ -6,6 +6,7 @@ import Html exposing (..)
66import Html.Attributes as Attr
77import Html.Events as Ev
88import Page exposing (..)
9+import Time
910import Utils exposing (..)
1011
1112
@@ -13,6 +14,7 @@ type alias Model =
1314 { cipher : Bridge.FullCipher
1415 , passwordHidden : Bool
1516 , cvvHidden : Bool
17+ , decodedTotp : Maybe { code : String, interval : Int }
1618 }
1719
1820
@@ -27,15 +29,16 @@ type alias Callbacks msg =
2729 { copy : String -> msg
2830 , open : String -> msg
2931 , edit : Bridge.FullCipher -> msg
32+ , needTotp : String -> msg
3033 }
3134
3235
3336page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
3437page callbacks liftMsg =
35− { init = \data -> Tuple.mapSecond (Cmd.map liftMsg) (init data)
38+ { init = \data -> init callbacks data
3639 , view = \model -> view model |> List.map (Html.map liftMsg)
3740 , update = \msg model -> update callbacks liftMsg msg model
38− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
41+ , subscriptions = \model -> subscriptions model callbacks
3942 , title =
4043 \{ cipher } ->
4144 [ text cipher.name
@@ -54,18 +57,41 @@ page callbacks liftMsg =
5457 model.cipher
5558 }
5659
60+ GlobalEvents.DecodedTotp { source, code, interval } ->
61+ case model.cipher.cipher of
62+ Bridge.LoginCipher { totp } ->
63+ if totp == Just source then
64+ { model | decodedTotp = Just { code = code, interval = interval } }
65+
66+ else
67+ model
68+
69+ _ ->
70+ model
71+
5772 _ ->
5873 model
5974 }
6075
6176
62−init : Bridge.FullCipher -> ( Model, Cmd Msg )
63−init cipher =
77+init : Callbacks emsg -> Bridge.FullCipher -> ( Model, Cmd emsg )
78+init { needTotp } cipher =
6479 ( { cipher = cipher
6580 , passwordHidden = True
6681 , cvvHidden = True
82+ , decodedTotp = Nothing
6783 }
68− , Cmd.none
84+ , case cipher.cipher of
85+ Bridge.LoginCipher { totp } ->
86+ case totp of
87+ Just x ->
88+ needTotp x |> pureCmd
89+
90+ Nothing ->
91+ Cmd.none
92+
93+ _ ->
94+ Cmd.none
6995 )
7096
7197
@@ -85,9 +111,19 @@ update { copy, open } _ msg model =
85111 ( Ok model, open uri |> pureCmd )
86112
87113
88−subscriptions : Model -> Sub Msg
89−subscriptions _ =
90− Sub.none
114+subscriptions : Model -> Callbacks msg -> Sub msg
115+subscriptions { cipher } { needTotp } =
116+ case cipher.cipher of
117+ Bridge.LoginCipher { totp } ->
118+ case totp of
119+ Just x ->
120+ Time.every 1000 (always (needTotp x))
121+
122+ Nothing ->
123+ Sub.none
124+
125+ _ ->
126+ Sub.none
91127
92128
93129row : { name : String, value : String, nameIcon : String, icons : List ( String, msg ) } -> Html msg
@@ -101,9 +137,9 @@ row { name, value, nameIcon, icons } =
101137
102138
103139view : Model -> List (Html Msg)
104−view { passwordHidden, cipher, cvvHidden } =
140+view { passwordHidden, cipher, cvvHidden, decodedTotp } =
105141 (case cipher.cipher of
106− Bridge.LoginCipher { username, password, uris } ->
142+ Bridge.LoginCipher { username, password, uris, totp } ->
107143 maybeList username
108144 (\x ->
109145 row
@@ -123,11 +159,36 @@ view { passwordHidden, cipher, cvvHidden } =
123159 , icons = [ ( hiddenButtonIcon passwordHidden, TogglePasswordVisiblity ), ( "copy", Copy x ) ]
124160 }
125161 )
162+ ++ maybeList totp
163+ (\_ ->
164+ row
165+ { name = "One-time password"
166+ , value =
167+ case decodedTotp of
168+ Nothing ->
169+ "Loading..."
170+
171+ Just { code } ->
172+ code
173+ , nameIcon = "revisions"
174+ , icons =
175+ case decodedTotp of
176+ Nothing ->
177+ []
178+
179+ Just { code } ->
180+ [ ( "copy", Copy code ) ]
181+ }
182+ )
126183 ++ (uris
127184 |> List.map
128185 (\uri ->
129186 row
130− { name = "URL", value = uri, nameIcon = "get-link", icons = [ ( "external-link", Open uri ), ( "copy", Copy uri ) ] }
187+ { name = "URL"
188+ , value = uri
189+ , nameIcon = "get-link"
190+ , icons = [ ( "external-link", Open uri ), ( "copy", Copy uri ) ]
191+ }
131192 )
132193 )
133194
frontend/Pages/EditCipher.elm+14−1
@@ -81,6 +81,9 @@ page liftMsg =
8181 c
8282 }
8383 }
84+
85+ _ ->
86+ model
8487 }
8588
8689
@@ -174,7 +177,7 @@ view { fullCipher, passwordGenerator, callbacks } =
174177 ++ (case fullCipher.cipher of
175178 Bridge.LoginCipher cipher ->
176179 let
177− { username, password, uris } =
180+ { username, password, uris, totp } =
178181 cipher
179182
180183 edit c =
@@ -203,6 +206,16 @@ view { fullCipher, passwordGenerator, callbacks } =
203206 []
204207 , iconButton "restart" OpenGeneratePasword
205208 ]
209+ , row
210+ { name = "One-time password"
211+ , nameIcon = "revisions"
212+ , attrs =
213+ [ Attr.type_ "text"
214+ , Attr.value (Maybe.withDefault "" totp)
215+ , Ev.onInput (\x -> edit { cipher | totp = Just x })
216+ , Attr.attribute "autocomplete" "off"
217+ ]
218+ }
206219 ]
207220 ++ (uris
208221 |> List.indexedMap