Generate and keep permanent salt for generating hashids for URIs

This commit is contained in:
fr33domlover 2019-02-08 21:54:22 +00:00
parent 9536d870e5
commit c2bf470fb6
10 changed files with 190 additions and 40 deletions

View file

@ -123,3 +123,6 @@ max-accounts: 3
# Signing key file for signing object capabilities sent to remote users # Signing key file for signing object capabilities sent to remote users
capability-signing-key: config/capability_signing_key capability-signing-key: config/capability_signing_key
# Salt file for encoding and decoding hashids
hashids-salt-file: config/hashids_salt

39
src/Data/Int/Local.hs Normal file
View file

@ -0,0 +1,39 @@
{- This file is part of Vervis.
-
- Written in 2019 by fr33domlover <fr33domlover@riseup.net>.
-
- Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
module Data.Int.Local
( toInts
, fromInts
)
where
import Prelude
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..), (<|))
modbase :: Int64
modbase = 2 ^ 29
toInts :: Int64 -> NonEmpty Int
toInts n =
let (d, m) = n `divMod` modbase
m' = fromIntegral m
in if d == 0
then m' :| []
else m' <| toInts d
fromInts :: NonEmpty Int -> Int64
fromInts = foldr (\ i n -> fromIntegral i + modbase * n) 0

46
src/Data/KeyFile.hs Normal file
View file

@ -0,0 +1,46 @@
{- This file is part of Vervis.
-
- Written in 2019 by fr33domlover <fr33domlover@riseup.net>.
-
- Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
module Data.KeyFile
( KeyFile (..)
, loadKeyFile
)
where
import Prelude
import Data.ByteString (ByteString)
import System.Directory (doesFileExist)
import qualified Data.ByteString as B (readFile, writeFile)
class KeyFile a where
generateKey :: IO a
parseKey :: ByteString -> IO a
renderKey :: a -> ByteString
loadKeyFile :: KeyFile a => Bool -> FilePath -> IO a
loadKeyFile setup path = do
e <- doesFileExist path
if e
then if setup
then fail $ "loadKeyFile: Initial setup but file already exists: " ++ path
else parseKey =<< B.readFile path
else if setup
then do
k <- generateKey
B.writeFile path $ renderKey k
return k
else fail $ "loadKeyFile: File not found: " ++ path

View file

@ -40,7 +40,7 @@ import System.Directory (doesFileExist)
import qualified Data.ByteString as B (writeFile, readFile) import qualified Data.ByteString as B (writeFile, readFile)
import Vervis.KeyFile import Data.KeyFile
-- | Ed25519 signing key, we generate it on the server and use for signing. We -- | Ed25519 signing key, we generate it on the server and use for signing. We
-- also make its public key available to whoever wishes to verify our -- also make its public key available to whoever wishes to verify our

View file

@ -59,8 +59,11 @@ import Control.Concurrent.Local (forkCheck)
import Database.Persist.Schema.PostgreSQL (schemaBackend) import Database.Persist.Schema.PostgreSQL (schemaBackend)
import Data.KeyFile (loadKeyFile)
import Web.Hashids.Local
import Vervis.ActorKey (generateActorKey, actorKeyRotator) import Vervis.ActorKey (generateActorKey, actorKeyRotator)
import Vervis.KeyFile (isInitialSetup, loadKeyFile) import Vervis.KeyFile (isInitialSetup)
-- Import all relevant handler modules here. -- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file! -- Don't forget to add new modules to your cabal file!
@ -125,7 +128,12 @@ makeFoundation appSettings = do
-- logging function. To get out of this loop, we initially create a -- logging function. To get out of this loop, we initially create a
-- temporary foundation without a real connection pool, get a log function -- temporary foundation without a real connection pool, get a log function
-- from there, and then create the real foundation. -- from there, and then create the real foundation.
let mkFoundation appConnPool appCapSignKey = App {..} let mkFoundation
appConnPool
appCapSignKey
appHashidEncode
appHashidDecode =
App {..}
-- The App {..} syntax is an example of record wild cards. For more -- The App {..} syntax is an example of record wild cards. For more
-- information, see: -- information, see:
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
@ -133,6 +141,8 @@ makeFoundation appSettings = do
mkFoundation mkFoundation
(error "connPool forced in tempFoundation") (error "connPool forced in tempFoundation")
(error "capSignKey forced in tempFoundation") (error "capSignKey forced in tempFoundation")
(error "hashidEncode forced in tempFoundation")
(error "hashidDecode forced in tempFoundation")
logFunc = loggingFunction tempFoundation logFunc = loggingFunction tempFoundation
-- Create the database connection pool -- Create the database connection pool
@ -142,6 +152,10 @@ makeFoundation appSettings = do
setup <- isInitialSetup pool schemaBackend setup <- isInitialSetup pool schemaBackend
capSignKey <- loadKeyFile setup $ appCapabilitySigningKeyFile appSettings capSignKey <- loadKeyFile setup $ appCapabilitySigningKeyFile appSettings
hashidsSalt <- loadKeyFile setup $ appHashidsSaltFile appSettings
let hashidsCtx = hashidsContext hashidsSalt
hashidEncode = decodeUtf8 . encodeInt64 hashidsCtx
hashidDecode = decodeInt64 hashidsCtx . encodeUtf8
-- Perform database migration using our application's logging settings. -- Perform database migration using our application's logging settings.
--runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc --runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc
@ -156,7 +170,7 @@ makeFoundation appSettings = do
Right (_from, _to) -> $logInfo "DB migration success" Right (_from, _to) -> $logInfo "DB migration success"
-- Return the foundation -- Return the foundation
return $ mkFoundation pool capSignKey return $ mkFoundation pool capSignKey hashidEncode hashidDecode
-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and -- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and
-- applying some additional middlewares. -- applying some additional middlewares.

View file

@ -76,15 +76,17 @@ import Vervis.Widget (breadcrumbsW, revisionW)
-- starts running, such as database connections. Every handler will have -- starts running, such as database connections. Every handler will have
-- access to the data present here. -- access to the data present here.
data App = App data App = App
{ appSettings :: AppSettings { appSettings :: AppSettings
, appStatic :: Static -- ^ Settings for static file serving. , appStatic :: Static -- ^ Settings for static file serving.
, appConnPool :: ConnectionPool -- ^ Database connection pool. , appConnPool :: ConnectionPool -- ^ Database connection pool.
, appHttpManager :: Manager , appHttpManager :: Manager
, appLogger :: Logger , appLogger :: Logger
, appMailQueue :: Maybe (Chan (MailRecipe App)) , appMailQueue :: Maybe (Chan (MailRecipe App))
, appSvgFont :: PreparedFont Double , appSvgFont :: PreparedFont Double
, appActorKeys :: TVar (ActorKey, ActorKey, Bool) , appActorKeys :: TVar (ActorKey, ActorKey, Bool)
, appCapSignKey :: ActorKey , appCapSignKey :: ActorKey
, appHashidEncode :: Int64 -> Text
, appHashidDecode :: Text -> Maybe Int64
, appActivities :: TVar (Vector (UTCTime, Either String (ByteString, BL.ByteString))) , appActivities :: TVar (Vector (UTCTime, Either String (ByteString, BL.ByteString)))
} }

View file

@ -33,50 +33,26 @@
-- * Have a reliable way to determine whether we're in the initial setup -- * Have a reliable way to determine whether we're in the initial setup
-- step, and make sure it's not easy to accidentally break this detection -- step, and make sure it's not easy to accidentally break this detection
-- --
-- This module implements such a mechanism for Vervis. It's really simple: -- This module, along with "Data.KeyFile", implements such a mechanism for
-- Vervis. It's really simple:
-- --
-- * If there are no tables in the DB, it's the initial setup phase -- * If there are no tables in the DB, it's the initial setup phase
-- * If initial setup, require that key file doesn't exist, and generate one -- * If initial setup, require that key file doesn't exist, and generate one
-- * If not initial setup, require that key file exists -- * If not initial setup, require that key file exists
module Vervis.KeyFile module Vervis.KeyFile
( KeyFile (..) ( isInitialSetup
, isInitialSetup
, loadKeyFile
) )
where where
import Prelude import Prelude
import Control.Monad.Trans.Reader (runReaderT) import Control.Monad.Trans.Reader (runReaderT)
import Data.ByteString (ByteString)
import Database.Persist.Schema (SchemaBackend, hasEntities) import Database.Persist.Schema (SchemaBackend, hasEntities)
import Database.Persist.Schema.SQL () import Database.Persist.Schema.SQL ()
import Database.Persist.Sql (SqlBackend, ConnectionPool, runSqlPool) import Database.Persist.Sql (SqlBackend, ConnectionPool, runSqlPool)
import System.Directory (doesFileExist)
import qualified Data.ByteString as B (readFile, writeFile)
class KeyFile a where
generateKey :: IO a
parseKey :: ByteString -> IO a
renderKey :: a -> ByteString
-- | Check whether we're in the initial setup step, in which we create keys. -- | Check whether we're in the initial setup step, in which we create keys.
-- Otherwise, we'll only use existing keys loaded from files. -- Otherwise, we'll only use existing keys loaded from files.
isInitialSetup :: ConnectionPool -> SchemaBackend SqlBackend -> IO Bool isInitialSetup :: ConnectionPool -> SchemaBackend SqlBackend -> IO Bool
isInitialSetup pool sb = isInitialSetup pool sb =
flip runSqlPool pool . flip runReaderT sb $ not <$> hasEntities flip runSqlPool pool . flip runReaderT sb $ not <$> hasEntities
loadKeyFile :: KeyFile a => Bool -> FilePath -> IO a
loadKeyFile setup path = do
e <- doesFileExist path
if e
then if setup
then fail $ "loadKeyFile: Initial setup but file already exists: " ++ path
else parseKey =<< B.readFile path
else if setup
then do
k <- generateKey
B.writeFile path $ renderKey k
return k
else fail $ "loadKeyFile: File not found: " ++ path

View file

@ -105,6 +105,8 @@ data AppSettings = AppSettings
-- Signing key file for signing object capabilities sent to remote users -- Signing key file for signing object capabilities sent to remote users
, appCapabilitySigningKeyFile :: FilePath , appCapabilitySigningKeyFile :: FilePath
-- Salt for encoding and decoding hashids
, appHashidsSaltFile :: FilePath
} }
instance FromJSON AppSettings where instance FromJSON AppSettings where
@ -145,6 +147,7 @@ instance FromJSON AppSettings where
appMail <- o .:? "mail" appMail <- o .:? "mail"
appCapabilitySigningKeyFile <- o .: "capability-signing-key" appCapabilitySigningKeyFile <- o .: "capability-signing-key"
appHashidsSaltFile <- o .: "hashids-salt-file"
return AppSettings {..} return AppSettings {..}

61
src/Web/Hashids/Local.hs Normal file
View file

@ -0,0 +1,61 @@
{- This file is part of Vervis.
-
- Written in 2019 by fr33domlover <fr33domlover@riseup.net>.
-
- Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
module Web.Hashids.Local
( hashidsContext
, encodeInt64
, decodeInt64
)
where
import Prelude
import Control.Monad (replicateM)
import Data.ByteString (ByteString)
import Data.Int (Int64)
import Data.List.NonEmpty (nonEmpty)
import System.Random (randomIO)
import Web.Hashids
import qualified Data.ByteString as B (pack, length)
import qualified Data.List.NonEmpty as NE (toList)
import Data.Int.Local
import Data.KeyFile
saltLength :: Int
saltLength = 32
newtype HashidsSalt = HashidsSalt ByteString
instance KeyFile HashidsSalt where
generateKey = HashidsSalt <$> generateRandomBytes saltLength
where
generateRandomBytes :: Int -> IO ByteString
generateRandomBytes n = B.pack <$> replicateM n randomIO
parseKey b =
if B.length b == saltLength
then return $ HashidsSalt b
else fail "parseKey HashidsSalt: Invalid length"
renderKey (HashidsSalt b) = b
hashidsContext :: HashidsSalt -> HashidsContext
hashidsContext = flip hashidsMinimum 5 . renderKey
encodeInt64 :: HashidsContext -> Int64 -> ByteString
encodeInt64 c = encodeList c . NE.toList . toInts
decodeInt64 :: HashidsContext -> ByteString -> Maybe Int64
decodeInt64 c = fmap fromInts . nonEmpty . decode c

View file

@ -60,6 +60,8 @@ library
Data.Graph.Inductive.Query.TransRed Data.Graph.Inductive.Query.TransRed
Data.HashMap.Lazy.Local Data.HashMap.Lazy.Local
Data.Hourglass.Local Data.Hourglass.Local
Data.Int.Local
Data.KeyFile
Data.List.Local Data.List.Local
Data.Maybe.Local Data.Maybe.Local
Data.Paginate.Local Data.Paginate.Local
@ -84,6 +86,7 @@ library
Text.FilePath.Local Text.FilePath.Local
Text.Jasmine.Local Text.Jasmine.Local
Web.ActivityPub Web.ActivityPub
Web.Hashids.Local
Web.PathPieces.Local Web.PathPieces.Local
Yesod.Auth.Unverified Yesod.Auth.Unverified
Yesod.Auth.Unverified.Creds Yesod.Auth.Unverified.Creds
@ -256,6 +259,7 @@ library
, filepath , filepath
, formatting , formatting
, hashable , hashable
, hashids
-- for source file highlighting -- for source file highlighting
, highlighter2 , highlighter2
, http-client-signature , http-client-signature
@ -295,6 +299,8 @@ library
, persistent-postgresql , persistent-postgresql
, persistent-template , persistent-template
, process , process
-- for generating hashids salt
, random
-- for Database.Persist.Local -- for Database.Persist.Local
, resourcet , resourcet
, safe , safe