Extensive commenting of functions; refactor some duplicate pattern matching logic
This commit is contained in:
parent
7796fcb9b4
commit
4909bb9c96
@ -9,6 +9,7 @@ import Configuration.Dotenv
|
||||
import System.Directory (doesFileExist)
|
||||
import System.Environment (getEnv, lookupEnv)
|
||||
|
||||
-- Make the dotenv file configuration available if PURRNOFILE is not present
|
||||
main :: IO ()
|
||||
main = do
|
||||
envFile <- lookupEnv "PURRNOFILE"
|
||||
@ -16,6 +17,7 @@ main = do
|
||||
Nothing -> loadFile defaultConfig
|
||||
_ -> putStrLn "Not using dotenv file"
|
||||
|
||||
-- Check if an encryption key exists on the filesystem and create one if not
|
||||
keyFileInit :: IO ()
|
||||
keyFileInit = do
|
||||
dataPathStr <- dataPath
|
||||
@ -27,6 +29,13 @@ keyFileInit = do
|
||||
B.writeFile (dataPathStr ++ "data/encryptionKey") (encode key)
|
||||
putStrLn "Creating new encryption key; any pre-existing DB entries will not decrypt"
|
||||
|
||||
-- Read and return the encryption key on the filesystem as a ByteString
|
||||
encKey :: IO B.ByteString
|
||||
encKey = do
|
||||
dataPathStr <- dataPath
|
||||
B.readFile (dataPathStr ++ "data/encryptionKey")
|
||||
|
||||
-- Helper functions for getting the value of environment variables
|
||||
adminEmail :: IO String
|
||||
adminEmail = getEnv "ADMINEMAIL"
|
||||
|
||||
@ -41,8 +50,3 @@ dbPath = "data/Purr.sqlite"
|
||||
|
||||
confLinkLength :: IO String
|
||||
confLinkLength = getEnv "LINKLENGTH"
|
||||
|
||||
encKey :: IO B.ByteString
|
||||
encKey = do
|
||||
dataPathStr <- dataPath
|
||||
B.readFile (dataPathStr ++ "data/encryptionKey")
|
||||
|
@ -15,7 +15,7 @@ import Web.Scotty
|
||||
|
||||
app :: PurrApp ()
|
||||
app = do
|
||||
-- Middleware definition
|
||||
-- Middleware that are processed on every request
|
||||
middleware logStdoutDev
|
||||
middleware $ staticPolicy (noDots >-> addBase "data/assets/public")
|
||||
|
||||
|
@ -9,6 +9,7 @@ import Database.SQLite.Simple.FromRow
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
-- Set up SQLite database table when Purr starts if it doesn't already exist
|
||||
main :: IO ()
|
||||
main = do
|
||||
conn <- open dbPath
|
||||
|
@ -11,6 +11,7 @@ import Numeric.Natural (Natural)
|
||||
import Text.Blaze.Html
|
||||
import Web.Scotty (ActionM, ScottyM)
|
||||
|
||||
-- Vestigial types from when Purr used a ReaderT in the monad stack
|
||||
type PurrApp a = ScottyM a
|
||||
type PurrAction a = ActionM a
|
||||
type Random a = IO a
|
||||
@ -24,6 +25,7 @@ newtype UserSecret = UserSecret' String
|
||||
newtype GenLink = GenLink' String
|
||||
deriving (Eq, Ord)
|
||||
|
||||
-- This record type matches Purr's SQL table columns
|
||||
data SecretEntry = SecretEntry
|
||||
{ link :: T.Text
|
||||
, secret :: T.Text
|
||||
|
@ -14,6 +14,9 @@ import Data.Maybe (listToMaybe)
|
||||
import Prelude
|
||||
import Web.Scotty
|
||||
|
||||
{- "Generator" route that provides radio button options to share a randomly
|
||||
generated password. This usecase targets IT professionals that are setting
|
||||
up temporary account credentials for someone else. -}
|
||||
routes :: PurrApp ()
|
||||
routes = do
|
||||
get "/gen" $ do
|
||||
|
@ -4,8 +4,8 @@ import Core.Types
|
||||
import Data.List (singleton)
|
||||
import Feature.Generation.Shared (rChar, rIndex, validChars)
|
||||
|
||||
-- Generates a string containing romly generated and capitalized
|
||||
-- characters. The number of characters used is defined in the global config.dhall.
|
||||
{- Generates a string containing randomly generated and capitalized characters.
|
||||
The length is determined by the LINKLENGTH environment variable. -}
|
||||
genLink :: Int -> IO GenLink
|
||||
genLink linkLength = genLink' linkLength (return "")
|
||||
where
|
||||
|
@ -10,31 +10,35 @@ module Feature.Generation.Passwords
|
||||
) where
|
||||
|
||||
import Core.Types
|
||||
import Feature.Generation.Shared (camelCase, rCharSym, rIndex,
|
||||
import Feature.Generation.Shared (titleCase, rCharSym, rIndex,
|
||||
validChars, validNumbers,
|
||||
validSymbols)
|
||||
|
||||
import Data.FileEmbed
|
||||
import Data.List (singleton)
|
||||
|
||||
{- Suggests one of Purr's generation schemes based on the
|
||||
desired number of characters in the password. -}
|
||||
suggestedScheme :: Int -> Random Password
|
||||
suggestedScheme i
|
||||
| i > 17 = xkcd
|
||||
| i > 12 = oldschool
|
||||
| otherwise = gibberish i
|
||||
|
||||
-- XKCD-style random password generator consisting of four titlecase words.
|
||||
xkcd :: Random Password
|
||||
xkcd = do
|
||||
wOne <- rCamel
|
||||
wTwo <- rCamel
|
||||
wThree <- rCamel
|
||||
wFour <- rCamel
|
||||
wOne <- rTitle
|
||||
wTwo <- rTitle
|
||||
wThree <- rTitle
|
||||
wFour <- rTitle
|
||||
return $ Password' (wOne <> wTwo <> wThree <> wFour)
|
||||
|
||||
-- Two random title case words, four random numbers, one random symbol.
|
||||
oldschool :: Random Password
|
||||
oldschool = do
|
||||
wOne <- rCamel
|
||||
wTwo <- rCamel
|
||||
wOne <- rTitle
|
||||
wTwo <- rTitle
|
||||
nOne <- rNum
|
||||
nTwo <- rNum
|
||||
nThr <- rNum
|
||||
@ -45,6 +49,7 @@ oldschool = do
|
||||
<> show nOne <> show nTwo <> show nThr <> show nFou
|
||||
<> pure sOne)
|
||||
|
||||
-- A completely random selection of characters supported by Purr generation
|
||||
gibberish :: Int -> Random Password
|
||||
gibberish i = go i (return "")
|
||||
where
|
||||
@ -61,8 +66,8 @@ rWord = rIndex wordList
|
||||
rSym :: Random Char
|
||||
rSym = rIndex validSymbols
|
||||
|
||||
rCamel :: Random String
|
||||
rCamel = camelCase <$> rWord
|
||||
rTitle :: Random String
|
||||
rTitle = titleCase <$> rWord
|
||||
|
||||
wordList :: [String]
|
||||
wordList = lines $(embedStringFile "data/assets/wordlist.txt")
|
||||
|
@ -3,10 +3,12 @@ module Feature.Generation.Shared where
|
||||
import Data.Char (intToDigit, toLower, toUpper)
|
||||
import System.Random (randomRIO)
|
||||
|
||||
-- Take a list of anything and return a random element from it
|
||||
rIndex :: [a] -> IO a
|
||||
rIndex arr = randomRIO (0, length arr - 1)
|
||||
>>= (\i -> return $ arr !! i)
|
||||
|
||||
-- Return the input Char randomly uppercased or lowercased
|
||||
rCap :: Char -> IO Char
|
||||
rCap c = rIndex [True,False] >>= (\r -> return $ rCap' r c)
|
||||
where
|
||||
@ -14,16 +16,20 @@ rCap c = rIndex [True,False] >>= (\r -> return $ rCap' r c)
|
||||
rCap' True c = toUpper c
|
||||
rCap' False c = toLower c
|
||||
|
||||
-- Return a random character from validChars with random capitalization applied
|
||||
rChar :: IO Char
|
||||
rChar = rIndex validChars >>= rCap
|
||||
|
||||
-- Return a random character or symbol with random capitalization applied
|
||||
rCharSym :: IO Char
|
||||
rCharSym = rIndex (validChars <> validSymbols)>>= rCap
|
||||
|
||||
camelCase :: [Char] -> [Char]
|
||||
camelCase [] = []
|
||||
camelCase x = toUpper (head x) : map toLower (tail x)
|
||||
-- Takes any string and returns it in Titlecase
|
||||
titleCase :: [Char] -> [Char]
|
||||
titleCase [] = []
|
||||
titleCase x = toUpper (head x) : map toLower (tail x)
|
||||
|
||||
-- List of valid characters consisting of latin alphabet and numbers
|
||||
validChars :: [Char]
|
||||
validChars = validLetters <> fmap intToDigit validNumbers
|
||||
|
||||
|
@ -17,10 +17,9 @@ import Web.Scotty
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Lazy as LT
|
||||
|
||||
|
||||
-- Routes related to secret sharing functionality
|
||||
routes :: PurrApp ()
|
||||
routes = do
|
||||
|
||||
get "/pw/:id" $ do
|
||||
reqId <- param "id"
|
||||
email <- liftIO adminEmail
|
||||
|
@ -20,58 +20,74 @@ import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as ET
|
||||
import qualified Data.Text.Lazy as LT
|
||||
|
||||
-- Look up a secret based on the "link" attribute
|
||||
findByLink :: String -> PurrAction (Maybe T.Text)
|
||||
findByLink link = do
|
||||
-- Get the encryption key from the filesystem as a ByteString
|
||||
key <- liftIO encKey
|
||||
-- Start up a connection to the SQLite database
|
||||
conn <- liftIO $ open dbPath
|
||||
res <- liftIO $ query conn "SELECT * from pws WHERE link = ?" (Only (last $ splitOn "/" link))
|
||||
-- Constant containing the results of a query looking for the "link" attribute
|
||||
res <- liftIO $
|
||||
query conn "SELECT * from pws WHERE link = ?"
|
||||
(Only (last $ splitOn "/" link))
|
||||
-- Close the SQLite database connection
|
||||
liftIO $ close conn
|
||||
-- Pass the encryption key and [SecretEntry] to be unencrypted
|
||||
readEncryptedSecret key res
|
||||
|
||||
readEncryptedSecret :: B.ByteString -> [SecretEntry] -> PurrAction (Maybe T.Text)
|
||||
readEncryptedSecret key secret = do
|
||||
let secNonce = nonce $ safeHead failedSecret secret
|
||||
readEncryptedSecret key [] = return Nothing
|
||||
readEncryptedSecret key (secret:_) = do
|
||||
-- Increment the number of views on the secret in the database by one
|
||||
liftIO $ incViews secret dbPath
|
||||
-- Delete the secret if it's expired
|
||||
delete <- liftIO $ deleteExpiredSecret secret dbPath
|
||||
case secret of
|
||||
[] -> return Nothing
|
||||
(x:_) -> if (delete)
|
||||
then return Nothing
|
||||
else return (ET.decodeLatin1 <$> (decryptSecret key secNonce $ decodeSecret x))
|
||||
where
|
||||
incViews :: [SecretEntry] -> String -> IO ()
|
||||
incViews [] _ = return ()
|
||||
incViews (secret : _) dbPath = do
|
||||
conn <- open dbPath
|
||||
execute conn
|
||||
"UPDATE pws SET views = views + 1 WHERE link = ?" (Only (link secret))
|
||||
close conn
|
||||
if (delete)
|
||||
-- Don't return the secret if it's expired
|
||||
then return Nothing
|
||||
-- Otherwise, try to decrypt and return it
|
||||
else return (ET.decodeLatin1 <$>
|
||||
(decryptSecret key (nonce secret) $ decodeSecret secret))
|
||||
where
|
||||
incViews :: SecretEntry -> String -> IO ()
|
||||
incViews secret dbPath = do
|
||||
conn <- open dbPath
|
||||
execute conn
|
||||
"UPDATE pws SET views = views + 1 WHERE link = ?" (Only (link secret))
|
||||
close conn
|
||||
|
||||
-- Returns True if deletion occurs, informing the calling function to not
|
||||
-- provide the successfully retrieved secret to the requestor.
|
||||
deleteExpiredSecret :: [SecretEntry] -> String -> IO Bool
|
||||
deleteExpiredSecret [] _ = return False
|
||||
deleteExpiredSecret (sec : _) dbPath = do
|
||||
deleteExpiredSecret :: SecretEntry -> String -> IO Bool
|
||||
deleteExpiredSecret sec dbPath = do
|
||||
-- Get the current Unix Epoch time in seconds
|
||||
time <- epochTime
|
||||
-- Compare the current time against the secret's initial insertion and lifetime
|
||||
if ((date sec) + ((life sec) * 86400) < time) || (views sec >= maxViews sec)
|
||||
-- Delete the secret if it's expired and return True for the caller
|
||||
then deleteSec sec dbPath
|
||||
-- Only return False if the secret is not expired
|
||||
else return False
|
||||
where
|
||||
deleteSec :: SecretEntry -> String -> IO Bool
|
||||
deleteSec sec dbPath = do
|
||||
conn <- open dbPath
|
||||
execute conn
|
||||
"DELETE FROM pws WHERE link = ?" (Only (link sec))
|
||||
close conn
|
||||
return True
|
||||
where
|
||||
deleteSec :: SecretEntry -> String -> IO Bool
|
||||
deleteSec sec dbPath = do
|
||||
conn <- open dbPath
|
||||
execute conn
|
||||
"DELETE FROM pws WHERE link = ?" (Only (link sec))
|
||||
close conn
|
||||
return True
|
||||
|
||||
insertNewSecret :: T.Text -> Integer -> T.Text -> Integer -> PurrAction ()
|
||||
insertNewSecret sec life link maxViews = do
|
||||
key <- liftIO encKey
|
||||
-- Create a new nonce to associate with the secret's encryption
|
||||
nonce <- liftIO Box.newNonce
|
||||
{- Encrypt the secret; this is a pure function because we seeded RNG at program
|
||||
initialization with "sodiumInit" -}
|
||||
let encSec = encryptSecret key sec nonce
|
||||
conn <- liftIO $ open dbPath
|
||||
-- Get the current time to timestamp the secret's initial insertion
|
||||
time <- liftIO epochTime
|
||||
-- Save the secret to the database
|
||||
liftIO $ execute conn
|
||||
"INSERT INTO pws (link, secret, nonce, date, life, views, maxViews) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
(SecretEntry link (encodeSecret encSec) (CL.encode nonce) time life 0 maxViews)
|
||||
@ -94,15 +110,12 @@ decryptSecret k n b = do
|
||||
case (CL.decode k) of
|
||||
(Just key) -> case (CL.decode n) of
|
||||
(Just nonce) -> Box.secretboxOpen key nonce b
|
||||
{- There's no sensible way to fail gracefully if our nonce or secret
|
||||
key can't be decoded. Throw an error so the instance admin can
|
||||
investigate -}
|
||||
Nothing -> error "Failed to decode nonce"
|
||||
Nothing -> error "Failed to decode secret key"
|
||||
|
||||
-- Helper function to provide the current Epoch Time in seconds
|
||||
epochTime :: IO Integer
|
||||
epochTime = fmap round getPOSIXTime
|
||||
|
||||
failedSecret :: SecretEntry
|
||||
failedSecret = SecretEntry "fail" "fail" (BSC8.pack "fail") 0 0 0 0
|
||||
|
||||
safeHead :: a -> [a] -> a
|
||||
safeHead x [] = x
|
||||
safeHead x l = head l
|
||||
|
18
src/Lib.hs
18
src/Lib.hs
@ -10,12 +10,22 @@ import GHC.Natural (popCountNatural)
|
||||
import Prelude hiding (id)
|
||||
import Web.Scotty
|
||||
|
||||
main :: IO ()
|
||||
-- Purr's entrypoint
|
||||
main :: IO ()
|
||||
main = do
|
||||
sodiumInit
|
||||
Configuration.main
|
||||
Configuration.keyFileInit
|
||||
-- Initialize the RNG used for sodium encryption (Saltine library)
|
||||
sodiumInit
|
||||
{- Initialize our dotenv configuration which reads from a .env configuration
|
||||
file unless the PURRNOFILE env var exists already. -}
|
||||
Configuration.main
|
||||
{- Initialize the encryption key file if it doesn't
|
||||
exist yet or use the existing key -}
|
||||
Configuration.keyFileInit
|
||||
{- Initialize our database by ensuring the SQLite file exists
|
||||
and has tables setup as the application expects -}
|
||||
DB.main
|
||||
{- Get the configured port to run on and start the Scotty webserver app
|
||||
defined in HTTP.app -}
|
||||
appPortStr <- Configuration.appPort
|
||||
let appPort = read appPortStr :: Int
|
||||
scotty appPort HTTP.app
|
||||
|
Loading…
x
Reference in New Issue
Block a user