diff --git a/Main.elm b/Main.elm index b41adec..f33d3a3 100644 --- a/Main.elm +++ b/Main.elm @@ -23,6 +23,7 @@ type alias Flags = { lockAfterSeconds : Maybe Int , fxaToken : Maybe String , contentWasSyncedRemotely : Maybe String + , passphrase : Maybe String } @@ -65,7 +66,7 @@ type alias Model = , lockAfterSeconds : Maybe Int , contentWasSynced : Bool , fxaToken : Maybe String - , passphrase : String + , passphrase : Maybe String , content : String , loadedContent : -- may be desynchronized with "content", only used to redraw the @@ -98,12 +99,20 @@ init flags = else False + lock = + case flags.passphrase of + Nothing -> + True + + Just _ -> + False + model = - { lock = True + { lock = lock , lockAfterSeconds = flags.lockAfterSeconds , fxaToken = flags.fxaToken , contentWasSynced = contentWasSynced - , passphrase = "" + , passphrase = flags.passphrase , content = "" , loadedContent = "" , modified = False @@ -114,7 +123,7 @@ init flags = , gearMenuOpen = False } in - lockOnStartup model flags.lockAfterSeconds + lockOnStartup (Debug.log "Init" model) flags.lockAfterSeconds @@ -151,19 +160,51 @@ lockOnStartup model lockAfterSeconds = model ! [ getData {}, retrieveData model.fxaToken ] +encryptIfPassphrase : Maybe String -> String -> Cmd Msg +encryptIfPassphrase passphrase content = + case passphrase of + Nothing -> + Cmd.none + + Just passphrase -> + encryptData { content = content, passphrase = passphrase } + + +decryptIfPassphrase : Maybe String -> Maybe String -> Cmd Msg +decryptIfPassphrase passphrase content = + case passphrase of + Nothing -> + Cmd.none + + Just passphrase -> + decryptData { content = content, passphrase = passphrase } + + update : Msg -> Model -> ( Model, Cmd Msg ) update message model = case message of NewPassphrase passphrase -> - { model | passphrase = passphrase } ! [] + { model + | passphrase = + if passphrase == "" then + Nothing + else + Just passphrase + } + ! [] GetData -> - model ! [ getData {}, retrieveData model.fxaToken ] + { model | error = "" } ! [ savePassphrase model.passphrase, getData {}, retrieveData model.fxaToken ] DataRetrieved list -> case list of [ "pad", data ] -> - model ! [ decryptData (Debug.log "data retrieved" { content = Just data, passphrase = model.passphrase }) ] + case model.passphrase of + Nothing -> + { model | error = "No passphrase to decrypt content." } ! [] + + Just passphrase -> + model ! [ decryptIfPassphrase model.passphrase (Just data) ] [ key, value ] -> Debug.crash ("Unsupported newData key: " ++ key) @@ -213,10 +254,10 @@ update message model = ] NewError error -> - { model | lock = True, content = "", passphrase = "", error = "Wrong passphrase" } ! [] + { model | lock = True, content = "", passphrase = Nothing, error = "Wrong passphrase" } ! [] Lock -> - { model | lock = True, gearMenuOpen = False, content = "", passphrase = "", error = "" } ! [ encryptData { content = model.content, passphrase = model.passphrase } ] + { model | lock = True, gearMenuOpen = False, content = "", passphrase = Nothing } ! [ dropPassphrase {}, encryptIfPassphrase model.passphrase model.content ] DataSaved key -> case key of @@ -234,7 +275,7 @@ update message model = TimeOut debounceCount -> if debounceCount == model.debounceCount then - model ! [ encryptData { content = model.content, passphrase = model.passphrase } ] + model ! [ encryptIfPassphrase model.passphrase model.content ] else model ! [] @@ -284,10 +325,10 @@ update message model = _ = Debug.log "kinto result" result in - model ! [ saveData { key = "contentWasSynced", content = Encode.bool True } ] + model ! [ saveData { key = "contentWasSynced", content = Encode.string "true" } ] DataRetrievedFromKinto (Ok data) -> - model ! [ decryptData (Debug.log "data retrieved" { content = Just data, passphrase = model.passphrase }) ] + model ! [ decryptIfPassphrase model.passphrase (Just data) ] DataRetrievedFromKinto (Err (Kinto.ServerError 404 _ error)) -> update (UpdateContent model.loadedContent) model @@ -376,7 +417,7 @@ formView model = [ Html.Attributes.id "password" , Html.Attributes.type_ "password" , Html.Attributes.placeholder "Passphrase" - , Html.Attributes.value model.passphrase + , Html.Attributes.value (Maybe.withDefault "" model.passphrase) , Html.Events.onInput NewPassphrase ] [] @@ -654,6 +695,12 @@ port saveData : { key : String, content : Encode.Value } -> Cmd msg port dataSaved : (String -> msg) -> Sub msg +port savePassphrase : Maybe String -> Cmd msg + + +port dropPassphrase : {} -> Cmd msg + + -- Decrypt data ports diff --git a/package.json b/package.json index eac511a..e5806f7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "publish-to-gh-pages": "npm run build && gh-pages --dist ./www", "cordova-emulate-android": "npm run build && cordova emulate android", "cordova-android": "npm run build && cordova run android", - "test": "npm run lint" + "test": "npm run lint", + "web-ext-sources": "rm -fr Hoverpad-Sources.zip && zip -r Hoverpad-Sources.zip Main.elm background.js elm-package.json encryption.js index.html Makefile manifest.json package.json ports.js README.md style.css icons/" }, "repository": { "type": "git", diff --git a/ports.js b/ports.js index 8d07029..3437b1d 100644 --- a/ports.js +++ b/ports.js @@ -4,6 +4,25 @@ const CONTENT_KEY = "pad"; const IS_WEB_EXTENSION = (typeof chrome === "object" && typeof chrome.storage === "object"); +function storePassphrase(passphrase) { + // Insecurely storing the passphrase. + sessionStorage.setItem("temporaryPassphrase", btoa(passphrase)); + return Promise.resolve(null); +} + +function dropPassphrase() { + sessionStorage.removeItem("temporaryPassphrase"); + return Promise.resolve(null); +} + +function getPassphrase() { + // Insecurely retrieving the passphrase. + const passphrase = sessionStorage.getItem("temporaryPassphrase"); + if (passphrase === null) { + return null; + } + return Promise.resolve(atob(passphrase)); +} function getItem(key) { if (!IS_WEB_EXTENSION) { @@ -11,11 +30,7 @@ function getItem(key) { } else { return new Promise(function(resolve, reject) { chrome.storage.local.get(key, function(data) { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } else { - resolve(data[key] || null); - } + resolve(data[key] || null); }); }); } @@ -34,11 +49,7 @@ function setItem(key, value) { let payload = {}; payload[key] = value; chrome.storage.local.set(payload, () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } else { - resolve(null); - } + resolve(null); }); }); } @@ -76,6 +87,28 @@ function createElmApp(flags) { }); }); + app.ports.savePassphrase.subscribe(function(passphrase) { + storePassphrase(passphrase) + .then(function() { + console.log("Passphrase saved"); + }) + .catch(function(err) { + console.error(err); + app.ports.newError.send('Could not save passphrase: ' + err.message); + }); + }); + + app.ports.dropPassphrase.subscribe(function() { + dropPassphrase() + .then(function() { + console.log("Passphrase dropped"); + }) + .catch(function(err) { + console.error(err); + app.ports.newError.send('Could not drop passphrase: ' + err.message); + }); + }); + app.ports.decryptData.subscribe(function(data) { if (!data.content) { app.ports.dataDecrypted.send(null); @@ -177,14 +210,19 @@ function handleMaybeInt(maybeString) { return maybeInt; } -Promise.all([getItem('lockAfterSeconds'), getItem('bearer'), getItem('contentWasSynced')]) - .then(function(results) { - console.log("Flags", results); - const flags = {lockAfterSeconds: handleMaybeInt(results[0]), - fxaToken: results[1], - contentWasSyncedRemotely: results[2]}; - createElmApp(flags); - }) - .catch(function(err) { - console.error(err); - }); +Promise.all([ + getItem('lockAfterSeconds'), + getItem('bearer'), + getItem('contentWasSynced'), + getPassphrase() +]).then(function(results) { + const flags = { + lockAfterSeconds: handleMaybeInt(results[0]), + fxaToken: results[1], + contentWasSyncedRemotely: results[2], + passphrase: results[3] + }; + createElmApp(flags); +}).catch(function(err) { + console.error(err); +});