aviary-ps/src/Main.purs

153 lines
5.9 KiB
Plaintext

module Main where
import Prelude
import AviaryFormat.Format (Format, Image, Index, parseIndex) as Format
import Control.Monad.Error.Class (try)
import Effect (Effect)
import Effect.Aff (Aff, launchAff)
import Effect.Class (liftEffect)
import Effect.Console (log)
import Effect.Exception (message)
import Fetch (fetch)
import Data.Array (head)
import Data.Base64 (decodeBase64, fromString)
import Data.Either (Either(..), hush, note)
import Data.Maybe (Maybe(..))
import Data.Newtype (unwrap)
import Data.ArrayBuffer.ArrayBuffer (byteLength)
import Data.ArrayBuffer.Builder (execPut, putInt8)
import Data.ArrayBuffer.Builder.Internal (toView)
import Data.ArrayBuffer.DataView (buffer, whole)
import Data.ArrayBuffer.Types (ArrayBuffer)
import Data.Filterable (partitionMap)
import Data.Foldable (traverse_)
import Data.String (drop, indexOf', indexOf, splitAt)
import Data.String.Pattern (Pattern(..))
import Parsing (runParserT)
import Web.HTML (window) as HTML
import Web.HTML.Window (location)
import Web.HTML.Location (hash, origin, pathname) as Location
import Crypto.Subtle.Constants.AES (aesGCM, t128)
import Crypto.Subtle.Encrypt (aesGCM, decrypt) as Alg
import Crypto.Subtle.Key.Import (aes)
import Crypto.Subtle.Key.Import (importKey) as SC
import Crypto.Subtle.Key.Types (CryptoKey, decrypt, raw)
nonceE :: Effect ArrayBuffer
nonceE = execPut $ traverse_ putInt8 [0xd0, 0xc3, 0x75, 0x56, 0x58, 0xc1, 0x7e, 0x5f, 0xd6, 0xcc, 0xb6, 0x76]
data AviaryError
= MalformedKey
| MalformedUrl
| DecryptFailed
| StrangeIndex
| IndexMissingField String
type Image =
{ key :: Aff CryptoKey
, fullUrl :: String
, thumbUrl :: String
, blurhash :: String
, format :: Format.Format
}
type Index =
{ images :: Array Image
, title :: Maybe String
, desc :: Maybe String
}
instance showAviaryError :: Show AviaryError where
show MalformedKey = "Malformed key!!"
show MalformedUrl = "Malformed url!!"
show DecryptFailed = "Either the data provided by the server was bad, or valid but incorrect key. Expiration likely."
show StrangeIndex = "The gallery index matched with the provided key, but was in a strange format. This could indicate a severe version mismatch, or that the gallery was created by a malfunctioning client."
show (IndexMissingField f) = "The gallery index was missing the crucial field " <> f <> ". This is almost certainly the result of a version mismatch or that the creator's client was not fully up-to-spec."
type UrlInfo =
{ server :: String
, fileId :: String
, key :: String
}
parseUrl :: String -> Either AviaryError UrlInfo
parseUrl url = note MalformedUrl do
slashIndex <- indexOf' (Pattern "/") 8 url
let { after, before: server } = splitAt (1 + slashIndex) url
poundIndex <- indexOf (Pattern "#") after
let { after: key, before: fileId } = splitAt poundIndex after
Just { server, fileId, key: drop 1 key }
decodeKey :: String -> Either AviaryError ArrayBuffer
decodeKey key = note MalformedKey $ decodeBase64 <$> fromString key
importKey :: ArrayBuffer -> Aff CryptoKey
importKey key =
SC.importKey raw key (aes aesGCM) false [decrypt]
decryptBlob :: CryptoKey -> ArrayBuffer -> ArrayBuffer -> Aff ArrayBuffer
decryptBlob key cyphertext nonce =
-- nonce <- makeAff (\_ -> nonceE)
Alg.decrypt (Alg.aesGCM nonce Nothing (Just t128)) key cyphertext
convertImageFromProtobuf :: Format.Image -> Either AviaryError Image
convertImageFromProtobuf protoimage = let protoimage' = unwrap protoimage in do
rawKey <- note (IndexMissingField "images[].key") protoimage'.key
fullUrl <- note (IndexMissingField "images[].full_url") protoimage'.full_url
thumbUrl <- note (IndexMissingField "images[].thumb_url") protoimage'.thumb_url
blurhash <- note (IndexMissingField "images[].blurhash") protoimage'.blurhash
format <- note (IndexMissingField "images[].format") protoimage'.format
let key = SC.importKey raw (buffer $ toView $ unwrap rawKey) (aes aesGCM) false [decrypt]
pure { key, fullUrl, thumbUrl, blurhash, format }
convertIndexFromProtobuf :: Format.Index -> Either AviaryError Index
convertIndexFromProtobuf protoindex =
let protoindex' = unwrap protoindex in
let {right: images, left: imagesErrors} = partitionMap convertImageFromProtobuf protoindex'.images
in
case head imagesErrors of
Nothing ->
Right
{ images
, title: protoindex'.title
, desc: protoindex'.desc
}
Just err -> Left err
parseIndex :: ArrayBuffer -> Effect (Either AviaryError Index)
parseIndex rawData = do
parseResult <- runParserT (whole rawData) (Format.parseIndex (byteLength rawData))
pure $ convertIndexFromProtobuf =<< (note StrangeIndex $ hush parseResult)
main_aff :: Aff Unit
main_aff = do
urlInfo <- liftEffect do
window <- HTML.window
location <- location window
keyB64 <- drop 1 <$> Location.hash location
server <- Location.origin location
fileId <- drop 1 <$> Location.pathname location
nonce <- nonceE
pure {keyB64, server, fileId, nonce}
maybeIndex <- case decodeKey urlInfo.keyB64 of
Left err -> pure $ Left err
Right keyBuffer -> do
cryptoKey <- importKey keyBuffer
{ status, arrayBuffer } <- fetch (urlInfo.server <> "/" <> urlInfo.fileId <> ".bin") {}
encryptedIndex <- arrayBuffer
serializedIndex <- try $ decryptBlob cryptoKey encryptedIndex urlInfo.nonce
case serializedIndex of
Left err -> do
_ <- liftEffect $ log $ message err
pure $ Left $ DecryptFailed
Right serializedIndex' -> do
_ <- liftEffect $ log "BBBBBBBBB!"
maybeIndex <- liftEffect $ parseIndex serializedIndex'
pure ((\i -> i.title) <$> maybeIndex)
liftEffect $ log $ show maybeIndex
main :: Effect Unit
main = launchAff main_aff $> unit