hellnet-0.1: Hellnet/Storage.hs
--------------------------------------------------------------------------------
-- This file is part of Hellnet
--
-- Hellnet is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- Hellnet is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with Hellnet. If not, see <http://www.gnu.org/licenses/>.
--------------------------------------------------------------------------------
{-# LANGUAGE FlexibleInstances #-}
module Hellnet.Storage (
Hellnet.Storage.generateKeyPair
, getChunk
, getDirectory
, getDirectory'
, getFile
, getFile'
, getKeyAliases
, getMeta
, getMetaNames
, getPrivateKey
, hashToPath
, insertChunk
, insertData
, insertFileContents
, insertFileContentsLazy
, isStored
, purgeChunk
, regenMeta
, resolveKeyName
, storeFile
, storeFile'
, storeKeyAliases
, storeMeta
, storePrivateKey
, toFullPath
) where
import Codec.Crypto.RSA as RSA
import Codec.Utils
import Control.Monad
import Data.Digest.SHA512 as SHA512
import Data.Foldable hiding (concat)
import Data.List
import Data.Maybe
import Hellnet
import Hellnet.Crypto
import Hellnet.Meta as Meta
import Hellnet.URI
import Hellnet.Utils
import qualified Data.ByteString as BS
import qualified Data.ByteString.UTF8 as BU
import qualified Data.ByteString.Lazy.UTF8 as BUL
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Map as Map
import System.Directory
import System.FilePath
import System.IO
import System.IO.Error
import System.Posix.Files
import System.Random
import Text.JSON.JPath
import Text.HJson as JSON
hashAndAppend :: Maybe Key -> Chunk -> Chunk -> IO Chunk
hashAndAppend encKey a b
| BSL.null b = return a
| otherwise = do
bHash <- insertChunk encKey b
return (BSL.append a $ BSL.pack bHash)
insertFileContentsLazy :: Maybe Key -> BSL.ByteString -> IO Hash
insertFileContentsLazy encKey bs = do
let chunks = splitBslFor chunkSize bs
chunkHashes <- mapM (insertChunk encKey) chunks
let fileLink = splitFor hashesPerChunk chunkHashes
let fileLinkChunks = Prelude.map (BSL.pack . flatten) fileLink
where flatten a = Prelude.foldl1 (++) a
fileLinkHead <- foldrM (hashAndAppend encKey) BSL.empty (fileLinkChunks)
fileLinkHash <- insertChunk encKey fileLinkHead
return fileLinkHash
insertFileContents :: Maybe Key -> BS.ByteString -> IO Hash
insertFileContents k b = insertFileContentsLazy k (BSL.pack $ BS.unpack b)
insertChunk :: Maybe Key -> Chunk -> IO Hash
insertChunk encKey ch = do
let chunk = maybe (ch) ((flip encryptSym) ch) encKey
let chunkDigestRaw = Hellnet.Crypto.hash chunk
let chunkDigest = hashToHex chunkDigestRaw
let fullPath = joinPath ["store", (Prelude.take 2 chunkDigest), (Prelude.drop 2 chunkDigest)]
storeFile fullPath (chunk)
return chunkDigestRaw
getChunk :: Maybe Key -> Hash -> IO (Maybe Chunk)
getChunk key hsh = do
let chunkKey = hashToHex hsh
fil <- getFile' ["store", (Prelude.take 2 chunkKey), (Prelude.drop 2 chunkKey)]
return $ maybe Nothing (\conts -> Just (maybe (conts) (\k -> decryptSym k conts) key)) fil
toFullPath :: FilePath -> IO FilePath
toFullPath fpath = do
dirEnv <- safeGetEnv "HELLNET_HOME"
dir <- maybe (getAppUserDataDirectory "hellnet") (return) dirEnv
return (joinPath [dir,fpath])
storeFile :: FilePath -> BSL.ByteString -> IO ()
storeFile fpath dat = do
fullPath <- toFullPath fpath
createDirectoryIfMissing True (dropFileName fullPath)
(tmpf, tmph) <- openTempFile "/tmp" "helltemp"
BSL.hPut tmph dat
hFlush tmph
hClose tmph
let copyMove = do
copyFile tmpf fullPath
removeFile tmpf
catch (renameFile tmpf fullPath) (\e -> copyMove)
storeFile' :: [String] -> BSL.ByteString -> IO ()
storeFile' fs = storeFile (joinPath fs)
getFile :: FilePath -> IO (Maybe BSL.ByteString)
getFile fpath = do
fullPath <- toFullPath fpath
catch (do
conts <- BSL.readFile fullPath
return (Just conts)) (const (return Nothing))
getFile' :: [String] -> IO (Maybe BSL.ByteString)
getFile' fs = getFile (joinPath fs)
purgeChunk :: Hash -> IO ()
purgeChunk hsh = do
let hexHsh = hashToHex hsh
fpath <- toFullPath (joinPath ["store", (Prelude.take 2 hexHsh), (Prelude.drop 2 hexHsh)])
removeFile fpath
hashToPath :: Hash -> IO FilePath
hashToPath hsh = let hexHsh = hashToHex hsh in toFullPath (joinPath ["store", (Prelude.take 2 hexHsh), (Prelude.drop 2 hexHsh)])
isStored :: Hash -> IO Bool
isStored hsh = do
fp <- hashToPath hsh
return =<< (doesFileExist fp)
getMeta :: KeyID -> String -> IO (Maybe Meta)
getMeta keyId mName = do
mFile <- getFile' ["meta", hashToHex keyId, mName]
return $ maybe (Nothing) (Meta.fromByteString) mFile
getMetaNames :: KeyID -> IO [String]
getMetaNames keyid = do
let path = ["meta", hashToHex keyid]
res <- getDirectory' path
return $ fromMaybe [] res
modifyMeta :: KeyID -- ^ public key ID
-> String -- ^ Meta name
-> String -- ^ Meta JPath
-> (Json -> Json) -- ^ JSON modifier function
-> Bool -- Whether meta was successfully modified
modifyMeta = undefined
storeMeta :: Meta -> IO ()
storeMeta m = storeFile' ["meta", hashToHex (keyID m), metaName m] $ Meta.toByteString m
getDirectory :: FilePath -> IO (Maybe [FilePath])
getDirectory fpath = do
fullPath <- toFullPath fpath
exists <- doesDirectoryExist fullPath
if exists then do
conts <- getDirectoryContents fullPath
return $ Just $ conts \\ [".", ".."]
else
return Nothing
getDirectory' :: [FilePath] -> IO (Maybe [FilePath])
getDirectory' = getDirectory . joinPath
insertData :: Maybe Key -> BSL.ByteString -> IO HellnetURI
insertData encKey dat = if BSL.null $ BSL.drop (256 * 1024) dat then do
hsh <- insertChunk encKey dat
return $ ChunkURI hsh encKey Nothing
else do
hsh <- insertFileContentsLazy encKey dat
return $ FileURI hsh encKey Nothing
storePrivateKey :: KeyID -> PrivateKey -> IO ()
storePrivateKey kid pKey = do
let bs = BUL.fromString $ JSON.toString $ toJson pKey
storeFile' ["privatekeys", hashToHex kid] bs
fPath <- toFullPath $ joinPath ["privatekeys", hashToHex kid]
setFileMode fPath $ unionFileModes ownerWriteMode ownerReadMode
generateKeyPair :: IO KeyID
generateKeyPair = do
g <- newStdGen
let (pub, priv, _) = RSA.generateKeyPair g 1024
hsh <- insertChunk Nothing $ BUL.fromString $ JSON.toString $ toJson pub
storePrivateKey hsh priv
return hsh
getPrivateKey :: KeyID -> IO (Maybe PrivateKey)
getPrivateKey kid = do
fil <- getFile' ["privatekeys", hashToHex kid]
return $ case fil of
Nothing -> Nothing
Just k -> either (const Nothing) (fromJson) $ JSON.fromString $ BUL.toString k
regenMeta :: Meta -> IO (Maybe Meta)
regenMeta meta = do
pKeyM <- getPrivateKey (keyID meta)
case pKeyM of
Nothing -> return Nothing
Just pKey -> do
updatedV <- getUnixTime
let sigV = BUL.fromString "FOOBAR"
let messageV = BUL.fromString $ toString $ toJson $ meta {timestamp = updatedV}
let sigV = signAsym pKey messageV
return $ Just $ meta {timestamp = updatedV, message = Just messageV, signature = Just sigV}
instance Jsonable (Map.Map String KeyID) where
toJson m = JObject $ Map.map (stringifyValue) m where
stringifyValue v = JString $ hashToHex v
fromJson (JObject m) = Just $ Map.map (hexToHash . unStringifyValue) m where
unStringifyValue (JString v) = v
unStringifyValue _ = ""
fromJson _ = Nothing
getKeyAliases :: IO (Map.Map String KeyID)
getKeyAliases = do
aliasesFileM <- getFile "keyaliases"
return $ case aliasesFileM of
Nothing -> Map.empty
Just aliasesFile -> case JSON.fromString $ BUL.toString aliasesFile of
Left _ -> Map.empty
Right j -> fromMaybe (Map.empty) $ fromJson j
storeKeyAliases :: Map.Map String KeyID -> IO ()
storeKeyAliases m = storeFile "keyaliases" $ BUL.fromString $ JSON.toString $ toJson m
resolveKeyName :: String -> IO KeyID
resolveKeyName name = do
aliases <- getKeyAliases
return $ fromMaybe (hexToHash name) $ Map.lookup name aliases