packages feed

sd-jwt-0.1.0.0: src/SDJWT/Internal/Serialization.hs

{-# LANGUAGE OverloadedStrings #-}
-- | Serialization and deserialization of SD-JWT structures.
--
-- This module provides functions to serialize and deserialize SD-JWTs
-- to/from the tilde-separated format specified in RFC 9901.
module SDJWT.Internal.Serialization
  ( serializeSDJWT
  , deserializeSDJWT
  , serializePresentation
  , deserializePresentation
  , parseTildeSeparated
  ) where

import SDJWT.Internal.Types (SDJWT(..), SDJWTPresentation(..), SDJWTError(..), EncodedDisclosure(..))
import Data.Maybe (fromMaybe)
import qualified Data.Text as T

-- | Serialize SD-JWT to tilde-separated format.
--
-- Format: @<Issuer-signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Disclosure N>~@
--
-- The last tilde is always present, even if there are no disclosures.
serializeSDJWT :: SDJWT -> T.Text
serializeSDJWT (SDJWT jwt sdDisclosures) =
  let
    disclosureParts = map unEncodedDisclosure sdDisclosures
    allParts = jwt : disclosureParts ++ [""]
  in
    T.intercalate "~" allParts

-- | Deserialize SD-JWT from tilde-separated format.
--
-- Parses a tilde-separated string into an 'SDJWT' structure.
-- Returns an error if the format is invalid or if a Key Binding JWT
-- is present (use 'deserializePresentation' for SD-JWT+KB).
deserializeSDJWT :: T.Text -> Either SDJWTError SDJWT
deserializeSDJWT input =
  case parseTildeSeparated input of
    Left err -> Left err
    Right (jwt, sdDisclosures, Nothing) ->
      -- Verify last part is empty (SD-JWT format)
      Right $ SDJWT jwt sdDisclosures
    Right (_, _, Just _) ->
      Left $ SerializationError "SD-JWT should not have Key Binding JWT (use SD-JWT+KB format)"

-- | Serialize SD-JWT presentation.
--
-- Format: @<Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~[<KB-JWT>]@
--
-- If a Key Binding JWT is present, it is included as the last component.
-- Otherwise, the last component is empty (just a trailing tilde).
serializePresentation :: SDJWTPresentation -> T.Text
serializePresentation (SDJWTPresentation jwt sdDisclosures mbKbJwt) =
  let
    disclosureParts = map unEncodedDisclosure sdDisclosures
    kbPart = fromMaybe "" mbKbJwt
    allParts = jwt : disclosureParts ++ [kbPart]
  in
    T.intercalate "~" allParts

-- | Deserialize SD-JWT presentation.
--
-- Parses a tilde-separated string into an 'SDJWTPresentation' structure.
-- This handles both SD-JWT (without KB-JWT) and SD-JWT+KB (with KB-JWT) formats.
deserializePresentation :: T.Text -> Either SDJWTError SDJWTPresentation
deserializePresentation input =
  case parseTildeSeparated input of
    Left err -> Left err
    Right (jwt, sdDisclosures, mbKbJwt) ->
      Right $ SDJWTPresentation jwt sdDisclosures mbKbJwt

-- | Parse tilde-separated format.
--
-- Low-level function that parses the tilde-separated format and returns
-- the components: (JWT, [Disclosures], Maybe KB-JWT).
--
-- The last component is 'Nothing' for SD-JWT format (empty string after
-- last tilde) or 'Just' KB-JWT for SD-JWT+KB format.
parseTildeSeparated :: T.Text -> Either SDJWTError (T.Text, [EncodedDisclosure], Maybe T.Text)
parseTildeSeparated input =
  let
    parts = T.splitOn "~" input
  in
    case parts of
      [] -> Left $ SerializationError "Empty SD-JWT"
      [jwt] ->
        -- Just JWT, no disclosures or KB-JWT
        Right (jwt, [], Nothing)
      jwt : rest ->
        let
          -- Last part could be empty (SD-JWT) or KB-JWT (SD-JWT+KB)
          -- Note: rest is guaranteed to be non-empty since [jwt] case is handled above
          (disclosureParts, lastPart) = case reverse rest of
            [] -> error "parseTildeSeparated: impossible case - rest should be non-empty"
            lastItem : revDisclosures ->
              if T.null lastItem
                then (reverse revDisclosures, Nothing)
                else (reverse revDisclosures, Just lastItem)
          sdDisclosures = map EncodedDisclosure disclosureParts
        in
          Right (jwt, sdDisclosures, lastPart)