packages feed

paseto-0.1.0.0: src/Crypto/Paseto/Token/Parser.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}

-- | Parsers for PASETO tokens according to the
-- [message format](https://github.com/paseto-standard/paseto-spec/tree/af79f25908227555404e7462ccdd8ce106049469/docs#paseto-message-format)
-- defined in the specification.
--
-- Note that the parsers exported from this module /do not/ perform any kind
-- of token validation, cryptographic or otherwise. These parsers simply
-- ensure that the input /looks like/ a well-formed token.
module Crypto.Paseto.Token.Parser
  ( -- * Token parsers
    parseTokenV3Local
  , parseTokenV3Public
  , parseTokenV4Local
  , parseTokenV4Public
  , parseSomeToken

    -- ** Parsec parsers
  , pVersion
  , pVersionV3
  , pVersionV4
  , pPurpose
  , pPurposeLocal
  , pPurposePublic
  , pPayload
  , pFooter
  , pPayloadAndFooter
  , pTokenParts
  , pTokenV3Local
  , pTokenV3Public
  , pTokenV4Local
  , pTokenV4Public
  , pSomeToken
  ) where

import Control.Applicative ( some, (<|>) )
import Crypto.Paseto.Mode ( Purpose (..), Version (..) )
import Crypto.Paseto.Token
  ( Footer (..), Payload (..), SomeToken (..), Token (..) )
import qualified Data.ByteString.Base64.URL as B64URL
import qualified Data.ByteString.Char8 as B8
import Data.Char ( isAsciiLower, isAsciiUpper, isDigit )
import Data.Functor ( void, ($>) )
import Data.Maybe ( isJust )
import Data.Text ( Text )
import Prelude
import Text.Parsec
  ( ParseError
  , ParsecT
  , Stream
  , char
  , eof
  , optionMaybe
  , parse
  , satisfy
  , string
  , try
  , (<?>)
  )
import Text.Parsec.Text ( Parser )

-- | Parse a version 3 local PASETO token from human-readable text according
-- to the
-- [message format](https://github.com/paseto-standard/paseto-spec/tree/af79f25908227555404e7462ccdd8ce106049469/docs#paseto-message-format)
-- defined in the specification.
--
-- Note that this function does not perform any kind of token validation,
-- cryptographic or otherwise. It simply parses the token and ensures that it
-- is well-formed.
parseTokenV3Local :: Text -> Either ParseError (Token V3 Local)
parseTokenV3Local = parse pTokenV3Local ""

-- | Parse a version 3 public PASETO token from human-readable text according
-- to the
-- [message format](https://github.com/paseto-standard/paseto-spec/tree/af79f25908227555404e7462ccdd8ce106049469/docs#paseto-message-format)
-- defined in the specification.
--
-- Note that this function does not perform any kind of token validation,
-- cryptographic or otherwise. It simply parses the token and ensures that it
-- is well-formed.
parseTokenV3Public :: Text -> Either ParseError (Token V3 Public)
parseTokenV3Public = parse pTokenV3Public ""

-- | Parse a version 4 local PASETO token from human-readable text according
-- to the
-- [message format](https://github.com/paseto-standard/paseto-spec/tree/af79f25908227555404e7462ccdd8ce106049469/docs#paseto-message-format)
-- defined in the specification.
--
-- Note that this function does not perform any kind of token validation,
-- cryptographic or otherwise. It simply parses the token and ensures that it
-- is well-formed.
parseTokenV4Local :: Text -> Either ParseError (Token V4 Local)
parseTokenV4Local = parse pTokenV4Local ""

-- | Parse a version 4 public PASETO token from human-readable text according
-- to the
-- [message format](https://github.com/paseto-standard/paseto-spec/tree/af79f25908227555404e7462ccdd8ce106049469/docs#paseto-message-format)
-- defined in the specification.
--
-- Note that this function does not perform any kind of token validation,
-- cryptographic or otherwise. It simply parses the token and ensures that it
-- is well-formed.
parseTokenV4Public :: Text -> Either ParseError (Token V4 Public)
parseTokenV4Public = parse pTokenV4Public ""

-- | Parse some kind of PASETO token from human-readable text according to the
-- [message format](https://github.com/paseto-standard/paseto-spec/tree/af79f25908227555404e7462ccdd8ce106049469/docs#paseto-message-format)
-- defined in the specification.
--
-- Note that this function does not perform any kind of token validation,
-- cryptographic or otherwise. It simply parses the token and ensures that it
-- is well-formed.
parseSomeToken :: Text -> Either ParseError SomeToken
parseSomeToken = parse pSomeToken ""

------------------------------------------------------------------------------
-- Parsec parsers
------------------------------------------------------------------------------

-- | Parse a valid @base64url@ character.
base64urlChar :: Stream s m Char => ParsecT s u m Char
base64urlChar = satisfy (\c -> isAsciiUpper c || isAsciiLower c || isDigit c || c == '-' || c == '_') <?> "base64url character"

-- | Period (\".\") parser.
period :: Parser ()
period = void $ char '.'

-- | Parse a 'Version' from its string representation.
pVersion :: Parser Version
pVersion =
  try pVersionV3
    <|> pVersionV4
    <?> "version"

-- | Parse the 'Version' string @v3@.
pVersionV3 :: Parser Version
pVersionV3 = string "v3" $> V3

-- | Parse the 'Version' string @v4@.
pVersionV4 :: Parser Version
pVersionV4 = string "v4" $> V4

-- | Parse a 'Purpose' from its string representation.
pPurpose :: Parser Purpose
pPurpose =
  try pPurposeLocal
    <|> pPurposePublic
    <?> "purpose"

-- | Parse the 'Purpose' string @local@.
pPurposeLocal :: Parser Purpose
pPurposeLocal = string "local" $> Local

-- | Parse the 'Purpose' string @public@.
pPurposePublic :: Parser Purpose
pPurposePublic = string "public" $> Public

-- | Parse a 'Payload' from its string representation.
pPayload :: Parser Payload
pPayload = do
  payloadB64 <- B8.pack <$> some base64urlChar
  case B64URL.decodeUnpadded payloadB64 of
    Left err -> fail err
    Right x -> pure (Payload x)

-- | Parse a 'Footer' from its string representation.
pFooter :: Parser Footer
pFooter = do
  footerB64 <- B8.pack <$> some base64urlChar
  case B64URL.decodeUnpadded footerB64 of
    Left err -> fail err
    Right x -> pure (Footer x)

-- | Parse a 'Payload' along with an optional 'Footer'.
pPayloadAndFooter :: Parser (Payload, Maybe Footer)
pPayloadAndFooter = do
  payload <- pPayload <?> "payload"
  hasFooter <- isJust <$> optionMaybe period
  case hasFooter of
    False -> do
      eof
      pure (payload, Nothing)
    True -> do
      footer <- pFooter <?> "footer"
      eof
      pure (payload, Just footer)

-- | Parse the parts of a PASETO token: version, purpose, payload, and an
-- optional footer.
pTokenParts
  :: Parser Version
  -- ^ Parser for the 'Version' part of a token.
  -> Parser Purpose
  -- ^ Parser for the 'Purpose' part of a token.
  -> Parser (Version, Purpose, Payload, Maybe Footer)
pTokenParts pV pP = do
  version <- pV
  period
  purpose <- pP
  period
  (payload, mbFooter) <- pPayloadAndFooter
  pure (version, purpose, payload, mbFooter)

-- | Parse a version 3 local PASETO token from its string representation.
--
-- Accepted token format:
--
-- * Without the optional footer: @v3.local.${payload}@
--
-- * With the optional footer: @v3.local.${payload}.${footer}@
--
-- Both the @payload@ and optional @footer@ are @base64url@-encoded values
-- (unpadded).
pTokenV3Local :: Parser (Token V3 Local)
pTokenV3Local = do
  (_, _, payload, mbFooter) <- pTokenParts pVersionV3 pPurposeLocal
  pure (TokenV3Local payload mbFooter)

-- | Parse a version 3 public PASETO token from its string representation.
--
-- Accepted token format:
--
-- * Without the optional footer: @v3.public.${payload}@
--
-- * With the optional footer: @v3.public.${payload}.${footer}@
--
-- Both the @payload@ and optional @footer@ are @base64url@-encoded values
-- (unpadded).
pTokenV3Public :: Parser (Token V3 Public)
pTokenV3Public = do
  (_, _, payload, mbFooter) <- pTokenParts pVersionV3 pPurposePublic
  pure (TokenV3Public payload mbFooter)

-- | Parse a version 4 local PASETO token from its string representation.
--
-- Accepted token format:
--
-- * Without the optional footer: @v4.local.${payload}@
--
-- * With the optional footer: @v4.local.${payload}.${footer}@
--
-- Both the @payload@ and optional @footer@ are @base64url@-encoded values
-- (unpadded).
pTokenV4Local :: Parser (Token V4 Local)
pTokenV4Local = do
  (_, _, payload, mbFooter) <- pTokenParts pVersionV4 pPurposeLocal
  pure (TokenV4Local payload mbFooter)

-- | Parse a version 4 public PASETO token from its string representation.
--
-- Accepted token format:
--
-- * Without the optional footer: @v4.public.${payload}@
--
-- * With the optional footer: @v4.public.${payload}.${footer}@
--
-- Both the @payload@ and optional @footer@ are @base64url@-encoded values
-- (unpadded).
pTokenV4Public :: Parser (Token V4 Public)
pTokenV4Public = do
  (_, _, payload, mbFooter) <- pTokenParts pVersionV4 pPurposePublic
  pure (TokenV4Public payload mbFooter)

-- | Parse some kind of PASETO token from its string representation.
--
-- PASETO token format:
--
-- * Without the optional footer: @version.purpose.payload@
--
-- * With the optional footer: @version.purpose.payload.footer@
--
-- Acceptable values for @version@ are @v3@ and @v4@. @v1@ and @v2@ are
-- deprecated, so they're not supported.
--
-- Acceptable values for @purpose@ are @local@ and @public@.
--
-- Both the @payload@ and optional @footer@ are @base64url@-encoded values
-- (unpadded).
pSomeToken :: Parser SomeToken
pSomeToken = do
  (version, purpose, payload, mbFooter) <- pTokenParts pVersion pPurpose
  pure (mkToken version purpose payload mbFooter)
  where
    mkToken :: Version -> Purpose -> Payload -> Maybe Footer -> SomeToken
    mkToken version purpose payload mbFooter =
      case (version, purpose) of
        (V3, Local) -> SomeTokenV3Local (TokenV3Local payload mbFooter)
        (V3, Public) -> SomeTokenV3Public (TokenV3Public payload mbFooter)
        (V4, Local) -> SomeTokenV4Local (TokenV4Local payload mbFooter)
        (V4, Public) -> SomeTokenV4Public (TokenV4Public payload mbFooter)