Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package ghc-crypto-token for openSUSE:Factory checked in at 2026-11-06 19:48:32 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ghc-crypto-token (Old) and /work/SRC/openSUSE:Factory/.ghc-crypto-token.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ghc-crypto-token" Fri Nov 6 19:48:32 2026 rev:5 rq:1395519 version:0.2.1 Changes: -------- --- /work/SRC/openSUSE:Factory/ghc-crypto-token/ghc-crypto-token.changes 2026-06-22 17:45:00.678505533 +0200 +++ /work/SRC/openSUSE:Factory/.ghc-crypto-token.new.3759290/ghc-crypto-token.changes 2026-11-06 19:54:47.336173805 +0100 @@ -1,0 +2,14 @@ +Tue Oct 27 09:06:03 UTC 2026 - Peter Simons <[email protected]> + +- Update crypto-token to version 0.2.1. + Upstream added a new change log file in this release. With no + previous version to compare against, the automatic updater cannot + reliable determine the relevante entries for this release. + +------------------------------------------------------------------- +Sun Oct 11 04:48:36 UTC 2026 - Peter Simons <[email protected]> + +- Update crypto-token to version 0.2.0 revision 3. + Upstream has revised the Cabal build instructions on Hackage. + +------------------------------------------------------------------- Old: ---- crypto-token-0.2.0.tar.gz New: ---- crypto-token-0.2.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ghc-crypto-token.spec ++++++ --- /var/tmp/diff_new_pack.GR21g9/_old 2026-11-06 19:54:48.329215384 +0100 +++ /var/tmp/diff_new_pack.GR21g9/_new 2026-11-06 19:54:48.330215426 +0100 @@ -20,7 +20,7 @@ %global pkgver %{pkg_name}-%{version} %bcond_with tests Name: ghc-%{pkg_name} -Version: 0.2.0 +Version: 0.2.1 Release: 0 Summary: Crypto tokens License: BSD-3-Clause @@ -97,6 +97,7 @@ %license LICENSE %files devel -f %{name}-devel.files +%doc ChangeLog.md %files -n ghc-%{pkg_name}-doc -f ghc-%{pkg_name}-doc.files %license LICENSE ++++++ crypto-token-0.2.0.tar.gz -> crypto-token-0.2.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crypto-token-0.2.0/ChangeLog.md new/crypto-token-0.2.1/ChangeLog.md --- old/crypto-token-0.2.0/ChangeLog.md 1970-01-01 01:00:00.000000000 +0100 +++ new/crypto-token-0.2.1/ChangeLog.md 2001-09-09 03:46:40.000000000 +0200 @@ -0,0 +1,20 @@ +# ChangeLog for crypto-token + +## 0.2.1 + +* `tokenLifetime` is exact. A token carries its issue time and is refused + once older than `tokenLifetime`; before, it could expire up to one + `interval` early. +* A `tokenLifetime` shorter than `interval`, or spanning 65536 intervals or + more, no longer makes every token operation divide by zero. + `spawnTokenManager` throws an `IOError` for a non-positive `interval`. +* The AES-GCM nonce is 96 bits, the length GCM is designed for, rather + than 64. +* The token header is encrypted rather than masked, so tokens no longer + reveal how their counters relate. +* The rotation thread survives a failed rotation, and its wait no longer + overflows a 32-bit `Int`. +* Tokens are 14 bytes longer: an 8-byte issue time and a 16-byte header + instead of 10. Tokens never outlive the process that made them, so no + token from an earlier version is affected. +* The lower bound on base moves to 4.11, where `GHC.Clock` is. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crypto-token-0.2.0/Crypto/Token.hs new/crypto-token-0.2.1/Crypto/Token.hs --- old/crypto-token-0.2.0/Crypto/Token.hs 2001-09-09 03:46:40.000000000 +0200 +++ new/crypto-token-0.2.1/Crypto/Token.hs 2001-09-09 03:46:40.000000000 +0200 @@ -22,20 +22,19 @@ ) where import Control.Concurrent +import qualified Control.Exception as E +import Control.Monad (when) import Crypto.Cipher.AES (AES256) import Crypto.Cipher.Types (AEADMode (..), AuthTag (..)) import qualified Crypto.Cipher.Types as C import Crypto.Error (maybeCryptoError, throwCryptoError) import Crypto.Random (getRandomBytes) import Data.Array.IO -import Data.Bits (xor) import qualified Data.ByteArray as BA import qualified Data.ByteString as BS -import qualified Data.ByteString.Internal as BS import qualified Data.IORef as I import Data.Word -import Foreign.Ptr -import Foreign.Storable +import GHC.Clock (getMonotonicTimeNSec) import GHC.Conc.Sync (labelThread) import Network.ByteOrder @@ -49,7 +48,8 @@ { interval :: Int -- ^ The interval to generate a new secret and remove the oldest one in seconds. , tokenLifetime :: Int - -- ^ The token lifetime, that is, tokens can be decrypted in this period. + -- ^ The token lifetime in seconds, that is, tokens can be decrypted in this period. + -- It is cut short if it spans more than 65535 intervals. , threadName :: String } deriving (Eq, Show) @@ -57,7 +57,7 @@ -- | Default configuration to update secrets in 30 minutes (1,800 seconds) and token lifetime is 2 hours (7,200 seconds) -- -- >>> defaultConfig --- Config {interval = 1800, tokenLifetime = 7200} +-- Config {interval = 1800, tokenLifetime = 7200, threadName = "Crypto token manager"} defaultConfig :: Config defaultConfig = Config @@ -68,60 +68,81 @@ ---------------------------------------------------------------- --- fixme: mask - -- | The abstract data type for token manager. data TokenManager = TokenManager - { headerMask :: Header + { headerKey :: AES256 , getEncryptSecret :: IO (Secret, Index) , getDecryptSecret :: Index -> IO Secret , threadId :: ThreadId + , lifetimeNSec :: Word64 } -- | Spawning a token manager. +-- This throws an 'IOError' if 'interval' is not positive. spawnTokenManager :: Config -> IO TokenManager spawnTokenManager Config{..} = do + when (interval <= 0) $ + E.throwIO $ + userError "Crypto.Token: interval must be positive" + -- the secret a token was made with must outlive the token itself, + -- and an index travels as Word16, so there are at most 65536 slots + let lifetime = max 0 tokenLifetime + slots = min 65536 $ (lifetime + interval - 1) `div` interval + 1 emp <- emptySecret - let lim = fromIntegral (tokenLifetime `div` interval) - arr <- newArray (0, lim - 1) emp + arr <- newArray (0, slots - 1) emp ent <- generateSecret writeArray arr 0 ent ref <- I.newIORef 0 tid <- forkIO $ loop arr ref labelThread tid threadName - msk <- newHeaderMask - return $ TokenManager msk (readCurrentSecret arr ref) (readSecret arr) tid + hkey <- newHeaderKey + return $ + TokenManager hkey (readCurrentSecret arr ref) (readSecret arr) tid $ + fromIntegral lifetime * 1000000000 where loop arr ref = do - threadDelay (interval * 1000000) - update arr ref + delaySeconds interval + -- a failed rotation is retried at the next interval + update arr ref `E.catch` ignoreSync loop arr ref - update :: IOArray Index Secret -> I.IORef Index -> IO () + ignoreSync :: E.SomeException -> IO () + ignoreSync e = case E.fromException e of + Just (E.SomeAsyncException _) -> E.throwIO e + Nothing -> return () + update :: IOArray Int Secret -> I.IORef Int -> IO () update arr ref = do idx0 <- I.readIORef ref - (_, n) <- getBounds arr - let idx = (idx0 + 1) `mod` (n + 1) + n <- rangeSize <$> getBounds arr + let idx = (idx0 + 1) `mod` n sec <- generateSecret writeArray arr idx sec I.writeIORef ref idx +-- threadDelay's microseconds overflow a 32-bit Int after 35 minutes. +delaySeconds :: Int -> IO () +delaySeconds n + | n <= 0 = return () + | otherwise = do + let m = min n 1000 + threadDelay (m * 1000000) + delaySeconds (n - m) + -- | Killing a token manager. killTokenManager :: TokenManager -> IO () killTokenManager TokenManager{..} = killThread threadId ---------------------------------------------------------------- -readSecret :: IOArray Index Secret -> Index -> IO Secret +readSecret :: IOArray Int Secret -> Index -> IO Secret readSecret secrets idx0 = do - (_, n) <- getBounds secrets - let idx = idx0 `mod` (n + 1) - readArray secrets idx + n <- rangeSize <$> getBounds secrets + readArray secrets $ fromIntegral idx0 `mod` n -readCurrentSecret :: IOArray Index Secret -> I.IORef Index -> IO (Secret, Index) +readCurrentSecret :: IOArray Int Secret -> I.IORef Int -> IO (Secret, Index) readCurrentSecret arr ref = do idx <- I.readIORef ref - sec <- readSecret arr idx - return (sec, idx) + sec <- readArray arr idx + return (sec, fromIntegral idx) ---------------------------------------------------------------- @@ -149,8 +170,9 @@ ---------------------------------------------------------------- +-- 96 bits, the nonce length GCM is designed for ivLength :: Int -ivLength = 8 +ivLength = 12 keyLength :: Int keyLength = 32 @@ -164,6 +186,10 @@ tagLength :: Int tagLength = 16 +-- one AES block: index, counter and zero padding +headerLength :: Int +headerLength = 16 + ---------------------------------------------------------------- data Header = Header @@ -180,40 +206,34 @@ decodeHeader bs = withReadBuffer bs $ \rbuf -> Header <$> read16 rbuf <*> read64 rbuf -newHeaderMask :: IO Header -newHeaderMask = do - bin <- getRandomBytes (indexLength + counterLength) :: IO ByteString - decodeHeader bin +newHeaderKey :: IO AES256 +newHeaderKey = do + key <- getRandomBytes keyLength :: IO ByteString + return $ throwCryptoError $ C.cipherInit key ---------------------------------------------------------------- -xorHeader :: Header -> Header -> Header -xorHeader x y = - Header - { headerIndex = headerIndex x `xor` headerIndex y - , headerCounter = headerCounter x `xor` headerCounter y - } - +-- The header is encrypted so that tokens do not reveal how they relate. addHeader :: TokenManager -> Index -> Counter -> ByteString -> IO ByteString addHeader TokenManager{..} idx counter cipher = do - let hdr = Header idx counter - mskhdr = headerMask `xorHeader` hdr - hdrbin <- encodeHeader mskhdr - return (hdrbin `BS.append` cipher) + hdrbin <- encodeHeader $ Header idx counter + let padding = BS.replicate (headerLength - BS.length hdrbin) 0 + blk = C.ecbEncrypt headerKey (hdrbin `BS.append` padding) + return (blk `BS.append` cipher) delHeader :: TokenManager -> ByteString -> IO (Maybe (Index, Counter, ByteString)) delHeader TokenManager{..} token - | BS.length token < minlen = return Nothing + | BS.length token < headerLength = return Nothing | otherwise = do - let (hdrbin, cipher) = BS.splitAt minlen token - mskhdr <- decodeHeader hdrbin - let hdr = headerMask `xorHeader` mskhdr - idx = headerIndex hdr - counter = headerCounter hdr - return $ Just (idx, counter, cipher) - where - minlen = indexLength + counterLength + let (blk, cipher) = BS.splitAt headerLength token + (hdrbin, padding) = + BS.splitAt (indexLength + counterLength) $ C.ecbDecrypt headerKey blk + if BS.any (/= 0) padding + then return Nothing + else do + Header{..} <- decodeHeader hdrbin + return $ Just (headerIndex, headerCounter, cipher) -- | Encrypting a target value to get a token. encryptToken @@ -222,7 +242,8 @@ -> IO ByteString encryptToken mgr x = do (secret, idx) <- getEncryptSecret mgr - (counter, cipher) <- encrypt secret x + now <- getMonotonicTimeNSec + (counter, cipher) <- encrypt secret (bytestring64 now `BS.append` x) addHeader mgr idx counter cipher encrypt @@ -231,8 +252,8 @@ -> IO (Counter, ByteString) encrypt secret plain = do counter <- I.atomicModifyIORef' (secretCounter secret) (\i -> (i + 1, i)) - nonce <- makeNonce counter $ secretIV secret - let cipher = aes256gcmEncrypt plain (secretKey secret) nonce + let nonce = makeNonce counter $ secretIV secret + cipher = aes256gcmEncrypt plain (secretKey secret) nonce return (counter, cipher) -- | Decrypting a token to get a target value. @@ -246,7 +267,17 @@ Nothing -> return Nothing Just (idx, counter, cipher) -> do secret <- getDecryptSecret mgr idx - decrypt secret counter cipher + mplain <- decrypt secret counter cipher + now <- getMonotonicTimeNSec + return $ mplain >>= checkLifetime now + where + checkLifetime now plain + | BS.length plain < 8 = Nothing + | issued <= now && now - issued <= lifetimeNSec mgr = Just x + | otherwise = Nothing + where + (hdr, x) = BS.splitAt 8 plain + issued = word64 hdr decrypt :: Secret @@ -254,13 +285,13 @@ -> ByteString -> IO (Maybe ByteString) decrypt secret counter cipher = do - nonce <- makeNonce counter $ secretIV secret + let nonce = makeNonce counter $ secretIV secret return $ aes256gcmDecrypt cipher (secretKey secret) nonce -makeNonce :: Counter -> ByteString -> IO ByteString -makeNonce counter iv = do - cv <- BS.create ivLength $ \ptr -> poke (castPtr ptr) counter - return $ iv `BA.xor` cv +makeNonce :: Counter -> ByteString -> ByteString +makeNonce counter iv = iv `BA.xor` cv + where + cv = BS.replicate (ivLength - counterLength) 0 `BS.append` bytestring64 counter ---------------------------------------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crypto-token-0.2.0/crypto-token.cabal new/crypto-token-0.2.1/crypto-token.cabal --- old/crypto-token-0.2.0/crypto-token.cabal 2001-09-09 03:46:40.000000000 +0200 +++ new/crypto-token-0.2.1/crypto-token.cabal 2001-09-09 03:46:40.000000000 +0200 @@ -1,6 +1,6 @@ cabal-version: >=1.10 name: crypto-token -version: 0.2.0 +version: 0.2.1 license: BSD3 license-file: LICENSE maintainer: Kazu Yamamoto <[email protected]> @@ -9,6 +9,7 @@ description: Encrypted tokens/tickets to keep state in the client side. category: Cryptography, Network build-type: Simple +extra-source-files: ChangeLog.md source-repository head type: git @@ -19,15 +20,14 @@ default-language: Haskell2010 ghc-options: -Wall build-depends: - base >=4.9 && <5, - array, - bytestring, - crypton >=1.1.0 && <1.2, - network-byte-order, - ram + base >=4.11 && <5, + array >=0.5 && <0.6, + bytestring >=0.10 && <0.13, + crypton >=1.1.0 && <2.3, + network-byte-order >=0.1.6 && <0.2, + ram >=0.19 && <0.23 - if impl(ghc >=8) - default-extensions: Strict StrictData + default-extensions: Strict StrictData test-suite spec type: exitcode-stdio-1.0 @@ -41,7 +41,7 @@ default-extensions: Strict StrictData ghc-options: -Wall -threaded -rtsopts build-depends: - base >=4.9 && <5, - bytestring, + base >=4.11 && <5, + bytestring >=0.10 && <0.13, crypto-token, hspec diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crypto-token-0.2.0/test/TokenSpec.hs new/crypto-token-0.2.1/test/TokenSpec.hs --- old/crypto-token-0.2.0/test/TokenSpec.hs 2001-09-09 03:46:40.000000000 +0200 +++ new/crypto-token-0.2.1/test/TokenSpec.hs 2001-09-09 03:46:40.000000000 +0200 @@ -1,9 +1,13 @@ {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} module TokenSpec where import Control.Concurrent +import qualified Control.Exception as E import Crypto.Token +import Data.Bits (xor) +import qualified Data.ByteString as BS import Test.Hspec ---------------------------------------------------------------- @@ -11,18 +15,51 @@ spec :: Spec spec = do describe "crypto token" $ do - it "encrypt & decrypt ByteString" $ do - mgr <- spawnTokenManager $ defaultConfig{interval = 1, tokenLifetime = 3} + it "encrypt & decrypt ByteString" $ + roundTrip defaultConfig + it "lives as long as its lifetime, across rotations" $ do + mgr <- spawnTokenManager $ defaultConfig{interval = 1, tokenLifetime = 2} + threadDelay 900000 + -- made just before a rotation tf <- encryptToken mgr "Foo" - threadDelay 1100000 + threadDelay 1200000 decryptToken mgr tf `shouldReturn` Just "Foo" tb <- encryptToken mgr "Bar" - threadDelay 1100000 - decryptToken mgr tf `shouldReturn` Just "Foo" - decryptToken mgr tb `shouldReturn` Just "Bar" - threadDelay 1100000 + threadDelay 1300000 decryptToken mgr tf `shouldReturn` Nothing decryptToken mgr tb `shouldReturn` Just "Bar" - threadDelay 1100000 - decryptToken mgr tf `shouldReturn` Nothing - decryptToken mgr tb `shouldReturn` Nothing + killTokenManager mgr + + it "refuses a tampered token" $ do + mgr <- spawnTokenManager defaultConfig + t <- encryptToken mgr "Foo" + let flip1 i = BS.take i t <> BS.singleton (BS.index t i `xor` 1) <> BS.drop (i + 1) t + -- in the header, the payload and the tag + mapM_ + (\i -> decryptToken mgr (flip1 i) `shouldReturn` Nothing) + [0, 20, BS.length t - 1] + decryptToken mgr (BS.take 15 t) `shouldReturn` Nothing + killTokenManager mgr + it "gives unrelated headers to consecutive tokens" $ do + mgr <- spawnTokenManager defaultConfig + t1 <- encryptToken mgr "Foo" + t2 <- encryptToken mgr "Foo" + -- with a fixed mask only the counter's low byte would differ + length (filter id $ BS.zipWith (/=) (BS.take 16 t1) (BS.take 16 t2)) + `shouldSatisfy` (> 4) + killTokenManager mgr + + describe "configuration" $ do + it "survives a lifetime shorter than the interval" $ + roundTrip defaultConfig{interval = 10, tokenLifetime = 1} + it "survives more intervals than an index can name" $ + roundTrip defaultConfig{interval = 1, tokenLifetime = 100000} + it "refuses a non-positive interval" $ + spawnTokenManager defaultConfig{interval = 0} + `shouldThrow` (\(_ :: E.IOException) -> True) + +roundTrip :: Config -> IO () +roundTrip conf = + E.bracket (spawnTokenManager conf) killTokenManager $ \mgr -> do + t <- encryptToken mgr "Foo" + decryptToken mgr t `shouldReturn` Just "Foo"
