purr

password generation and secret sharing
Log | Files | Refs | README | LICENSE

commit ca73ed7982881f8df02d6c8ca0b5df37475c41a4
parent 478384aae90fa4d605ac2141dc46af4d173a285b
Author: James Eversole <james@eversole.co>
Date:   Sun, 31 Jul 2022 17:41:54 -0500

Read encrypted secret entries from the database and decode/unencrypt appropriately, add max duration/view count entries for the database and frontend interface, update README to remove graceful JS degradation goal, remove a few wordlist entries

Diffstat:
MREADME.md | 1-
Msrc/Core/SQLite.hs | 5++++-
Msrc/Feature/Generation/wordlist.txt | 4----
Msrc/Feature/Sharing/HTTP.hs | 6++++--
Msrc/Feature/Sharing/SQLite.hs | 45++++++++++++++++++++++++++++++---------------
Msrc/Feature/Sharing/Types.hs | 9++++++---
Mviews/cassius/style.cassius | 32+++++++++++++++++++++++++++++++-
Mviews/index.hamlet | 23+++++++++++++++++++++--
8 files changed, 96 insertions(+), 29 deletions(-)

diff --git a/README.md b/README.md @@ -18,7 +18,6 @@ You shouldn't! This is [free and open-source software](https://git.eversole.co/J ## Project Goals 1. Provide a minimal and clean interface for generating and sharing passwords. 2. Maintain a clean and organized codebase that can be extended to include more utilities than originally anticipated. -3. Aim for graceful degradation when JavaScript isn't enabled. ## Development & Support Please send me an [email](mailto:james@eversole.co) or join the [Support Chat](openpgp4fpr://FEB27223219E8DB3203225350462EA0901FE08F7#a=james%40eversole.co&g=Purr%20Support&x=RbVs8iQCVnf&i=-FuzUDK_RM1&s=KgeGtFFJtkq) in [DeltaChat](https://delta.chat)! diff --git a/src/Core/SQLite.hs b/src/Core/SQLite.hs @@ -15,7 +15,10 @@ main db = do "CREATE TABLE IF NOT EXISTS pws\ \ (link TEXT PRIMARY KEY,\ \ secret TEXT,\ - \ date DATETIME DEFAULT CURRENT_TIMESTAMP)" + \ date DATETIME DEFAULT CURRENT_TIMESTAMP,\ + \ life INT,\ + \ views INT,\ + \ maxViews INT)" close conn dbPath :: PurrAction String diff --git a/src/Feature/Generation/wordlist.txt b/src/Feature/Generation/wordlist.txt @@ -175,7 +175,6 @@ bread break breast breath -breed brick bridge brief @@ -820,9 +819,7 @@ media medium meet melt -member memory -mental menu mere merely @@ -1035,7 +1032,6 @@ profit prompt proof proper -proud prove public pull diff --git a/src/Feature/Sharing/HTTP.hs b/src/Feature/Sharing/HTTP.hs @@ -26,10 +26,12 @@ routes = do post "/pw" $ do reqId <- param "userLink" res <- findByLink reqId - html $ renderPw reqId (secret <$> res) + html $ renderPw reqId res post "/new" $ do reqSecret <- param "newSec" + reqDur <- param "newSecDuration" + reqViews <- param "newSecViews" link <- genLink - insertNewSecret reqSecret (T.pack link) + insertNewSecret reqSecret reqDur (T.pack link) reqViews html $ renderPw link (Just reqSecret) diff --git a/src/Feature/Sharing/SQLite.hs b/src/Feature/Sharing/SQLite.hs @@ -9,7 +9,6 @@ import Crypto.Simple.CBC (encrypt, decrypt) import Data.Maybe (listToMaybe) import Data.Time.Clock.POSIX (getPOSIXTime) import Database.SQLite.Simple -import Database.SQLite.Simple.FromRow import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as B @@ -17,28 +16,44 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as ET import qualified Data.Text.Lazy as LT -findByLink :: String -> PurrAction (Maybe SecretEntry) +findByLink :: String -> PurrAction (Maybe T.Text) findByLink link = do - db <- dbPath - conn <- liftIO $ open db - res <- liftIO $ query conn "SELECT * from pws WHERE link = ?" (Only link) + db <- dbPath + key <- encKey + conn <- liftIO $ open db + res <- liftIO $ query conn "SELECT * from pws WHERE link = ?" (Only link) liftIO $ close conn - return $ listToMaybe res + readEncryptedSecret key res -insertNewSecret :: T.Text -> T.Text -> PurrAction () -insertNewSecret sec link = do +insertNewSecret :: T.Text -> Integer -> T.Text -> Integer -> PurrAction () +insertNewSecret sec life link maxViews = do db <- dbPath key <- encKey - encSec <- liftIO $ encrypt (B.pack key) (ET.encodeUtf8 sec) + encSec <- liftIO $ encryptSecret key sec conn <- liftIO $ open db time <- liftIO $ epochTime - liftIO $ execute conn - "INSERT INTO pws (link, secret, date) VALUES (?, ?, ?)" - (SecretEntry link (encodeSecret encSec) time) - liftIO $ close conn + liftIO $ execute conn + "INSERT INTO pws (link, secret, date, life, views, maxViews) VALUES (?, ?, ?, ?, ?, ?)" + (SecretEntry link (encodeSecret encSec) time life 0 maxViews) + liftIO $ close conn -epochTime :: IO Integer -epochTime = fmap round getPOSIXTime +readEncryptedSecret :: String -> [SecretEntry] -> PurrAction (Maybe T.Text) +readEncryptedSecret key sec = do + decKey <- liftIO + (sequence $ decryptSecret key <$> decodeSecret <$> listToMaybe sec) + return (ET.decodeLatin1 <$> decKey) encodeSecret :: B.ByteString -> T.Text encodeSecret b = ET.decodeUtf8 $ B64.encode b + +decodeSecret :: SecretEntry -> B.ByteString +decodeSecret s = B64.decodeLenient $ ET.encodeUtf8 (secret s) + +encryptSecret :: String -> T.Text -> IO B.ByteString +encryptSecret k s = encrypt (B.pack k) (ET.encodeUtf8 s) + +decryptSecret :: String -> B.ByteString -> IO B.ByteString +decryptSecret k b = decrypt (B.pack k) b + +epochTime :: IO Integer +epochTime = fmap round getPOSIXTime diff --git a/src/Feature/Sharing/Types.hs b/src/Feature/Sharing/Types.hs @@ -10,9 +10,12 @@ import Database.SQLite.Simple.FromRow import qualified Data.Text as T data SecretEntry = SecretEntry - { link :: T.Text - , secret :: T.Text - , date :: Integer + { link :: T.Text + , secret :: T.Text + , date :: Integer + , life :: Integer + , views :: Integer + , maxViews :: Integer } deriving (Show, Generic) instance FromRow SecretEntry where diff --git a/views/cassius/style.cassius b/views/cassius/style.cassius @@ -56,7 +56,7 @@ a outline: none color: #{colorOne} background: #{colorTwo} - margin: 0.5em 0 + margin: 0.5em 0 1em 0 border-style: none none solid none padding: 0.4em 0 box-sizing: border-box @@ -75,6 +75,33 @@ a color: #{colorThree} opacity: 0.5 +.numberInput + text-align: center + font-weight: 400 + font-size: 1em + width: 10% + outline: none + color: #{colorOne} + background: #{colorTwo} + margin: 0.5em 0 + border-style: none none solid none + padding: 0.4em 0 + box-sizing: border-box + -webkit-box-sizing: border-box + -moz-box-sizing: border-box + -webkit-transition: all 0.1s ease-in-out + -moz-transition: all 0.1s ease-in-out + -ms-transition: all 0.1s ease-in-out + -o-transition: all 0.1s ease-in-out + transition: all 0.1s ease-in-out + +.numberInput:focus + border-bottom: 0.2em solid #{colorThree}; + +.numberInput:focus::placeholder + color: #{colorThree} + opacity: 0.5 + .pwResult font-size: 1.5em color: #{colorFour} @@ -109,6 +136,9 @@ a width: 95% text-align: center + .numberInput + width: 25% + .title margin: 8% auto 2% auto font-size: 3em diff --git a/views/index.hamlet b/views/index.hamlet @@ -36,14 +36,33 @@ $doctype 5 <p> <input .mainInput name="newSec" - type="text" + type="text" placeholder="Enter a Secret to Share" /> + <br /> + Valid for: + <br /> + <input .numberInput + name="newSecDuration" + type="number" + min="1" + max="90" + value="20" + onkeyup="if (value < 1 || value > 90) { value = 0 }" + /> days + <input .numberInput + name="newSecViews" + type="number" + min="1" + max="60" + value="20" + onkeyup="if (value < 1 || value > 60) { value = 0 }" + /> views <button .mainButton hx-post="/new" hx-target="#requestedPw" hx-swap="outerHTML" - hx-include="[name='newSec']" + hx-include="[id='shareNew']" /> Share Secret <img class="htmx-indicator" src="/loading.svg" />