From 36faf26d4bd25152072e9efb643da71c63d7cfc3 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 15 Apr 2019 15:36:51 +0200 Subject: [PATCH] Add very rudimentary support for RemoteStorage --- Makefile | 1 + src/Applications/Brain.elm | 6 + src/Applications/Brain/Authentication.elm | 35 ++- src/Applications/Brain/Ports.elm | 8 +- src/Applications/UI.elm | 31 ++- src/Applications/UI/Authentication.elm | 250 +++++++++++++------ src/Applications/UI/Core.elm | 5 + src/Applications/UI/Kit.elm | 30 ++- src/Applications/UI/Reply.elm | 2 + src/Applications/UI/Settings.elm | 3 + src/Javascript/Workers/brain.js | 74 +++++- src/Library/Alien.elm | 7 + src/Library/Authentication.elm | 23 +- src/Library/Authentication/RemoteStorage.elm | 50 ++++ 14 files changed, 432 insertions(+), 93 deletions(-) create mode 100644 src/Library/Authentication/RemoteStorage.elm diff --git a/Makefile b/Makefile index e0475981..9f2ab1ca 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,7 @@ install: @echo "> Downloading & minifying dependencies" @mkdir -p $(VENDOR_DIR) @curl https://unpkg.com/lunr@2.3.6/lunr.js -o $(VENDOR_DIR)/lunr.js + @curl https://unpkg.com/remotestoragejs@1.2.2/release/remotestorage.js -o $(VENDOR_DIR)/remotestorage.min.js @curl https://unpkg.com/fast-text-encoding@1.0.0/text.min.js -o $(VENDOR_DIR)/text-encoding-polyfill.min.js @curl https://unpkg.com/tachyons@4.11.1/css/tachyons.min.css -o $(VENDOR_DIR)/tachyons.min.css diff --git a/src/Applications/Brain.elm b/src/Applications/Brain.elm index 2994ec45..e78ce82f 100644 --- a/src/Applications/Brain.elm +++ b/src/Applications/Brain.elm @@ -345,6 +345,9 @@ translateAlienData event = Just Alien.AuthMethod -> AuthenticationMsg (Authentication.MethodRetrieved event.data) + Just Alien.AuthRemoteStorage -> + AuthenticationMsg (Authentication.HypaethralDataRetrieved event.data) + Just Alien.FabricateSecretKey -> AuthenticationMsg Authentication.SecretKeyFabricated @@ -417,6 +420,9 @@ translateAlienError event err = Just Alien.AuthIpfs -> report Alien.AuthIpfs "Something went wrong regarding the IPFS storage. Maybe you used the wrong passphrase, or your IPFS node is offline?" + Just Alien.AuthRemoteStorage -> + report Alien.AuthRemoteStorage "I couldn't decrypt your data, maybe you used the wrong passphrase?" + Just tag -> report tag err diff --git a/src/Applications/Brain/Authentication.elm b/src/Applications/Brain/Authentication.elm index 0442d63e..c61aba7c 100644 --- a/src/Applications/Brain/Authentication.elm +++ b/src/Applications/Brain/Authentication.elm @@ -25,7 +25,7 @@ import Brain.Ports as Ports import Brain.Reply exposing (Reply(..)) import Conditional exposing (..) import Json.Decode as Decode -import Json.Encode as J +import Json.Encode as Json import Replying exposing (R3D3, do) @@ -59,22 +59,22 @@ initialCommand = type Msg - = PerformSignIn J.Value + = PerformSignIn Json.Value | PerformSignOut -- 0. Secret Key | FabricateSecretKey String | SecretKeyFabricated -- 1. Method | RetrieveMethod - | MethodRetrieved J.Value + | MethodRetrieved Json.Value -- 2. Data | RetrieveHypaethralData - | HypaethralDataRetrieved J.Value + | HypaethralDataRetrieved Json.Value -- x. Data | RetrieveEnclosedData - | EnclosedDataRetrieved J.Value - | SaveEnclosedData J.Value - | SaveHypaethralData J.Value + | EnclosedDataRetrieved Json.Value + | SaveEnclosedData Json.Value + | SaveHypaethralData Json.Value update : Msg -> Model -> R3D3 Model Msg Reply @@ -129,7 +129,7 @@ update msg model = FabricateSecretKey passphrase -> ( model , passphrase - |> J.string + |> Json.string |> Alien.broadcast Alien.FabricateSecretKey |> Ports.fabricateSecretKey , Nothing @@ -182,6 +182,14 @@ update msg model = Just Local -> Ports.requestCache (Alien.trigger Alien.AuthAnonymous) + Just (RemoteStorage { userAddress, token }) -> + [ ( "token", Json.string token ) + , ( "userAddress", Json.string userAddress ) + ] + |> Json.object + |> Alien.broadcast Alien.AuthRemoteStorage + |> Ports.requestRemoteStorage + -- ✋ Nothing -> Cmd.none @@ -239,6 +247,15 @@ update msg model = Just Local -> Ports.toCache (Alien.broadcast Alien.AuthAnonymous json) + Just (RemoteStorage { userAddress, token }) -> + [ ( "data", json ) + , ( "token", Json.string token ) + , ( "userAddress", Json.string userAddress ) + ] + |> Json.object + |> Alien.broadcast Alien.AuthRemoteStorage + |> Ports.toRemoteStorage + -- ✋ Nothing -> Cmd.none @@ -251,7 +268,7 @@ update msg model = type Termination - = Authenticated Method J.Value + = Authenticated Method Json.Value | NotAuthenticated diff --git a/src/Applications/Brain/Ports.elm b/src/Applications/Brain/Ports.elm index c9f9f821..a80f7ab6 100644 --- a/src/Applications/Brain/Ports.elm +++ b/src/Applications/Brain/Ports.elm @@ -1,4 +1,4 @@ -port module Brain.Ports exposing (fabricateSecretKey, fromAlien, receiveSearchResults, receiveTags, removeCache, removeIpfs, requestCache, requestIpfs, requestSearch, requestTags, toCache, toIpfs, toUI, updateSearchIndex) +port module Brain.Ports exposing (fabricateSecretKey, fromAlien, receiveSearchResults, receiveTags, removeCache, removeIpfs, requestCache, requestIpfs, requestRemoteStorage, requestSearch, requestTags, toCache, toIpfs, toRemoteStorage, toUI, updateSearchIndex) import Alien import Json.Encode as Json @@ -24,6 +24,9 @@ port requestCache : Alien.Event -> Cmd msg port requestIpfs : Alien.Event -> Cmd msg +port requestRemoteStorage : Alien.Event -> Cmd msg + + port requestSearch : String -> Cmd msg @@ -33,6 +36,9 @@ port requestTags : ContextForTags -> Cmd msg port toCache : Alien.Event -> Cmd msg +port toRemoteStorage : Alien.Event -> Cmd msg + + port toIpfs : Alien.Event -> Cmd msg diff --git a/src/Applications/UI.elm b/src/Applications/UI.elm index 4d8bd0b4..1b278382 100644 --- a/src/Applications/UI.elm +++ b/src/Applications/UI.elm @@ -2,6 +2,7 @@ module UI exposing (main) import Alien import Authentication +import Authentication.RemoteStorage import Browser import Browser.Navigation as Nav import Chunky exposing (..) @@ -105,7 +106,7 @@ init flags url key = -- Children ----------- - , authentication = Authentication.initialModel + , authentication = Authentication.initialModel url , backdrop = Backdrop.initialModel , equalizer = Equalizer.initialModel , queue = Queue.initialModel @@ -189,6 +190,29 @@ update msg model = Unstall -> R2.withCmd (Ports.unstall ()) model + ----------------------------------------- + -- Authentication + ----------------------------------------- + Core.ExternalAuth (Authentication.RemoteStorage _) input -> + -- TODO: + -- 1. Make request to RemoteStorage.webfingerAddress + -- 2. If proper response -> Valid RemoteStorage + -- If not -> Not a RemoteStorage -> Show notification + -- 3. Extract oauth address from webfinger response + let + origin = + Common.urlOrigin model.url + in + input + |> Authentication.RemoteStorage.parseUserAddress + |> Maybe.map (Authentication.RemoteStorage.oauthAddress { origin = origin }) + |> Maybe.map Nav.load + |> Maybe.withDefault Cmd.none + |> Tuple.pair model + + Core.ExternalAuth _ _ -> + R2.withNoCmd model + ----------------------------------------- -- Brain ----------------------------------------- @@ -278,7 +302,7 @@ update msg model = |> Ports.toBrain in { model - | authentication = Authentication.initialModel + | authentication = Authentication.initialModel model.url , sources = Sources.initialModel , tracks = Tracks.initialModel } @@ -519,6 +543,9 @@ translateReply reply = Reply.DismissNotification opts -> Core.DismissNotification opts + Reply.ExternalAuth a b -> + Core.ExternalAuth a b + Reply.FillQueue -> Core.FillQueue diff --git a/src/Applications/UI/Authentication.elm b/src/Applications/UI/Authentication.elm index 84ef50f7..56e6de79 100644 --- a/src/Applications/UI/Authentication.elm +++ b/src/Applications/UI/Authentication.elm @@ -2,6 +2,7 @@ module UI.Authentication exposing (Model(..), Msg(..), extractMethod, initialMod import Alien import Authentication exposing (Method(..)) +import Base64 import Chunky exposing (..) import Color exposing (Color) import Color.Ext as Color @@ -24,6 +25,7 @@ import UI.Kit import UI.Ports as Ports import UI.Reply exposing (Reply(..)) import UI.Svg.Elements +import Url exposing (Url) @@ -44,14 +46,37 @@ passphraseLengthErrorMessage = type Model = Authenticated Method + | InputScreen Method { input : String, placeholder : String, question : String } | NewEncryptionKeyScreen Method (Maybe String) | UpdateEncryptionKeyScreen Method (Maybe String) | Unauthenticated -initialModel : Model -initialModel = - Unauthenticated +initialModel : Url -> Model +initialModel url = + case String.split "/" (String.dropLeft 1 url.path) of + [ "authenticate", "remotestorage", encodedUserAddress ] -> + let + userAddress = + encodedUserAddress + |> Url.percentDecode + |> Maybe.andThen (Base64.decode >> Result.toMaybe) + |> Maybe.withDefault encodedUserAddress + in + url.fragment + |> Maybe.map (String.split "&") + |> Maybe.map (List.filter <| String.startsWith "access_token=") + |> Maybe.andThen List.head + |> Maybe.withDefault "" + |> String.replace "access_token=" "" + |> (\t -> + NewEncryptionKeyScreen + (RemoteStorage { userAddress = userAddress, token = t }) + Nothing + ) + + _ -> + Unauthenticated extractMethod : Model -> Maybe Method @@ -60,6 +85,9 @@ extractMethod model = Authenticated method -> Just method + InputScreen method _ -> + Just method + NewEncryptionKeyScreen method _ -> Just method @@ -76,14 +104,23 @@ extractMethod model = type Msg = Bypass - | HideEncryptionKeyScreen - | KeepPassphraseInMemory String - | ShowNewEncryptionKeyScreen Method - | ShowUpdateEncryptionKeyScreen Method + | Cancel | SignIn Method | SignInWithPassphrase Method String | SignedIn Method + ----------------------------------------- + -- Encryption + ----------------------------------------- + | KeepPassphraseInMemory String + | ShowNewEncryptionKeyScreen Method + | ShowUpdateEncryptionKeyScreen Method | UpdateEncryptionKey Method String + ----------------------------------------- + -- More Input + ----------------------------------------- + | AskForInput Method { placeholder : String, question : String } + | Input String + | ConfirmInput update : Msg -> Model -> R3D3 Model Msg Reply @@ -92,11 +129,14 @@ update msg model = Bypass -> R3.withNothing model - HideEncryptionKeyScreen -> + Cancel -> case model of Authenticated method -> R3.withNothing (Authenticated method) + InputScreen _ _ -> + R3.withNothing Unauthenticated + NewEncryptionKeyScreen _ _ -> R3.withNothing Unauthenticated @@ -106,26 +146,16 @@ update msg model = Unauthenticated -> R3.withNothing Unauthenticated - KeepPassphraseInMemory passphrase -> - case model of - NewEncryptionKeyScreen method _ -> - R3.withNothing (NewEncryptionKeyScreen method <| Just passphrase) - - UpdateEncryptionKeyScreen method _ -> - R3.withNothing (UpdateEncryptionKeyScreen method <| Just passphrase) - - _ -> - R3.withNothing model - - ShowNewEncryptionKeyScreen method -> - R3.withNothing (NewEncryptionKeyScreen method Nothing) - - ShowUpdateEncryptionKeyScreen method -> - R3.withNothing (UpdateEncryptionKeyScreen method Nothing) - SignIn method -> ( Unauthenticated - , signIn method + -- + , [ ( "method", Authentication.encodeMethod method ) + , ( "passphrase", Json.Encode.null ) + ] + |> Json.Encode.object + |> Alien.broadcast Alien.SignIn + |> Ports.toBrain + -- , Just [ ToggleLoadingScreen On ] ) @@ -138,13 +168,40 @@ update msg model = else ( Unauthenticated - , signInWithPassphrase method passphrase + -- + , [ ( "method", Authentication.encodeMethod method ) + , ( "passphrase", Json.Encode.string <| Crypto.Hash.sha256 passphrase ) + ] + |> Json.Encode.object + |> Alien.broadcast Alien.SignIn + |> Ports.toBrain + -- , Just [ ToggleLoadingScreen On ] ) SignedIn method -> R3.withNothing (Authenticated method) + ----------------------------------------- + -- Encryption + ----------------------------------------- + KeepPassphraseInMemory passphrase -> + case model of + NewEncryptionKeyScreen method _ -> + R3.withNothing (NewEncryptionKeyScreen method <| Just passphrase) + + UpdateEncryptionKeyScreen method _ -> + R3.withNothing (UpdateEncryptionKeyScreen method <| Just passphrase) + + _ -> + R3.withNothing model + + ShowNewEncryptionKeyScreen method -> + R3.withNothing (NewEncryptionKeyScreen method Nothing) + + ShowUpdateEncryptionKeyScreen method -> + R3.withNothing (UpdateEncryptionKeyScreen method Nothing) + UpdateEncryptionKey method passphrase -> if String.length passphrase < minimumPassphraseLength then ( model @@ -162,25 +219,35 @@ update msg model = , Nothing ) + ----------------------------------------- + -- More Input + ----------------------------------------- + AskForInput method opts -> + { input = "" + , placeholder = opts.placeholder + , question = opts.question + } + |> InputScreen method + |> R3.withNothing -signIn : Method -> Cmd Msg -signIn method = - [ ( "method", Authentication.encodeMethod method ) - , ( "passphrase", Json.Encode.null ) - ] - |> Json.Encode.object - |> Alien.broadcast Alien.SignIn - |> Ports.toBrain + Input string -> + case model of + InputScreen method opts -> + R3.withNothing (InputScreen method { opts | input = string }) + m -> + R3.withNothing m -signInWithPassphrase : Method -> String -> Cmd Msg -signInWithPassphrase method passphrase = - [ ( "method", Authentication.encodeMethod method ) - , ( "passphrase", Json.Encode.string <| Crypto.Hash.sha256 passphrase ) - ] - |> Json.Encode.object - |> Alien.broadcast Alien.SignIn - |> Ports.toBrain + ConfirmInput -> + case model of + InputScreen method { input } -> + ( model + , Cmd.none + , Just [ ExternalAuth method input ] + ) + + _ -> + R3.withNothing model @@ -203,26 +270,28 @@ view model = [ chunk [ T.relative ] [ img - [ onClick HideEncryptionKeyScreen + [ onClick Cancel , src "/images/diffuse-light.svg" , width 190 -- , case model of - NewEncryptionKeyScreen _ _ -> - style "cursor" "pointer" - - UpdateEncryptionKeyScreen _ _ -> - style "cursor" "pointer" + Unauthenticated -> + style "cursor" "default" _ -> - style "cursor" "default" + style "cursor" "pointer" ] [] -- Speech bubble ---------------- , case model of + InputScreen _ { question } -> + question + |> text + |> speechBubble + NewEncryptionKeyScreen _ _ -> [ text "I need a passphrase" , lineBreak @@ -253,6 +322,9 @@ view model = -- Content ----------------------------------------- , case model of + InputScreen method opts -> + inputScreen opts + NewEncryptionKeyScreen method Nothing -> encryptionKeyScreen Bypass @@ -298,26 +370,6 @@ view model = -- CHOICES -encryptionKeyScreen : Msg -> Html Msg -encryptionKeyScreen msg = - chunk - [ T.flex - , T.flex_column - ] - [ UI.Kit.textArea - [ attribute "autocomplete" "off" - , attribute "autocorrect" "off" - , attribute "spellcheck" "false" - , placeholder "anQLS9Usw24gxUi11IgVBg76z8SCWZgLKkoWIeJ1ClVmBHLRlaiA0CtvONVAMGritbgd3U45cPTxrhFU0WXaOAa8pVt186KyEccfUNyAq97" - , Html.Styled.Events.onInput KeepPassphraseInMemory - ] - , UI.Kit.button - UI.Kit.Normal - msg - (text "Continue") - ] - - choicesScreen : Html Msg choicesScreen = chunk @@ -348,11 +400,16 @@ choicesScreen = , outOfOrder = False } , choiceButton - { action = Bypass + { action = + AskForInput + (Authentication.RemoteStorage { userAddress = "", token = "" }) + { placeholder = "username@5apps.com" + , question = "What's your user address?" + } , icon = \_ _ -> Svg.map never UI.Svg.Elements.remoteStorageLogo , isLast = False , label = "RemoteStorage" - , outOfOrder = True + , outOfOrder = False } , choiceButton { action = Bypass @@ -410,6 +467,55 @@ choiceButton { action, icon, isLast, label, outOfOrder } = -- ENCRYPTION KEY +encryptionKeyScreen : Msg -> Html Msg +encryptionKeyScreen msg = + slab + Html.form + [ onSubmit msg ] + [ T.flex + , T.flex_column + ] + [ UI.Kit.textArea + [ attribute "autocomplete" "off" + , attribute "autocorrect" "off" + , attribute "spellcheck" "false" + , placeholder "anQLS9Usw24gxUi11IgVBg76z8SCWZgLKkoWIeJ1ClVmBHLRlaiA0CtvONVAMGritbgd3U45cPTxrhFU0WXaOAa8pVt186KyEccfUNyAq97" + , Html.Styled.Events.onInput KeepPassphraseInMemory + ] + , UI.Kit.button + UI.Kit.Normal + Bypass + (text "Continue") + ] + + + +-- INPUT SCREEN + + +inputScreen : { question : String, input : String, placeholder : String } -> Html Msg +inputScreen opts = + slab + Html.form + [ onSubmit ConfirmInput ] + [ T.flex + , T.flex_column + ] + [ UI.Kit.textFieldAlt + [ placeholder opts.placeholder + , Html.Styled.Events.onInput Input + ] + , UI.Kit.button + UI.Kit.Normal + Bypass + (text "Continue") + ] + + + +-- SPEECH BUBBLE + + speechBubble : Html msg -> Html msg speechBubble contents = brick diff --git a/src/Applications/UI/Core.elm b/src/Applications/UI/Core.elm index 7233ba7b..6df64d97 100644 --- a/src/Applications/UI/Core.elm +++ b/src/Applications/UI/Core.elm @@ -1,6 +1,7 @@ module UI.Core exposing (Flags, Model, Msg(..)) import Alien +import Authentication import Browser import Browser.Navigation as Nav import Common exposing (Switch(..)) @@ -93,6 +94,10 @@ type Msg | SetAudioIsLoading Bool | SetAudioIsPlaying Bool | Unstall + ----------------------------------------- + -- Authentication + ----------------------------------------- + | ExternalAuth Authentication.Method String ----------------------------------------- -- Brain ----------------------------------------- diff --git a/src/Applications/UI/Kit.elm b/src/Applications/UI/Kit.elm index 87b51d72..f6ffeb29 100644 --- a/src/Applications/UI/Kit.elm +++ b/src/Applications/UI/Kit.elm @@ -1,4 +1,4 @@ -module UI.Kit exposing (ButtonType(..), button, buttonFocus, buttonLink, buttonWithColor, buttonWithOptions, canister, centeredContent, checkbox, colorKit, colors, defaultFontFamilies, h1, h2, h3, headerFontFamilies, inlineIcon, inputFocus, insulationWidth, intro, label, link, logoBackdrop, navFocus, receptacle, select, textArea, textButton, textField, textFocus, vessel) +module UI.Kit exposing (ButtonType(..), button, buttonFocus, buttonLink, buttonWithColor, buttonWithOptions, canister, centeredContent, checkbox, colorKit, colors, defaultFontFamilies, h1, h2, h3, headerFontFamilies, inlineIcon, inputFocus, insulationWidth, intro, label, link, logoBackdrop, navFocus, receptacle, select, textArea, textButton, textField, textFieldAlt, textFocus, vessel) import Chunky exposing (..) import Color @@ -473,6 +473,24 @@ textField attributes = [] +textFieldAlt : List (Html.Attribute msg) -> Html msg +textFieldAlt attributes = + slab + Html.input + (css textFieldAltStyles :: attributes) + [ T.bn + , T.bg_white + , T.br2 + , T.db + , T.f6 + , T.lh_copy + , T.mb3 + , T.pa3 + , T.w_100 + ] + [] + + vessel : List (Html msg) -> Html msg vessel = brick @@ -610,6 +628,16 @@ textFieldStyles = ] +textFieldAltStyles : List Css.Style +textFieldAltStyles = + [ Css.color (Color.toElmCssColor colors.text) + , Css.maxWidth (Css.vw 87.5) + , Css.resize Css.none + , Css.width (px 292) + , textAreaFocus + ] + + vesselStyles : List Css.Style vesselStyles = [ Css.boxShadow5 (Css.px 0) (Css.px 2) (Css.px 4) (Css.px 0) (Css.rgba 0 0 0 0.2) diff --git a/src/Applications/UI/Reply.elm b/src/Applications/UI/Reply.elm index 7e6540b8..03c6b03c 100644 --- a/src/Applications/UI/Reply.elm +++ b/src/Applications/UI/Reply.elm @@ -1,6 +1,7 @@ module UI.Reply exposing (Reply(..)) import Alien +import Authentication import Common exposing (Switch(..)) import Coordinates exposing (Coordinates) import Json.Decode as Json @@ -19,6 +20,7 @@ type Reply = ActiveQueueItemChanged (Maybe Queue.Item) | AddSourceToCollection Source | DismissNotification { id : Int } + | ExternalAuth Authentication.Method String | FillQueue | GoToPage Page | InsertDemo diff --git a/src/Applications/UI/Settings.elm b/src/Applications/UI/Settings.elm index ce2ce452..87e07738 100644 --- a/src/Applications/UI/Settings.elm +++ b/src/Applications/UI/Settings.elm @@ -67,6 +67,9 @@ index model = Just Local -> text "in this browser." + Just (RemoteStorage _) -> + text "on a RemoteStorage server." + Nothing -> text "on nothing, wtf?" , lineBreak diff --git a/src/Javascript/Workers/brain.js b/src/Javascript/Workers/brain.js index d04fd701..c533b70d 100644 --- a/src/Javascript/Workers/brain.js +++ b/src/Javascript/Workers/brain.js @@ -50,6 +50,11 @@ app.ports.fabricateSecretKey.subscribe(event => { }) +function getSecretKey() { + return getFromIndex({ key: SECRET_KEY_LOCATION }) +} + + // Cache // ----- @@ -63,7 +68,7 @@ app.ports.requestCache.subscribe(event => { const dataPromise = (_ => { if (event.tag == "AUTH_ANONYMOUS") { return getFromIndex({ key: event.tag }) - .then(r => getFromIndex({ key: SECRET_KEY_LOCATION }).then(s => [r, s])) + .then(r => getSecretKey().then(s => [r, s])) .then(([r, s]) => r ? decrypt(s, r) : null) .then(d => d ? JSON.parse(d) : null) @@ -94,7 +99,7 @@ function toCache(key, data) { if (key == "AUTH_ANONYMOUS") { const json = JSON.stringify(data) - return getFromIndex({ key: SECRET_KEY_LOCATION }) + return getSecretKey() .then(secretKey => encrypt(secretKey, json)) .then(encryptedData => setInIndex({ key: key, data: encryptedData })) @@ -124,11 +129,10 @@ function ipfsFilePath(tag) { app.ports.requestIpfs.subscribe(event => { const path = ipfsFilePath(event.tag) - const secretKeyPromise = getFromIndex({ key: SECRET_KEY_LOCATION }) fetch("http://localhost:5001/api/v0/files/read?arg=" + path) .then(r => r.ok ? r.text() : r.json()) - .then(r => secretKeyPromise.then(s => [r, s])) + .then(r => getSecretKey().then(s => [r, s])) .then(([r, s]) => r.Code === 0 ? {} : decrypt(s, r)) .then(data => { app.ports.fromAlien.send({ @@ -153,7 +157,7 @@ app.ports.toIpfs.subscribe(event => { truncate: true }).toString() - getFromIndex({ key: SECRET_KEY_LOCATION }) + getSecretKey() .then(secretKey => encrypt(secretKey, json)) .then(data => { const formData = new FormData() @@ -173,6 +177,66 @@ app.ports.toIpfs.subscribe(event => { +// Remote Storage +// -------------- + +let rs +let rsClient + + +function remoteStorage(event) { + if (!rs) { + importScripts("/vendor/remotestorage.min.js") + + rs = new RemoteStorage({ cache: false }) + rs.access.claim("diffuse", "rw") + + rsClient = rs.scope("/diffuse-v2/") + + return new Promise(resolve => { + rs.on("connected", resolve) + rs.connect(event.data.userAddress, event.data.token) + }) + + } else { + return Promise.resolve() + + } +} + + +app.ports.requestRemoteStorage.subscribe(event => { + remoteStorage(event) + .then(_ => rsClient.getFile("diffuse.json")) + .then(r => getSecretKey().then(s => [r.data, s])) + .then(([r, s]) => r ? decrypt(s, r) : null) + .then(data => { + app.ports.fromAlien.send({ + tag: event.tag, + data: typeof data === "string" ? JSON.parse(data) : data, + error: null + }) + }) + .catch( + reportError(event) + ) +}) + + +app.ports.toRemoteStorage.subscribe(event => { + const json = JSON.stringify(event.data.data) + + remoteStorage(event) + .then(_ => getSecretKey()) + .then(secretKey => encrypt(secretKey, json)) + .then(data => rsClient.storeFile("application/json", "diffuse.json", data)) + .catch( + reportError(event) + ) +}) + + + // Search // ------ diff --git a/src/Library/Alien.elm b/src/Library/Alien.elm index c8da3003..296be148 100644 --- a/src/Library/Alien.elm +++ b/src/Library/Alien.elm @@ -24,6 +24,7 @@ type Tag | AuthEnclosedData | AuthIpfs | AuthMethod + | AuthRemoteStorage | AuthSecretKey | FabricateSecretKey | SearchTracks @@ -93,6 +94,9 @@ tagToString tag = AuthMethod -> "AUTH_METHOD" + AuthRemoteStorage -> + "AUTH_REMOTE_STORAGE" + AuthSecretKey -> "AUTH_SECRET_KEY" @@ -181,6 +185,9 @@ tagFromString string = "AUTH_METHOD" -> Just AuthMethod + "AUTH_REMOTE_STORAGE" -> + Just AuthRemoteStorage + "AUTH_SECRET_KEY" -> Just AuthSecretKey diff --git a/src/Library/Authentication.elm b/src/Library/Authentication.elm index fcbc239e..dd56044c 100644 --- a/src/Library/Authentication.elm +++ b/src/Library/Authentication.elm @@ -19,6 +19,7 @@ import Tracks.Encoding as Tracks type Method = Ipfs | Local + | RemoteStorage { userAddress : String, token : String } type alias EnclosedUserData = @@ -69,20 +70,36 @@ methodToString method = Local -> "LOCAL" + RemoteStorage { userAddress, token } -> + String.join + methodSeparator + [ "REMOTE_STORAGE" + , userAddress + , token + ] + methodFromString : String -> Maybe Method methodFromString string = - case string of - "IPFS" -> + case String.split methodSeparator string of + [ "IPFS" ] -> Just Ipfs - "LOCAL" -> + [ "LOCAL" ] -> Just Local + [ "REMOTE_STORAGE", u, t ] -> + Just (RemoteStorage { userAddress = u, token = t }) + _ -> Nothing +methodSeparator : String +methodSeparator = + "___" + + -- 🔱 ░░ ENCLOSED diff --git a/src/Library/Authentication/RemoteStorage.elm b/src/Library/Authentication/RemoteStorage.elm new file mode 100644 index 00000000..c1dc8bc9 --- /dev/null +++ b/src/Library/Authentication/RemoteStorage.elm @@ -0,0 +1,50 @@ +module Authentication.RemoteStorage exposing (RemoteStorage, oauthAddress, parseUserAddress) + +import Base64 +import Url + + + +-- 🌳 + + +type alias RemoteStorage = + { host : String + , username : String + } + + + +-- 🔱 + + +parseUserAddress : String -> Maybe RemoteStorage +parseUserAddress str = + case String.split "@" str of + [ u, h ] -> + Just { host = h, username = u } + + _ -> + Nothing + + +oauthAddress : { origin : String } -> RemoteStorage -> String +oauthAddress { origin } { host, username } = + let + ua = + (username ++ "@" ++ host) + |> Base64.encode + |> Url.percentEncode + in + String.concat + [ "https://" ++ host ++ "/rs/oauth/" ++ username + , "?redirect_uri=" ++ Url.percentEncode (origin ++ "/authenticate/remotestorage/" ++ ua) + , "&client_id=" ++ Url.percentEncode origin + , "&scope=" ++ Url.percentEncode "diffuse-v2:rw" + , "&response_type=token" + ] + + +webfingerAddress : RemoteStorage -> String +webfingerAddress { host, username } = + "https://" ++ host ++ "/.well-known/webfinger?resource=acct:" ++ username -- 2.51.2