on master help sign in
Got logins really working
27aaecciko
bw/src/BW.js → backend/BW.jsrenamed+3−2
@@ -9,7 +9,7 @@ import { TokenService } from "../../deps/bw/libs/shared/dist/src/services/token.
9import { WebCryptoFunctionService } from "../../deps/bw/libs/shared/dist/src/services/webCryptoFunction.service";
9import { WebCryptoFunctionService } from "../../deps/bw/libs/shared/dist/src/services/webCryptoFunction.service";
10import { Account, AccountProfile, AccountTokens } from "../../deps/bw/libs/shared/dist/src/models/domain/account";
10import { Account, AccountProfile, AccountTokens } from "../../deps/bw/libs/shared/dist/src/models/domain/account";
11
11
12−// import BrowserStorageService from "../../deps/bw/libs/shared/dist/src/services/browserStorage.service";
12+// import BrowserStorageService from "../../../../deps/bw/libs/shared/dist/src/services/browserStorage.service";
13import { CryptoService } from "../../deps/bw/libs/shared/dist/src/services/crypto.service";
13import { CryptoService } from "../../deps/bw/libs/shared/dist/src/services/crypto.service";
14
14
15import { I18nService } from "../../deps/bw/libs/shared/dist/src/services/i18n.service";
15import { I18nService } from "../../deps/bw/libs/shared/dist/src/services/i18n.service";
@@ -74,7 +74,8 @@ export function getServices() {
74 getSync: () => api.getSync().then(sanitize),
74 getSync: () => api.getSync().then(sanitize),
75 }
75 }
76 },
76 },
77− crypto: bg.cryptoService
77+ crypto: bg.cryptoService,
78+ cryptoFunctions: bg.cryptoFunctionService
78 }
79 }
79}
80}
80
81
backend/BW.pursnew file+78
@@ -0,0 +1,78 @@
1+module BW where
2+
3+import BW.Types
4+import Prelude
5+
6+import Control.Promise (Promise)
7+import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
8+import Data.ArrayBuffer.Typed as AB
9+import Data.ArrayBuffer.Types (ArrayBuffer, ArrayView, Int32)
10+import Data.EncString (EncString)
11+import Data.Function.Uncurried (Fn2, Fn3, Fn4)
12+import Data.JNullable (JNullable)
13+import Data.SymmetricCryptoKey (SymmetricCryptoKey)
14+import Effect (Effect)
15+import Effect.Unsafe (unsafePerformEffect)
16+
17+type CryptoService
18+ = { makeKey :: Fn4 Password String KDF Int (Promise SymmetricCryptoKey)
19+ , hashPassword :: Fn3 Password SymmetricCryptoKey (JNullable HashPurpose) (Promise StringHash)
20+ , decryptToUtf8 :: Fn2 EncString SymmetricCryptoKey (Promise String)
21+ , decryptToBytes :: Fn2 EncString SymmetricCryptoKey (Promise ArrayBuffer)
22+ , stretchKey :: SymmetricCryptoKey -> Promise SymmetricCryptoKey
23+ }
24+
25+type ApiService
26+ = { postPrelogin :: PreloginRequest -> Promise PreloginResponse
27+ , getProfile :: Unit -> Promise ProfileResponse
28+ -- TODO: There can really be more request and response types. Maybe handle this later.
29+ , postIdentityToken :: PasswordTokenRequest -> Promise IdentityTokenResponse
30+ , getSync :: Unit -> Promise SyncResponse
31+ }
32+
33+cryptoFunctionsTypeSha1 = "sha1" :: CryptoFunctionsType
34+
35+cryptoFunctionsTypeSha256 = "sha256" :: CryptoFunctionsType
36+
37+cryptoFunctionsTypeSha512 = "sha512" :: CryptoFunctionsType
38+
39+cryptoFunctionsTypeMd5 = "md5" :: CryptoFunctionsType
40+
41+type CryptoFunctionsType
42+ = String
43+
44+type CryptoFunctions
45+ = { hash :: Fn2 String CryptoFunctionsType (Promise Hash)
46+ }
47+
48+newtype Hash
49+ = Hash (ArrayBuffer)
50+
51+hashToArray :: Hash -> Array Int
52+hashToArray (Hash buf) =
53+ unsafePerformEffect do
54+ av :: ArrayView Int32 <- AB.whole buf
55+ AB.toArray av
56+
57+arrayToHash :: Array Int -> Hash
58+arrayToHash arr =
59+ unsafePerformEffect do
60+ av :: ArrayView Int32 <- AB.fromArray arr
61+ pure $ Hash $ AB.buffer av
62+
63+instance Eq Hash where
64+ eq a b = eq (hashToArray a) (hashToArray b)
65+
66+instance EncodeJson Hash where
67+ encodeJson = hashToArray >>> encodeJson
68+
69+instance DecodeJson Hash where
70+ decodeJson = decodeJson >>> map arrayToHash
71+
72+type Services
73+ = { crypto :: CryptoService
74+ , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
75+ , cryptoFunctions :: CryptoFunctions
76+ }
77+
78+foreign import getServices :: Effect Services
bw/src/BW/Logic.purs → backend/BW/Logic.pursrenamed+18−13
@@ -9,8 +9,8 @@ import Control.Promise (Promise)
9import Control.Promise as Promise
9import Control.Promise as Promise
10import Data.EncString as EncString
10import Data.EncString as EncString
11import Data.Function.Uncurried (runFn2, runFn3, runFn4)
11import Data.Function.Uncurried (runFn2, runFn3, runFn4)
12+import Data.JNullable (jnull, nullify)
12import Data.Maybe (Maybe(..))
13import Data.Maybe (Maybe(..))
13−import Data.Nullable (null, toNullable)
14import Data.String as String
14import Data.String as String
15import Data.SymmetricCryptoKey (SymmetricCryptoKey)
15import Data.SymmetricCryptoKey (SymmetricCryptoKey)
16import Data.SymmetricCryptoKey as SymmetricCryptoKey
16import Data.SymmetricCryptoKey as SymmetricCryptoKey
@@ -19,19 +19,19 @@ import Effect.Class (liftEffect)
19import Effect.Class.Console (log)
19import Effect.Class.Console (log)
20import Effect.Exception (error)
20import Effect.Exception (error)
21import Effect.Exception as Exc
21import Effect.Exception as Exc
22+import Localstorage (class StorageKey)
23+import Web.Storage.Storage (Storage)
22
24
25+-- | Creates the master key
23makePreloginKey ::
26makePreloginKey ::
24 forall r.
27 forall r.
25− HasL "api" ApiService r =>
26 HasL "crypto" CryptoService r =>
28 HasL "crypto" CryptoService r =>
27− Email -> Password -> Al r SymmetricCryptoKey
29+ PreloginResponse -> Email -> Password -> Al r SymmetricCryptoKey
28−makePreloginKey (Email email') (Password password) = do
30+makePreloginKey { kdf, kdfIterations } (Email email') password = do
29− api :: ApiService <- l (L :: L "api")
30 crypto <- l (L :: L "crypto")
31 crypto <- l (L :: L "crypto")
31 let
32 let
32 email = (String.trim >>> String.toLower) email'
33 email = (String.trim >>> String.toLower) email'
33− { kdf, kdfIterations } <- liftPromise $ api.postPrelogin { email }
34− liftPromise $ runFn4 crypto.makeKey (Password password) email kdf kdfIterations
34+ liftPromise $ runFn4 crypto.makeKey password email kdf kdfIterations
35
35
36makeDecryptionKey ::
36makeDecryptionKey ::
37 forall r.
37 forall r.
@@ -58,15 +58,15 @@ getLogInRequestToken ::
58 forall r.
58 forall r.
59 HasL "api" ApiService r =>
59 HasL "api" ApiService r =>
60 HasL "crypto" CryptoService r =>
60 HasL "crypto" CryptoService r =>
61− Email -> Password -> Al r IdentityTokenResponse
61+ PreloginResponse -> Email -> Password -> Al r IdentityTokenResponse
62−getLogInRequestToken email password = do
62+getLogInRequestToken prelogin email password = do
63− key <- makePreloginKey email password
63+ key <- makePreloginKey prelogin email password
64 crypto <- l (L :: L "crypto")
64 crypto <- l (L :: L "crypto")
65 api :: ApiService <- l (L :: L "api")
65 api :: ApiService <- l (L :: L "api")
66 _localHashedPassword <-
66 _localHashedPassword <-
67 liftPromise
67 liftPromise
68− $ runFn3 crypto.hashPassword password key (toNullable $ Just hashPurposeLocalAuthorization)
68+ $ runFn3 crypto.hashPassword password key (nullify hashPurposeLocalAuthorization)
69− Hash hashedPassword <- liftPromise $ runFn3 crypto.hashPassword password key null
69+ StringHash hashedPassword <- liftPromise $ runFn3 crypto.hashPassword password key jnull
70 liftPromise
70 liftPromise
71 $ api.postIdentityToken
71 $ api.postIdentityToken
72 { email: email
72 { email: email
@@ -81,10 +81,15 @@ getLogInRequestToken email password = do
81 { type: deviceTypeUnknownBrowser
81 { type: deviceTypeUnknownBrowser
82 , name: "Temporary device name"
82 , name: "Temporary device name"
83 , identifier: "42"
83 , identifier: "42"
84− , pushToken: null
84+ , pushToken: jnull
85 }
85 }
86 }
86 }
87
87
88+hashPassword :: forall r. HasL "cryptoFunctions" CryptoFunctions r => Password -> Al r Hash
89+hashPassword (Password password) = do
90+ cryptoFunctions <- l (L :: L "cryptoFunctions")
91+ liftPromise $ runFn2 cryptoFunctions.hash password cryptoFunctionsTypeSha512
92+
88decrypt ::
93decrypt ::
89 forall r.
94 forall r.
90 HasL "crypto" CryptoService r =>
95 HasL "crypto" CryptoService r =>
bw/src/BW/Types.purs → backend/BW/Types.pursrenamed+75−63
@@ -2,17 +2,21 @@ module BW.Types where
2
2
3import Prelude
3import Prelude
4
4
5−import Data.ArrayBuffer.Types (ArrayBuffer)
5+import Data.Argonaut (class DecodeJson, class EncodeJson)
6import Data.Date (Date)
6import Data.Date (Date)
7−import Data.Nullable (Nullable)
7+import Data.JNullable (JNullable)
8+import Data.JOpt (JOpt)
8import Data.ShowableJson (ShowableJson)
9import Data.ShowableJson (ShowableJson)
9import Data.Timestamp (Timestamp(..))
10import Data.Timestamp (Timestamp(..))
10−import Data.Undefined.NoProblem (Opt)
11
11
12newtype EncryptedString
12newtype EncryptedString
13 = EncryptedString String
13 = EncryptedString String
14
14
15derive newtype instance Show EncryptedString
15derive newtype instance Show EncryptedString
16+derive newtype instance Ord EncryptedString
17+derive newtype instance Eq EncryptedString
18+derive newtype instance EncodeJson EncryptedString
19+derive newtype instance DecodeJson EncryptedString
16
20
17kdfPBKDF2_SHA256 = 0 :: KDF
21kdfPBKDF2_SHA256 = 0 :: KDF
18
22
@@ -35,7 +39,7 @@ type ProfileResponse
35 , emailVerified :: Boolean
39 , emailVerified :: Boolean
36 , masterPasswordHint :: String
40 , masterPasswordHint :: String
37 , premiumPersonally :: Boolean
41 , premiumPersonally :: Boolean
38− , premiumFromOrganization :: Boolean
42+ -- , premiumFromOrganization :: Boolean
39 , culture :: String
43 , culture :: String
40 , twoFactorEnabled :: Boolean
44 , twoFactorEnabled :: Boolean
41 , key :: EncryptedString
45 , key :: EncryptedString
@@ -65,7 +69,7 @@ type ProfileOrganizationResponse
65 , usersGetPremium :: Boolean
69 , usersGetPremium :: Boolean
66 , seats :: Int
70 , seats :: Int
67 , maxCollections :: Int
71 , maxCollections :: Int
68− , maxStorageGb :: Nullable Int
72+ , maxStorageGb :: JNullable Int
69 , key :: String
73 , key :: String
70 , hasPublicAndPrivateKeys :: Boolean
74 , hasPublicAndPrivateKeys :: Boolean
71 , status :: OrganizationUserStatusType
75 , status :: OrganizationUserStatusType
@@ -83,9 +87,9 @@ type ProfileOrganizationResponse
83 , planProductType :: ProductType
87 , planProductType :: ProductType
84 , keyConnectorEnabled :: Boolean
88 , keyConnectorEnabled :: Boolean
85 , keyConnectorUrl :: String
89 , keyConnectorUrl :: String
86− , familySponsorshipLastSyncDate :: Nullable Date
90+ , familySponsorshipLastSyncDate :: JNullable Timestamp
87− , familySponsorshipValidUntil :: Nullable Date
91+ , familySponsorshipValidUntil :: JNullable Timestamp
88− , familySponsorshipToDelete :: Nullable Boolean
92+ , familySponsorshipToDelete :: JNullable Boolean
89 }
93 }
90
94
91organizationUserStatusTypeInvited = 0 :: OrganizationUserStatusType
95organizationUserStatusTypeInvited = 0 :: OrganizationUserStatusType
@@ -185,7 +189,7 @@ type ProfileProviderOrganizationResponse
185 , usersGetPremium :: Boolean
189 , usersGetPremium :: Boolean
186 , seats :: Int
190 , seats :: Int
187 , maxCollections :: Int
191 , maxCollections :: Int
188− , maxStorageGb :: Nullable Int
192+ , maxStorageGb :: JNullable Int
189 , key :: String
193 , key :: String
190 , hasPublicAndPrivateKeys :: Boolean
194 , hasPublicAndPrivateKeys :: Boolean
191 , status :: OrganizationUserStatusType
195 , status :: OrganizationUserStatusType
@@ -203,20 +207,20 @@ type ProfileProviderOrganizationResponse
203 , planProductType :: ProductType
207 , planProductType :: ProductType
204 , keyConnectorEnabled :: Boolean
208 , keyConnectorEnabled :: Boolean
205 , keyConnectorUrl :: String
209 , keyConnectorUrl :: String
206− , familySponsorshipLastSyncDate :: Nullable Date
210+ , familySponsorshipLastSyncDate :: JNullable Timestamp
207− , familySponsorshipValidUntil :: Nullable Date
211+ , familySponsorshipValidUntil :: JNullable Timestamp
208− , familySponsorshipToDelete :: Nullable Boolean
212+ , familySponsorshipToDelete :: JNullable Boolean
209 }
213 }
210
214
211type Urls
215type Urls
212− = { base :: Nullable String
216+ = { base :: JNullable String
213− , webVault :: Nullable String
217+ , webVault :: JNullable String
214− , api :: Nullable String
218+ , api :: JNullable String
215− , identity :: Nullable String
219+ , identity :: JNullable String
216− , icons :: Nullable String
220+ , icons :: JNullable String
217− , notifications :: Nullable String
221+ , notifications :: JNullable String
218− , events :: Nullable String
222+ , events :: JNullable String
219− , keyConnector :: Nullable String
223+ , keyConnector :: JNullable String
220 }
224 }
221
225
222type PasswordTokenRequest
226type PasswordTokenRequest
@@ -231,7 +235,7 @@ type DeviceRequest
231 = { type :: DeviceType
235 = { type :: DeviceType
232 , name :: String
236 , name :: String
233 , identifier :: String
237 , identifier :: String
234− , pushToken :: Nullable String
238+ , pushToken :: JNullable String
235 }
239 }
236
240
237deviceTypeAndroid = 0 :: DeviceType
241deviceTypeAndroid = 0 :: DeviceType
@@ -314,18 +318,26 @@ hashPurposeLocalAuthorization = 2 :: HashPurpose
314type HashPurpose
318type HashPurpose
315 = Int
319 = Int
316
320
317−newtype Hash
321+newtype StringHash
318− = Hash String
322+ = StringHash String
319
323
320newtype AccessToken
324newtype AccessToken
321 = AccessToken String
325 = AccessToken String
322
326
323derive newtype instance Show AccessToken
327derive newtype instance Show AccessToken
328+derive newtype instance Eq AccessToken
329+derive newtype instance Ord AccessToken
330+derive newtype instance EncodeJson AccessToken
331+derive newtype instance DecodeJson AccessToken
324
332
325newtype RefreshToken
333newtype RefreshToken
326 = RefreshToken String
334 = RefreshToken String
327
335
328derive newtype instance Show RefreshToken
336derive newtype instance Show RefreshToken
337+derive newtype instance Eq RefreshToken
338+derive newtype instance Ord RefreshToken
339+derive newtype instance EncodeJson RefreshToken
340+derive newtype instance DecodeJson RefreshToken
329
341
330
342
331type IdentityTokenResponse
343type IdentityTokenResponse
@@ -336,12 +348,12 @@ type IdentityTokenResponse
336 , resetMasterPassword :: Boolean
348 , resetMasterPassword :: Boolean
337 , privateKey :: EncryptedString
349 , privateKey :: EncryptedString
338 , key :: EncryptedString
350 , key :: EncryptedString
339− , twoFactorToken :: Nullable String
351+ , twoFactorToken :: JNullable String
340 , kdf :: KDF
352 , kdf :: KDF
341 , kdfIterations :: Int
353 , kdfIterations :: Int
342− , forcePasswordReset :: Nullable Boolean
354+ , forcePasswordReset :: JNullable Boolean
343− , apiUseKeyConnector :: Nullable Boolean
355+ , apiUseKeyConnector :: JNullable Boolean
344− , keyConnectorUrl :: Nullable String
356+ , keyConnectorUrl :: JNullable String
345 }
357 }
346
358
347newtype Email
359newtype Email
@@ -352,8 +364,8 @@ type SyncResponse
352 , folders :: Array FolderResponse
364 , folders :: Array FolderResponse
353 , collections :: Array CollectionDetailsResponse
365 , collections :: Array CollectionDetailsResponse
354 , ciphers :: Array CipherResponse
366 , ciphers :: Array CipherResponse
355− , domains :: Nullable DomainsResponse
367+ , domains :: JNullable DomainsResponse
356− , policies :: Nullable (Array PolicyResponse)
368+ , policies :: JNullable (Array PolicyResponse)
357 , sends :: Array SendResponse
369 , sends :: Array SendResponse
358 }
370 }
359
371
@@ -365,33 +377,33 @@ type FolderResponse
365
377
366type CollectionDetailsResponse
378type CollectionDetailsResponse
367 = { id :: String
379 = { id :: String
368− , organizationId :: Nullable String
380+ , organizationId :: JNullable String
369 , name :: String
381 , name :: String
370− , externalId :: Nullable String
382+ , externalId :: JNullable String
371 , readOnly :: Boolean
383 , readOnly :: Boolean
372 }
384 }
373
385
374type CipherResponse
386type CipherResponse
375 = { id :: String
387 = { id :: String
376− , organizationId :: Nullable String
388+ , organizationId :: JNullable String
377− , folderId :: Nullable String
389+ , folderId :: JNullable String
378 , type :: CipherType
390 , type :: CipherType
379 , name :: EncryptedString
391 , name :: EncryptedString
380− , notes :: Nullable EncryptedString
392+ , notes :: JNullable EncryptedString
381− , fields :: Nullable (Array FieldApi)
393+ , fields :: JNullable (Array FieldApi)
382− , login :: Nullable LoginApi
394+ , login :: JNullable LoginApi
383− , card :: Nullable CardApi
395+ , card :: JNullable CardApi
384− , identity :: Nullable IdentityApi
396+ , identity :: JNullable IdentityApi
385− , secureNote :: Nullable SecureNoteApi
397+ , secureNote :: JNullable SecureNoteApi
386 , favorite :: Boolean
398 , favorite :: Boolean
387 , edit :: Boolean
399 , edit :: Boolean
388 , viewPassword :: Boolean
400 , viewPassword :: Boolean
389 , organizationUseTotp :: Boolean
401 , organizationUseTotp :: Boolean
390 , revisionDate :: Timestamp
402 , revisionDate :: Timestamp
391− , attachments :: Nullable (Array AttachmentResponse)
403+ , attachments :: JNullable (Array AttachmentResponse)
392− , passwordHistory :: Nullable (Array PasswordHistoryResponse)
404+ , passwordHistory :: JNullable (Array PasswordHistoryResponse)
393− , collectionIds :: Nullable (Array String)
405+ , collectionIds :: JNullable (Array String)
394− , deletedDate :: Nullable String
406+ , deletedDate :: JNullable String
395 , reprompt :: CipherRepromptType
407 , reprompt :: CipherRepromptType
396 }
408 }
397
409
@@ -410,7 +422,7 @@ type FieldApi
410 = { name :: String
422 = { name :: String
411 , value :: String
423 , value :: String
412 , type :: FieldType
424 , type :: FieldType
413− , linkedId :: Nullable LinkedIdType
425+ , linkedId :: JNullable LinkedIdType
414 }
426 }
415
427
416fieldTypeText = 0 :: FieldType
428fieldTypeText = 0 :: FieldType
@@ -482,18 +494,18 @@ type LinkedIdType
482 = Int
494 = Int
483
495
484type LoginApi
496type LoginApi
485− = { uris :: Opt (Array LoginUriApi)
497+ = { uris :: JOpt (Array LoginUriApi)
486− , username :: Nullable EncryptedString
498+ , username :: JNullable EncryptedString
487− , password :: Nullable EncryptedString
499+ , password :: JNullable EncryptedString
488− , passwordRevisionDate :: Nullable String
500+ , passwordRevisionDate :: JNullable String
489− , totp :: Nullable EncryptedString
501+ , totp :: JNullable EncryptedString
490 -- This thing is undefined in some cases for some reson. Just ignoring it
502 -- This thing is undefined in some cases for some reson. Just ignoring it
491− , autofillOnPageLoad :: Opt (Nullable Boolean)
503+ , autofillOnPageLoad :: JOpt (JNullable Boolean)
492 }
504 }
493
505
494type LoginUriApi
506type LoginUriApi
495 = { uri :: String
507 = { uri :: String
496− , match :: Nullable UriMatchType
508+ , match :: JNullable UriMatchType
497 }
509 }
498
510
499uriMatchTypeDomain = 0 :: UriMatchType
511uriMatchTypeDomain = 0 :: UriMatchType
@@ -512,12 +524,12 @@ type UriMatchType
512 = Int
524 = Int
513
525
514type CardApi
526type CardApi
515− = { cardholderName :: Nullable String
527+ = { cardholderName :: JNullable String
516− , brand :: Nullable String
528+ , brand :: JNullable String
517− , number :: Nullable String
529+ , number :: JNullable String
518− , expMonth :: Nullable String
530+ , expMonth :: JNullable String
519− , expYear :: Nullable String
531+ , expYear :: JNullable String
520− , code :: Nullable String
532+ , code :: JNullable String
521 }
533 }
522
534
523type IdentityApi
535type IdentityApi
@@ -580,7 +592,7 @@ type GlobalDomainResponse
580
592
581type PolicyResponse
593type PolicyResponse
582 = { id :: String
594 = { id :: String
583− , organizationId :: Nullable String
595+ , organizationId :: JNullable String
584 , type :: PolicyType
596 , type :: PolicyType
585 , data :: ShowableJson
597 , data :: ShowableJson
586 , enabled :: Boolean
598 , enabled :: Boolean
@@ -616,15 +628,15 @@ type SendResponse
616 , accessId :: String
628 , accessId :: String
617 , type :: SendType
629 , type :: SendType
618 , name :: String
630 , name :: String
619− , notes :: Nullable String
631+ , notes :: JNullable String
620− , file :: Nullable SendFileApi
632+ , file :: JNullable SendFileApi
621− , text :: Nullable SendTextApi
633+ , text :: JNullable SendTextApi
622 , key :: String
634 , key :: String
623− , maxAccessCount :: Nullable Int
635+ , maxAccessCount :: JNullable Int
624 , accessCount :: Int
636 , accessCount :: Int
625 , revisionDate :: String
637 , revisionDate :: String
626 , expirationDate :: String
638 , expirationDate :: String
627− , deletionDate :: Nullable String
639+ , deletionDate :: JNullable String
628 , password :: String
640 , password :: String
629 , disable :: Boolean
641 , disable :: Boolean
630 , hideEmail :: Boolean
642 , hideEmail :: Boolean
backend/Bridge.purs+20
@@ -30,6 +30,9 @@ derive instance ordCmd_Login :: Ord Cmd_Login
30data Cmd =
30data Cmd =
31 Init
31 Init
32 | Login Cmd_Login
32 | Login Cmd_Login
33+ | NeedCiphersList
34+ | NeedsReset
35+ | SendMasterPassword String
33
36
34instance encodeJsonCmd :: EncodeJson Cmd where
37instance encodeJsonCmd :: EncodeJson Cmd where
35 encodeJson = genericEncodeAeson Argonaut.defaultOptions
38 encodeJson = genericEncodeAeson Argonaut.defaultOptions
@@ -64,10 +67,27 @@ derive instance genericSub_LoadCiphers_List :: Generic Sub_LoadCiphers_List _
64derive instance eqSub_LoadCiphers_List :: Eq Sub_LoadCiphers_List
67derive instance eqSub_LoadCiphers_List :: Eq Sub_LoadCiphers_List
65derive instance ordSub_LoadCiphers_List :: Ord Sub_LoadCiphers_List
68derive instance ordSub_LoadCiphers_List :: Ord Sub_LoadCiphers_List
66
69
70+newtype Sub_NeedsMasterPassword =
71+ Sub_NeedsMasterPassword {
72+ login :: String
73+ , server :: String
74+ }
75+
76+instance encodeJsonSub_NeedsMasterPassword :: EncodeJson Sub_NeedsMasterPassword where
77+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
78+instance decodeJsonSub_NeedsMasterPassword :: DecodeJson Sub_NeedsMasterPassword where
79+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
80+derive instance genericSub_NeedsMasterPassword :: Generic Sub_NeedsMasterPassword _
81+derive instance eqSub_NeedsMasterPassword :: Eq Sub_NeedsMasterPassword
82+derive instance ordSub_NeedsMasterPassword :: Ord Sub_NeedsMasterPassword
83+
67data Sub =
84data Sub =
68 Error String
85 Error String
69 | LoadCiphers Sub_LoadCiphers_List
86 | LoadCiphers Sub_LoadCiphers_List
87+ | LoginSuccessful
70 | NeedsLogin
88 | NeedsLogin
89+ | NeedsMasterPassword Sub_NeedsMasterPassword
90+ | Reset
71
91
72instance encodeJsonSub :: EncodeJson Sub where
92instance encodeJsonSub :: EncodeJson Sub where
73 encodeJson = genericEncodeAeson Argonaut.defaultOptions
93 encodeJson = genericEncodeAeson Argonaut.defaultOptions
bw/src/Data/EncString.js → backend/Data/EncString.jsrenamed

The content is the same.

bw/src/Data/EncString.purs → backend/Data/EncString.pursrenamed

The content is the same.

backend/Data/JNullable.pursnew file+27
@@ -0,0 +1,27 @@
1+module Data.JNullable where
2+
3+import Prelude
4+
5+import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
6+import Data.Maybe (Maybe(..))
7+import Data.Nullable (Nullable, null, toNullable)
8+import Data.Nullable as Nullable
9+
10+newtype JNullable a
11+ = JNullable (Nullable a)
12+
13+derive newtype instance Eq a => Eq (JNullable a)
14+derive newtype instance Ord a => Ord (JNullable a)
15+derive newtype instance Show a => Show (JNullable a)
16+
17+instance EncodeJson a => EncodeJson (JNullable a) where
18+ encodeJson (JNullable a) = encodeJson $ Nullable.toMaybe a
19+
20+instance DecodeJson a => DecodeJson (JNullable a) where
21+ decodeJson json = JNullable <<< Nullable.toNullable <$> decodeJson json
22+
23+jnull :: forall a. JNullable a
24+jnull = JNullable null
25+
26+nullify :: forall a. a -> JNullable a
27+nullify = Just >>> toNullable >>> JNullable
backend/Data/JOpt.pursnew file+20
@@ -0,0 +1,20 @@
1+module Data.JOpt where
2+
3+import Prelude
4+
5+import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
6+import Data.Undefined.NoProblem (Opt)
7+import Data.Undefined.NoProblem as Opt
8+
9+newtype JOpt a
10+ = JOpt (Opt a)
11+
12+derive newtype instance Eq a => Eq (JOpt a)
13+derive newtype instance Ord a => Ord (JOpt a)
14+derive newtype instance Show a => Show (JOpt a)
15+
16+instance EncodeJson a => EncodeJson (JOpt a) where
17+ encodeJson (JOpt a) = encodeJson $ Opt.toMaybe a
18+
19+instance DecodeJson a => DecodeJson (JOpt a) where
20+ decodeJson json = JOpt <<< Opt.fromMaybe <$> decodeJson json
backend/Data/ShowableJson.pursnew file+19
@@ -0,0 +1,19 @@
1+module Data.ShowableJson
2+ ( ShowableJson(..)
3+ )
4+ where
5+
6+import Prelude
7+
8+import Data.Argonaut (class DecodeJson, class EncodeJson, Json)
9+import Data.Argonaut as Json
10+
11+newtype ShowableJson = ShowableJson Json
12+
13+instance Show ShowableJson where
14+ show (ShowableJson x) = Json.stringify x
15+
16+derive newtype instance Eq ShowableJson
17+derive newtype instance Ord ShowableJson
18+derive newtype instance EncodeJson ShowableJson
19+derive newtype instance DecodeJson ShowableJson
bw/src/Data/SymmetricCryptoKey.js → backend/Data/SymmetricCryptoKey.jsrenamed

The content is the same.

bw/src/Data/SymmetricCryptoKey.purs → backend/Data/SymmetricCryptoKey.pursrenamed+3−3
@@ -2,12 +2,12 @@ module Data.SymmetricCryptoKey where
2
2
3import Prelude
3import Prelude
4import Data.ArrayBuffer.Types (ArrayBuffer)
4import Data.ArrayBuffer.Types (ArrayBuffer)
5−import Data.Nullable (Nullable)
5+import Data.JNullable (JNullable)
6
6
7type SymmetricCryptoKey
7type SymmetricCryptoKey
8 = { key :: ArrayBuffer
8 = { key :: ArrayBuffer
9− , encKey :: Nullable ArrayBuffer
9+ , encKey :: JNullable ArrayBuffer
10− , macKey :: Nullable ArrayBuffer
10+ , macKey :: JNullable ArrayBuffer
11 , encType :: EncryptionType
11 , encType :: EncryptionType
12 , keyB64 :: String
12 , keyB64 :: String
13 , encKeyB64 :: String
13 , encKeyB64 :: String
bw/src/Data/Timestamp.js → backend/Data/Timestamp.jsrenamed

The content is the same.

bw/src/Data/Timestamp.purs → backend/Data/Timestamp.pursrenamed+8
@@ -1,6 +1,8 @@
1module Data.Timestamp where
1module Data.Timestamp where
2
2
3import Prelude
3import Prelude
4+
5+import Data.Argonaut (class DecodeJson, class EncodeJson)
4import Data.Bifunctor (lmap)
6import Data.Bifunctor (lmap)
5import Data.DateTime (DateTime)
7import Data.DateTime (DateTime)
6import Data.DateTime.Parsing as DateTime
8import Data.DateTime.Parsing as DateTime
@@ -12,6 +14,12 @@ import Effect.Exception (throw)
12newtype Timestamp
14newtype Timestamp
13 = Timestamp String
15 = Timestamp String
14
16
17+derive newtype instance Show Timestamp
18+derive newtype instance Ord Timestamp
19+derive newtype instance Eq Timestamp
20+derive newtype instance EncodeJson Timestamp
21+derive newtype instance DecodeJson Timestamp
22+
15toDateTime :: Timestamp -> Effect DateTime
23toDateTime :: Timestamp -> Effect DateTime
16toDateTime (Timestamp ts) =
24toDateTime (Timestamp ts) =
17 either throw pure
25 either throw pure
backend/Localstorage.pursnew file+33
@@ -0,0 +1,33 @@
1+module Localstorage where
2+
3+import Prelude
4+import Data.Argonaut (class DecodeJson, class EncodeJson)
5+import Data.Argonaut as Json
6+import Data.Either (Either(..))
7+import Data.Maybe (Maybe(..))
8+import Effect (Effect)
9+import Effect.Exception as Exc
10+import Type.Prelude (Proxy)
11+import Web.Storage.Storage (Storage)
12+import Web.Storage.Storage as Storage
13+
14+class TypeId a where
15+ typeId :: Proxy a -> String
16+
17+class StorageKey k t | k -> t where
18+ storageKey :: k -> String
19+
20+store :: forall k t. StorageKey k t => EncodeJson t => Storage -> k -> t -> Effect Unit
21+store storage key t = Storage.setItem (storageKey key) (Json.stringify $ Json.encodeJson $ t) storage
22+
23+get :: forall k t. StorageKey k t => DecodeJson t => Storage -> k -> Effect (Maybe t)
24+get storage key =
25+ Storage.getItem (storageKey key) storage
26+ >>= \x -> case x of
27+ Nothing -> pure Nothing
28+ Just y -> case Json.parseJson y >>= Json.decodeJson of
29+ Left err -> Exc.throw $ Json.printJsonDecodeError err
30+ Right y -> pure $ Just y
31+
32+delete :: forall k t. StorageKey k t => Storage -> k -> Effect Unit
33+delete storage key = Storage.removeItem (storageKey key) storage
backend/Main.purs+147−30
@@ -4,20 +4,22 @@ module Main
4
4
5import Bridge
5import Bridge
6import Prelude
6import Prelude
7+
7−import BW (CryptoService)
8+import BW (ApiService, CryptoService, Hash(..), Services)
8import BW as WB
9import BW as WB
9−import BW.Logic (decrypt, liftPromise)
10+import BW.Logic (decrypt, hashPassword, liftPromise)
10import BW.Logic as Logic
11import BW.Logic as Logic
11−import BW.Types (Email(..), EncryptedString(..), Password(..), Urls, CipherResponse)
12+import BW.Types (CipherResponse, Email(..), EncryptedString(..), IdentityTokenResponse, Password(..), PreloginResponse, SyncResponse, Urls, PreloginRequest)
12import Bridge as Bridge
13import Bridge as Bridge
13import Control.El (class HasL, Al, L(..), l)
14import Control.El (class HasL, Al, L(..), l)
14import Control.Monad.Reader (runReaderT)
15import Control.Monad.Reader (runReaderT)
15import Control.Promise (Promise)
16import Control.Promise (Promise)
16import Control.Promise as Promise
17import Control.Promise as Promise
17−import Data.Argonaut (class EncodeJson, encodeJson)
18+import Data.Argonaut (class DecodeJson, class EncodeJson, encodeJson)
18import Data.Argonaut as Json
19import Data.Argonaut as Json
19import Data.Array as A
20import Data.Array as A
20import Data.Either (Either(..))
21import Data.Either (Either(..))
22+import Data.JNullable (JNullable(..), jnull, nullify)
21import Data.Maybe (Maybe(..))
23import Data.Maybe (Maybe(..))
22import Data.Nullable (Nullable, null, toNullable)
24import Data.Nullable (Nullable, null, toNullable)
23import Data.Nullable as Nullable
25import Data.Nullable as Nullable
@@ -32,40 +34,124 @@ import Effect.Aff.Class as Aff
32import Effect.Class (liftEffect)
34import Effect.Class (liftEffect)
33import Effect.Class.Console (log)
35import Effect.Class.Console (log)
34import Effect.Exception as Exc
36import Effect.Exception as Exc
37+import Effect.Ref as Ref
35import Elm as Elm
38import Elm as Elm
39+import FFI (Elm)
36import FFI as FFI
40import FFI as FFI
41+import Localstorage (class StorageKey, storageKey)
42+import Localstorage as Storage
43+import Localstorage as Storage
37import Prelude as Array
44import Prelude as Array
38import Unsafe.Coerce (unsafeCoerce)
45import Unsafe.Coerce (unsafeCoerce)
46+import Web.HTML (Window)
47+import Web.HTML as Html
48+import Web.HTML.Window as Window
49+import Web.Storage.Storage (Storage)
50+import Web.Storage.Storage as WebStorage
39
51
40main :: Effect Unit
52main :: Effect Unit
41main = do
53main = do
42 app <- FFI.startElm "elm"
54 app <- FFI.startElm "elm"
55+ storage <- Html.window >>= Window.localStorage
43 services <- WB.getServices
56 services <- WB.getServices
57+ masterKeyRef <- Ref.new Nothing
58+ let run act = do
59+ maybeKey <- Ref.read masterKeyRef
60+ runElmAff app $ flip runReaderT {app: app, crypto : services.crypto, storage: storage} $
61+ case maybeKey of
62+ Just masterKey -> do
63+ token <- getOrReset storage TokenKey
64+ key <- Logic.makeDecryptionKey masterKey token.key
65+ liftEffect $ runElmAff app $ runReaderT act
66+ {app : app , storage : storage, services : services, crypto : services.crypto
67+ , key : key}
68+ Nothing -> requestMasterPassword
44 Elm.subscribe app \cmd -> case cmd of
69 Elm.subscribe app \cmd -> case cmd of
45 Bridge.Login (Bridge.Cmd_Login { email, server, password }) -> do
70 Bridge.Login (Bridge.Cmd_Login { email, server, password }) -> do
46 let
71 let
47 urls = baseUrl server
72 urls = baseUrl server
48 runElmAff app do
73 runElmAff app do
49− unauthedApi <- liftPromise $ services.getApi urls null
74+ unauthedApi <- liftPromise $ services.getApi urls jnull
50− { masterKey, token, key } <-
51− flip runReaderT { api: unauthedApi, crypto: services.crypto } do
75+ flip runReaderT { api: unauthedApi, crypto: services.crypto, storage:
52− token <-
76+ storage, cryptoFunctions: services.cryptoFunctions } do
77+ {token, prelogin: (prelogin :: PreloginResponse)} <-
78+ try (do
79+ prelogin <- liftPromise $ unauthedApi.postPrelogin {email}
53− try (Logic.getLogInRequestToken (Email email) (Password password))
80+ token <- (Logic.getLogInRequestToken prelogin (Email email) (Password password))
81+ pure {token, prelogin}
82+ )
54− >>= \et -> case et of
83+ >>= \et -> case et of
55− Left _ -> liftEffect $ Exc.throw "Could not log in. Please check your data and try again."
84+ Left _ -> liftEffect $ Exc.throw "Could not log in. Please check your data and try again."
56− Right t -> pure t
85+ Right t -> pure t
57− masterKey <- Logic.makePreloginKey (Email email) (Password password)
86+ masterKey <- Logic.makePreloginKey prelogin (Email email) (Password password)
58− key <- Logic.makeDecryptionKey masterKey token.key
59− pure { token, masterKey, key }
60− api <- liftPromise $ services.getApi urls (nullify token)
87+ api <- liftPromise $ services.getApi urls (nullify token)
61− _ <-
62− flip runReaderT { api: api, crypto: services.crypto, key: key } do
63 sync <- liftPromise $ api.getSync unit
88 sync <- liftPromise $ api.getSync unit
64− processedCiphers <- traverse processCipher sync.ciphers
89+ hash <- hashPassword (Password password)
90+ log $ show sync
91+ liftEffect $ Ref.write (Just masterKey) masterKeyRef
92+ liftEffect $ Storage.store storage UrlsKey server
93+ liftEffect $ Storage.store storage MasterPasswordHashKey hash
94+ liftEffect $ Storage.store storage PreloginResponseKey prelogin
95+ liftEffect $ Storage.store storage SyncKey sync
96+ liftEffect $ Storage.store storage TokenKey token
65− liftEffect $ Elm.send app $ LoadCiphers $ Bridge.Sub_LoadCiphers_List (A.take 3 processedCiphers)
97+ liftEffect $ Elm.send app Bridge.LoginSuccessful
66 pure unit
98 pure unit
67 pure unit
99 pure unit
68− Bridge.Init -> Elm.send app (Bridge.NeedsLogin)
100+ Bridge.NeedCiphersList -> run do
101+ sync <- getOrReset storage SyncKey
102+ ciphers <- traverse processCipher sync.ciphers
103+ liftEffect $ Elm.send app $ Bridge.LoadCiphers $ Bridge.Sub_LoadCiphers_List ciphers
104+ pure unit
105+ Bridge.NeedsReset -> pure unit
106+ Bridge.SendMasterPassword masterPassword -> runElmAff app $ do
107+ flip runReaderT {app: app, storage: storage, crypto: services.crypto, cryptoFunctions: services.cryptoFunctions} $ do
108+ hash <- getOrReset storage MasterPasswordHashKey
109+ newHash <- hashPassword (Password masterPassword)
110+ if hash == newHash
111+ then do
112+ sync <- getOrReset storage SyncKey
113+ prelogin <- getOrReset storage PreloginResponseKey
114+ masterKey <- Logic.makePreloginKey prelogin (Email sync.profile.email) (Password masterPassword)
115+ liftEffect $ Ref.write (Just masterKey) masterKeyRef
116+ liftEffect $ Elm.send app Bridge.LoginSuccessful
117+ else do
118+ liftEffect $ Elm.send app $ Bridge.Error "The password is wrong. Please try again."
119+ requestMasterPassword
120+ pure unit
121+ Bridge.Init -> do
122+ liftEffect $ Storage.get storage TokenKey >>= \x -> case x of
123+ Just _ -> liftEffect $ Elm.send app Bridge.LoginSuccessful
124+ Nothing -> Elm.send app Bridge.NeedsLogin
125+
126+data SyncKey = SyncKey
127+
128+instance StorageKey SyncKey SyncResponse where
129+ storageKey SyncKey = "sync"
130+
131+data TokenKey
132+ = TokenKey
133+
134+instance StorageKey TokenKey IdentityTokenResponse where
135+ storageKey TokenKey = "token"
136+
137+data UrlsKey
138+ = UrlsKey
139+
140+instance StorageKey UrlsKey String where
141+ storageKey UrlsKey = "urls"
142+
143+data PreloginResponseKey
144+ = PreloginResponseKey
145+
146+instance StorageKey PreloginResponseKey PreloginResponse where
147+ storageKey PreloginResponseKey = "prelogin-reponse"
148+
149+data MasterPasswordHashKey
150+ = MasterPasswordHashKey
151+
152+instance StorageKey MasterPasswordHashKey Hash where
153+ storageKey MasterPasswordHashKey = "master-password-hash"
154+
69
155
70processCipher ::
156processCipher ::
71 forall r.
157 forall r.
@@ -91,14 +177,45 @@ runElmAff app =
91baseUrl :: String -> Urls
177baseUrl :: String -> Urls
92baseUrl server =
178baseUrl server =
93 { base: nullify server
179 { base: nullify server
94− , webVault: null
180+ , webVault: jnull
95− , api: null
181+ , api: jnull
96− , identity: null
182+ , identity: jnull
97− , icons: null
183+ , icons: jnull
98− , notifications: null
184+ , notifications: jnull
99− , events: null
185+ , events: jnull
100− , keyConnector: null
186+ , keyConnector: jnull
101 }
187 }
102
188
103−nullify :: forall a. a -> Nullable a
189+getOrReset :: forall k t r. HasL "app" Elm r =>
104−nullify = Just >>> toNullable
190+ StorageKey k t => DecodeJson t => Storage -> k -> Al r t
191+getOrReset storage key = do
192+ let handleError = do
193+ app <- l (L :: L "app")
194+ liftEffect $ WebStorage.clear storage
195+ liftEffect $ Elm.send app Bridge.Reset
196+ liftEffect $ Exc.throw "Encountered an internal error. Resetting the app."
197+ try (liftEffect $ Storage.get storage key) >>= \x -> case x of
198+ Left err -> do
199+ liftEffect $ log $ show err
200+ handleError
201+ Right Nothing -> handleError
202+ Right (Just y) -> pure y
203+
204+getAuthedApi :: forall r. HasL "services" Services r => HasL "storage" Storage r
205+ => HasL "app" Elm r =>
206+ Al r ApiService
207+getAuthedApi = do
208+ storage <- l (L :: L "storage")
209+ services <- l (L :: L "services")
210+ token <- getOrReset storage TokenKey
211+ url <- getOrReset storage UrlsKey
212+ liftPromise $ services.getApi (baseUrl url) (nullify token)
213+
214+requestMasterPassword :: forall r. HasL "storage" Storage r => HasL "app" Elm r => Al r Unit
215+requestMasterPassword = do
216+ storage <- l (L :: L "storage")
217+ app <- l (L :: L "app")
218+ url <- getOrReset storage UrlsKey
219+ sync <- getOrReset storage SyncKey
220+ liftEffect $ Elm.send app $ Bridge.NeedsMasterPassword
221+ $ Bridge.Sub_NeedsMasterPassword {server: url, login: sync.profile.email}
bridge.yaml+9
@@ -5,6 +5,12 @@ Sub:
5 List:
5 List:
6 name: String
6 name: String
7 date: String
7 date: String
8+- LoginSuccessful
9+- NeedsMasterPassword:
10+ server: String
11+ login: String
12+- Reset
13+
8
14
9Cmd:
15Cmd:
10- Init
16- Init
@@ -12,3 +18,6 @@ Cmd:
12 email: String
18 email: String
13 server: String
19 server: String
14 password: String
20 password: String
21+- NeedCiphersList
22+- SendMasterPassword: String
23+- NeedsReset
bw/spago.dhalldeleted−20
@@ -1,20 +0,0 @@
1−{ name = "bw"
2−, dependencies =
3− [ "aff"
4− , "aff-promise"
5− , "argonaut"
6− , "argonaut-generic"
7− , "arraybuffer-types"
8− , "console"
9− , "datetime-parsing"
10− , "effect"
11− , "el"
12− , "foreign"
13− , "maybe"
14− , "newtype"
15− , "prelude"
16− , "undefined-is-not-a-problem"
17− ]
18−, packages = ../packages.dhall
19−, sources = [ "src/**/*.purs" ]
20−}
bw/src/BW.pursdeleted−34
@@ -1,34 +0,0 @@
1−module BW where
2−
3−import BW.Types
4−import Prelude
5−import Control.Promise (Promise)
6−import Data.ArrayBuffer.Types (ArrayBuffer)
7−import Data.EncString (EncString)
8−import Data.Function.Uncurried (Fn2, Fn3, Fn4)
9−import Data.Nullable (Nullable)
10−import Data.SymmetricCryptoKey (SymmetricCryptoKey)
11−import Effect (Effect)
12−
13−type CryptoService
14− = { makeKey :: Fn4 Password String KDF Int (Promise SymmetricCryptoKey)
15− , hashPassword :: Fn3 Password SymmetricCryptoKey (Nullable HashPurpose) (Promise Hash)
16− , decryptToUtf8 :: Fn2 EncString SymmetricCryptoKey (Promise String)
17− , decryptToBytes :: Fn2 EncString SymmetricCryptoKey (Promise ArrayBuffer)
18− , stretchKey :: SymmetricCryptoKey -> Promise SymmetricCryptoKey
19− }
20−
21−type ApiService
22− = { postPrelogin :: PreloginRequest -> Promise PreloginResponse
23− , getProfile :: Unit -> Promise ProfileResponse
24− -- TODO: There can really be more request and response types. Maybe handle this later.
25− , postIdentityToken :: PasswordTokenRequest -> Promise IdentityTokenResponse
26− , getSync :: Unit -> Promise SyncResponse
27− }
28−
29−type Services
30− = { crypto :: CryptoService
31− , getApi :: Urls -> Nullable IdentityTokenResponse -> Promise ApiService
32− }
33−
34−foreign import getServices :: Effect Services
bw/src/Data/ShowableJson.pursdeleted−14
@@ -1,14 +0,0 @@
1−module Data.ShowableJson
2− ( ShowableJson(..)
3− )
4− where
5−
6−import Prelude
7−
8−import Data.Argonaut (Json)
9−import Data.Argonaut as Json
10−
11−newtype ShowableJson = ShowableJson Json
12−
13−instance Show ShowableJson where
14− show (ShowableJson x) = Json.stringify x
el/test/Main.purs+10−10
@@ -1,11 +1,9 @@
1module Test.Main
1module Test.Main
2 ( main
2 ( main
3− )
4− where
3+ ) where
5
4
6import Prelude
5import Prelude
7import Test.Unit
6import Test.Unit
8−
9import Control.El
7import Control.El
10import Control.Monad.Reader (runReaderT)
8import Control.Monad.Reader (runReaderT)
11import Effect (Effect)
9import Effect (Effect)
@@ -15,13 +13,15 @@ import Test.Unit.Output.Fancy (runTest)
15import Type.Proxy (Proxy(..))
13import Type.Proxy (Proxy(..))
16
14
17main ∷ Effect Unit
15main ∷ Effect Unit
16+main =
18−main = launchAff_ $ void $ runTest do
17+ launchAff_ $ void
18+ $ runTest do
19− suite "sync code" do
19+ suite "sync code" do
20− test "arithmetic" do
20+ test "arithmetic" do
21− x <- runReaderT singleHandler {handle: 2, ignore: "Hello"}
21+ x <- runReaderT singleHandler { handle: 2, ignored: "Hello" }
22− Assert.equal x 2
22+ Assert.equal x 2
23−
24
23
25singleHandler :: forall r. HasL "handle" Int r => Al r Int
24singleHandler :: forall r. HasL "handle" Int r => Al r Int
26singleHandler = do
25singleHandler = do
27− l (L :: L "handle")
26+ x <- l (L :: L "handle")
27+ pure $ x + 1
frontend/Bridge.elm+38
@@ -34,12 +34,18 @@ jsonEncCmd_Login val =
34type Cmd =
34type Cmd =
35 Init
35 Init
36 | Login Cmd_Login
36 | Login Cmd_Login
37+ | NeedCiphersList
38+ | NeedsReset
39+ | SendMasterPassword String
37
40
38jsonDecCmd : Json.Decode.Decoder ( Cmd )
41jsonDecCmd : Json.Decode.Decoder ( Cmd )
39jsonDecCmd =
42jsonDecCmd =
40 let jsonDecDictCmd = Dict.fromList
43 let jsonDecDictCmd = Dict.fromList
41 [ ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
44 [ ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
42 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
45 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
46+ , ("NeedCiphersList", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedCiphersList))
47+ , ("NeedsReset", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsReset))
48+ , ("SendMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map SendMasterPassword (Json.Decode.string)))
43 ]
49 ]
44 jsonDecObjectSetCmd = Set.fromList []
50 jsonDecObjectSetCmd = Set.fromList []
45 in decodeSumTaggedObject "Cmd" "tag" "contents" jsonDecDictCmd jsonDecObjectSetCmd
51 in decodeSumTaggedObject "Cmd" "tag" "contents" jsonDecDictCmd jsonDecObjectSetCmd
@@ -49,6 +55,9 @@ jsonEncCmd val =
49 let keyval v = case v of
55 let keyval v = case v of
50 Init -> ("Init", encodeValue (Json.Encode.list identity []))
56 Init -> ("Init", encodeValue (Json.Encode.list identity []))
51 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
57 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
58+ NeedCiphersList -> ("NeedCiphersList", encodeValue (Json.Encode.list identity []))
59+ NeedsReset -> ("NeedsReset", encodeValue (Json.Encode.list identity []))
60+ SendMasterPassword v1 -> ("SendMasterPassword", encodeValue (Json.Encode.string v1))
52 in encodeSumTaggedObject "tag" "contents" keyval val
61 in encodeSumTaggedObject "tag" "contents" keyval val
53
62
54
63
@@ -84,17 +93,43 @@ jsonEncSub_LoadCiphers_List val = (Json.Encode.list jsonEncSub_LoadCiphers) val
84
93
85
94
86
95
96+type alias Sub_NeedsMasterPassword =
97+ { login: String
98+ , server: String
99+ }
100+
101+jsonDecSub_NeedsMasterPassword : Json.Decode.Decoder ( Sub_NeedsMasterPassword )
102+jsonDecSub_NeedsMasterPassword =
103+ Json.Decode.succeed (\plogin pserver -> {login = plogin, server = pserver})
104+ |> required "login" (Json.Decode.string)
105+ |> required "server" (Json.Decode.string)
106+
107+jsonEncSub_NeedsMasterPassword : Sub_NeedsMasterPassword -> Value
108+jsonEncSub_NeedsMasterPassword val =
109+ Json.Encode.object
110+ [ ("login", Json.Encode.string val.login)
111+ , ("server", Json.Encode.string val.server)
112+ ]
113+
114+
115+
87type Sub =
116type Sub =
88 Error String
117 Error String
89 | LoadCiphers Sub_LoadCiphers_List
118 | LoadCiphers Sub_LoadCiphers_List
119+ | LoginSuccessful
90 | NeedsLogin
120 | NeedsLogin
121+ | NeedsMasterPassword Sub_NeedsMasterPassword
122+ | Reset
91
123
92jsonDecSub : Json.Decode.Decoder ( Sub )
124jsonDecSub : Json.Decode.Decoder ( Sub )
93jsonDecSub =
125jsonDecSub =
94 let jsonDecDictSub = Dict.fromList
126 let jsonDecDictSub = Dict.fromList
95 [ ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
127 [ ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
96 , ("LoadCiphers", Json.Decode.lazy (\_ -> Json.Decode.map LoadCiphers (jsonDecSub_LoadCiphers_List)))
128 , ("LoadCiphers", Json.Decode.lazy (\_ -> Json.Decode.map LoadCiphers (jsonDecSub_LoadCiphers_List)))
129+ , ("LoginSuccessful", Json.Decode.lazy (\_ -> Json.Decode.succeed LoginSuccessful))
97 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
130 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
131+ , ("NeedsMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map NeedsMasterPassword (jsonDecSub_NeedsMasterPassword)))
132+ , ("Reset", Json.Decode.lazy (\_ -> Json.Decode.succeed Reset))
98 ]
133 ]
99 jsonDecObjectSetSub = Set.fromList []
134 jsonDecObjectSetSub = Set.fromList []
100 in decodeSumTaggedObject "Sub" "tag" "contents" jsonDecDictSub jsonDecObjectSetSub
135 in decodeSumTaggedObject "Sub" "tag" "contents" jsonDecDictSub jsonDecObjectSetSub
@@ -104,6 +139,9 @@ jsonEncSub val =
104 let keyval v = case v of
139 let keyval v = case v of
105 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
140 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
106 LoadCiphers v1 -> ("LoadCiphers", encodeValue (jsonEncSub_LoadCiphers_List v1))
141 LoadCiphers v1 -> ("LoadCiphers", encodeValue (jsonEncSub_LoadCiphers_List v1))
142+ LoginSuccessful -> ("LoginSuccessful", encodeValue (Json.Encode.list identity []))
107 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
143 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
144+ NeedsMasterPassword v1 -> ("NeedsMasterPassword", encodeValue (jsonEncSub_NeedsMasterPassword v1))
145+ Reset -> ("Reset", encodeValue (Json.Encode.list identity []))
108 in encodeSumTaggedObject "tag" "contents" keyval val
146 in encodeSumTaggedObject "tag" "contents" keyval val
109
147
frontend/Main.elm+73−15
@@ -2,13 +2,14 @@ module Main exposing (..)
2
2
3import Bridge
3import Bridge
4import Browser
4import Browser
5−import FFI exposing (getBridge)
5+import FFI exposing (getBridge, sendBridge)
6import Html exposing (..)
6import Html exposing (..)
7import Html.Attributes as Attr
7import Html.Attributes as Attr
8import Notification
8import Notification
9import Pages.Ciphers as Ciphers
9import Pages.Ciphers as Ciphers
10import Pages.Loader exposing (loader)
10import Pages.Loader exposing (loader)
11import Pages.Login as Login
11import Pages.Login as Login
12+import Pages.MasterPassword as MasterPassword
12import Pages.Navigation as Navigation
13import Pages.Navigation as Navigation
13import Utils exposing (..)
14import Utils exposing (..)
14
15
@@ -22,6 +23,12 @@ type Msg
22 | LoginMsg Login.Msg
23 | LoginMsg Login.Msg
23 | CiphersMsg Ciphers.Msg
24 | CiphersMsg Ciphers.Msg
24 | LoadCiphers Bridge.Sub_LoadCiphers_List
25 | LoadCiphers Bridge.Sub_LoadCiphers_List
26+ | OpenCiphersScreen
27+ | Reset
28+ | NeedsReset
29+ | MasterPasswordMsg MasterPassword.Msg
30+ | SendMasterPassword { password : String }
31+ | ShowMasterPasswordPage { server : String, login : String }
25
32
26
33
27type alias Model =
34type alias Model =
@@ -39,6 +46,7 @@ type PageModel
39 = LoginModel Login.Model
46 = LoginModel Login.Model
40 | LoadingPage
47 | LoadingPage
41 | CiphersModel Ciphers.Model
48 | CiphersModel Ciphers.Model
49+ | MasterPasswordModel MasterPassword.Model
42
50
43
51
44showPage : PageModel -> ( String, List (Html Msg) )
52showPage : PageModel -> ( String, List (Html Msg) )
@@ -61,6 +69,13 @@ showPage page =
61 LoadingPage ->
69 LoadingPage ->
62 ( "", [ loader ] )
70 ( "", [ loader ] )
63
71
72+ MasterPasswordModel model ->
73+ let
74+ p =
75+ MasterPassword.page masterPasswordCallbacks MasterPasswordMsg
76+ in
77+ ( p.title model, p.view model )
78+
64
79
65main : Program () Model Msg
80main : Program () Model Msg
66main =
81main =
@@ -68,23 +83,35 @@ main =
68 { init = \_ -> init
83 { init = \_ -> init
69 , view = view
84 , view = view
70 , update = update
85 , update = update
71− , subscriptions =
86+ , subscriptions = subscriptions
72− \_ ->
73− getBridge ShowError
74− (\msg ->
75− case msg of
76− Bridge.NeedsLogin ->
77− ShowLoginPage
78−
79− Bridge.Error err ->
80− ShowError err
81−
82− Bridge.LoadCiphers ciphers ->
83− LoadCiphers ciphers
84− )
85 }
87 }
86
88
87
89
90+subscriptions : Model -> Sub Msg
91+subscriptions _ =
92+ getBridge ShowError
93+ (\msg ->
94+ case msg of
95+ Bridge.NeedsLogin ->
96+ ShowLoginPage
97+
98+ Bridge.Error err ->
99+ ShowError err
100+
101+ Bridge.LoadCiphers ciphers ->
102+ LoadCiphers ciphers
103+
104+ Bridge.LoginSuccessful ->
105+ OpenCiphersScreen
106+
107+ Bridge.Reset ->
108+ Reset
109+
110+ Bridge.NeedsMasterPassword { server, login } ->
111+ ShowMasterPasswordPage { server = server, login = login }
112+ )
113+
114+
88init : ( Model, Cmd Msg )
115init : ( Model, Cmd Msg )
89init =
116init =
90 ( { notifications = []
117 ( { notifications = []
@@ -181,6 +208,30 @@ update msg model =
181 (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
208 (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
182 |> Tuple.mapFirst (\pageModel -> { model | page = CiphersModel pageModel })
209 |> Tuple.mapFirst (\pageModel -> { model | page = CiphersModel pageModel })
183
210
211+ OpenCiphersScreen ->
212+ ( { model | page = LoadingPage }, sendBridge Bridge.NeedCiphersList )
213+
214+ Reset ->
215+ init
216+
217+ ShowMasterPasswordPage { server, login } ->
218+ (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).init { server = server, login = login }
219+ |> Tuple.mapFirst (\pageModel -> { model | page = MasterPasswordModel pageModel })
220+
221+ NeedsReset ->
222+ ( { model | page = LoadingPage }, sendBridge Bridge.NeedsReset )
223+
224+ SendMasterPassword { password } ->
225+ ( { model | page = LoadingPage }, sendBridge (Bridge.SendMasterPassword password) )
226+
227+ MasterPasswordMsg imsg ->
228+ case model.page of
229+ MasterPasswordModel page ->
230+ (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).update imsg page |> processPage MasterPasswordModel
231+
232+ _ ->
233+ ( model, Cmd.none )
234+
184
235
185loginCallbacks : Login.Callbacks Msg
236loginCallbacks : Login.Callbacks Msg
186loginCallbacks =
237loginCallbacks =
@@ -190,3 +241,10 @@ loginCallbacks =
190ciphersCallbacks : Ciphers.Callbacks
241ciphersCallbacks : Ciphers.Callbacks
191ciphersCallbacks =
242ciphersCallbacks =
192 {}
243 {}
244+
245+
246+masterPasswordCallbacks : MasterPassword.Callbacks Msg
247+masterPasswordCallbacks =
248+ { submit = SendMasterPassword
249+ , reset = NeedsReset
250+ }
frontend/Pages/MasterPassword.elmnew file+85
@@ -0,0 +1,85 @@
1+module Pages.MasterPassword exposing (..)
2+
3+import Html exposing (..)
4+import Html.Attributes as Attr
5+import Html.Events as Ev
6+import Page exposing (..)
7+import Utils exposing (..)
8+
9+
10+type alias Model =
11+ { server : String
12+ , email : String
13+ , password : String
14+ }
15+
16+
17+type Msg
18+ = UpdatePassword String
19+ | Submit
20+ | Reset
21+
22+
23+type alias Callbacks emsg =
24+ { submit : { password : String } -> emsg
25+ , reset : emsg
26+ }
27+
28+
29+page : Callbacks emsg -> Page { server : String, login : String } Model Msg emsg
30+page callbacks liftMsg =
31+ { init = \cfg -> Tuple.mapSecond (Cmd.map liftMsg) (init cfg)
32+ , view = \model -> view model |> List.map (Html.map liftMsg)
33+ , update = \msg model -> update callbacks liftMsg msg model
34+ , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
35+ , title = always "Enter master password"
36+ }
37+
38+
39+init : { server : String, login : String } -> ( Model, Cmd Msg )
40+init { server, login } =
41+ ( { server = server
42+ , email = login
43+ , password = ""
44+ }
45+ , Cmd.none
46+ )
47+
48+
49+update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
50+update { submit, reset } _ msg model =
51+ case msg of
52+ UpdatePassword password ->
53+ ( Ok { model | password = password }, Cmd.none )
54+
55+ Submit ->
56+ ( Ok model, pureCmd (submit { password = model.password }) )
57+
58+ Reset ->
59+ ( Ok model, pureCmd reset )
60+
61+
62+subscriptions : Model -> Sub Msg
63+subscriptions _ =
64+ Sub.none
65+
66+
67+view : Model -> List (Html Msg)
68+view model =
69+ [ alignRight [ button [ Attr.class "p-button--negative" ] [ text "Log out" ] ]
70+ , table [ Attr.class "p-table--mobile-card" ]
71+ [ thead [] [ tr [] [ th [] [ text "Server" ], th [] [ text "Email" ] ] ]
72+ , tbody
73+ []
74+ [ tr []
75+ [ td [ Attr.attribute "data-heading" "Server" ] [ text model.server ]
76+ , td [ Attr.attribute "data-heading" "Email" ] [ text model.email ]
77+ ]
78+ ]
79+ ]
80+ , form []
81+ [ label [] [ text "Password" ]
82+ , input [ Attr.type_ "password", Attr.value model.password, Ev.onInput UpdatePassword ] []
83+ ]
84+ , alignRight [ button [ Ev.onClick Submit ] [ text "Log in" ] ]
85+ ]
packages.dhall+20−1
@@ -103,7 +103,6 @@ let upstream =
103 sha256:7a24ebdbacb2bfa27b2fc6ce3da96f048093d64e54369965a2a7b5d9892b6031
103 sha256:7a24ebdbacb2bfa27b2fc6ce3da96f048093d64e54369965a2a7b5d9892b6031
104
104
105in upstream
105in upstream
106− with bw = ./bw/spago.dhall as Location
107 with el = ./el/spago.dhall as Location
106 with el = ./el/spago.dhall as Location
108 with argonaut-aeson-generic =
107 with argonaut-aeson-generic =
109 { dependencies =
108 { dependencies =
@@ -129,3 +128,23 @@ in upstream
129 "https://github.com/ilyakooo0/purescript-argonaut-aeson-generic.git"
128 "https://github.com/ilyakooo0/purescript-argonaut-aeson-generic.git"
130 , version = "purescript-0.14"
129 , version = "purescript-0.14"
131 }
130 }
131+ with subtlecrypto =
132+ { dependencies =
133+ [ "aff"
134+ , "aff-promise"
135+ , "arraybuffer-types"
136+ , "console"
137+ , "effect"
138+ , "either"
139+ , "exceptions"
140+ , "foreign"
141+ , "functions"
142+ , "maybe"
143+ , "prelude"
144+ , "transformers"
145+ , "tuples"
146+ , "unsafe-coerce"
147+ ]
148+ , repo = "https://github.com/xc-jp/purescript-subtlecrypto.git"
149+ , version = "7999dc92b94d1b7285375cfaad4a0ae6644b71b6"
150+ }
spago.dhall+11−1
@@ -17,8 +17,10 @@ to generate this file without the comments in this block.
17 , "argonaut"
17 , "argonaut"
18 , "argonaut-aeson-generic"
18 , "argonaut-aeson-generic"
19 , "argonaut-codecs"
19 , "argonaut-codecs"
20+ , "arraybuffer"
21+ , "arraybuffer-types"
20 , "arrays"
22 , "arrays"
21− , "bw"
23+ , "bifunctors"
22 , "console"
24 , "console"
23 , "datetime"
25 , "datetime"
24 , "datetime-parsing"
26 , "datetime-parsing"
@@ -28,13 +30,21 @@ to generate this file without the comments in this block.
28 , "exceptions"
30 , "exceptions"
29 , "foldable-traversable"
31 , "foldable-traversable"
30 , "foreign"
32 , "foreign"
33+ , "functions"
31 , "lists"
34 , "lists"
32 , "maybe"
35 , "maybe"
33 , "newtype"
36 , "newtype"
34 , "nullable"
37 , "nullable"
35 , "prelude"
38 , "prelude"
39+ , "refs"
40+ , "strings"
41+ , "subtlecrypto"
36 , "transformers"
42 , "transformers"
43+ , "typelevel-prelude"
44+ , "undefined-is-not-a-problem"
37 , "unsafe-coerce"
45 , "unsafe-coerce"
46+ , "web-html"
47+ , "web-storage"
38 ]
48 ]
39, packages = ./packages.dhall
49, packages = ./packages.dhall
40, sources = [ "backend/**/*.purs" ]
50, sources = [ "backend/**/*.purs" ]