diff --git a/LICENSE.GPLv2 b/LICENSE.GPLv2
new file mode 100644
--- /dev/null
+++ b/LICENSE.GPLv2
@@ -0,0 +1,339 @@
+              GNU GENERAL PUBLIC LICENSE
+                 Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                     Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+             GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                     NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+              END OF TERMS AND CONDITIONS
+
+     How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program 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 2 of the License, or
+    (at your option) any later version.
+
+    This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/src-bench/Main.hs b/src-bench/Main.hs
new file mode 100644
--- /dev/null
+++ b/src-bench/Main.hs
@@ -0,0 +1,167 @@
+{-# LANGUAGE BangPatterns      #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+{-# LANGUAGE CApiFFI           #-}
+
+{-# OPTIONS_GHC -Wall  #-}
+
+-- |
+-- Copyright: © 2020  Herbert Valerio Riedel
+-- SPDX-License-Identifier: GPL-2.0-or-later
+--
+module Main
+    ( main
+    , xor32ByteString'ref
+    , xor32ByteString'v3
+    , xor32ByteString'v4
+    ) where
+
+import           Control.Exception        (assert)
+import           Control.Monad
+import           Criterion.Main
+import           Data.Bits
+import qualified Data.ByteString          as BS
+import           Data.ByteString.Internal as BS
+import qualified Data.ByteString.Short    as SBS
+import           Data.Word                (Word32, Word8)
+import           Foreign.ForeignPtr
+import           Foreign.Ptr
+import           Foreign.Storable
+import           GHC.ByteOrder            (ByteOrder (..), targetByteOrder)
+
+import qualified Data.XOR                 as IUT
+
+main :: IO ()
+main = defaultMain benches
+
+benches :: [Benchmark]
+benches =
+    [ doGroup "4k"   bs4k
+    , doGroup "4k1"  bs4k1
+    , doGroup "4k2"  bs4k2
+    , doGroup "4k3"  bs4k3
+    , doGroup "32k"  bs32k
+    , doGroup "256k" bs256k
+    ]
+  where
+    doGroup label bs = let sbs = SBS.toShort bs in bgroup label
+      [ bench "REF"      $ whnf (xor32ByteString'ref msk)       bs
+      , bench "IUT"      $ whnf (IUT.xor32StrictByteString msk) bs
+      , bench "IUT/SBS"  $ whnf (IUT.xor32ShortByteString msk) sbs
+      , bench "v3"       $ whnf (xor32ByteString'v3 msk)        bs
+      , bench "v4"       $ whnf (xor32ByteString'v4 msk)        bs
+      , bench "REF 8bit" $ whnf (xor8StrictByteString'ref msk8) bs
+      , bench "IUT 8bit" $ whnf (IUT.xor8StrictByteString msk8) bs
+      ]
+
+    {-# NOINLINE bs32k #-}
+    !bs4k = BS.replicate (4*1024) 0x55
+    !bs4k1 = BS.replicate (4*1024+1) 0x55
+    !bs4k2 = BS.replicate (4*1024+2) 0x55
+    !bs4k3 = BS.replicate (4*1024+3) 0x55
+    !bs32k = BS.replicate (32*1024) 0x55
+    !bs256k = BS.replicate (256*1024) 0x55
+
+    {-# NOINLINE msk #-}
+    msk = 0x12345678
+
+    msk8 = 0x42
+
+----------------------------------------------------------------------------
+
+-- reference impl
+
+{-# NOINLINE xor32ByteString'ref #-}
+xor32ByteString'ref :: Word32 -> BS.ByteString -> BS.ByteString
+xor32ByteString'ref 0    = id
+xor32ByteString'ref msk0 = snd . BS.mapAccumL go msk0
+  where
+    go :: Word32 -> Word8 -> (Word32,Word8)
+    go msk b = let b'   = fromIntegral msk' `xor` b
+                   msk' = rotateL msk 8
+               in b' `seq` (msk',b')
+
+
+{-# NOINLINE xor8StrictByteString'ref #-}
+xor8StrictByteString'ref :: Word8 -> BS.ByteString -> BS.ByteString
+xor8StrictByteString'ref 0    = id
+xor8StrictByteString'ref msk0 = BS.map (xor msk0)
+
+-- {-# NOINLINE xor32ByteString'v2 #-}
+-- xor32ByteString'v2 :: Word32 -> BS.ByteString -> BS.ByteString
+-- xor32ByteString'v2 msk0 = snd . BS.mapAccumL go mskstr
+--   where
+--     mskstr :: [Word8]
+--     mskstr = cycle (map fromIntegral (tail (take 5 (iterate rotl8 msk0))))
+--
+--     rotl8 :: Word32 -> Word32
+--     rotl8 = flip rotateL 8
+--
+--     go (x:xs) b = let !b' = xor x b in (xs,b')
+
+{-# NOINLINE xor32ByteString'v3 #-}
+xor32ByteString'v3 :: Word32 -> BS.ByteString -> BS.ByteString
+xor32ByteString'v3 0 bs = bs
+xor32ByteString'v3 _ bs | BS.null bs = bs
+xor32ByteString'v3 msk0 (BS.PS x s l)
+    = unsafeCreate l $ \p8 ->
+        withForeignPtr x $ \f -> do
+          memcpy p8 (f `plusPtr` s) (fromIntegral l)
+          let p32 = castPtr p8 :: Ptr Word32
+              l32 = l `quot` 4
+              p32end = p32 `plusPtr` (l32*4)
+          unless (alignPtr p32 4 == p32) $ fail "bytestring allocation not aligned"
+          xor32PtrAligned msk0 p32 (l32*4)
+          _ <- xor32PtrNonAligned msk0 (castPtr p32end) (l - (l32*4))
+          return ()
+
+{-# NOINLINE xor32ByteString'v4 #-}
+xor32ByteString'v4 :: Word32 -> BS.ByteString -> BS.ByteString
+xor32ByteString'v4  0 bs = bs
+xor32ByteString'v4  _ bs | BS.null bs = bs
+xor32ByteString'v4 msk0 (BS.PS x s l)
+    = unsafeCreate l $ \p8 ->
+        withForeignPtr x $ \f -> do
+          memcpy p8 (f `plusPtr` s) (fromIntegral l)
+          _ <- IUT.xor32CStringLen msk0 (castPtr p8,l)
+          return ()
+
+{-# INLINE xor32PtrNonAligned #-}
+xor32PtrNonAligned :: Word32 -> Ptr Word8 -> Int -> IO Word32
+xor32PtrNonAligned mask0 _ 0 = return mask0
+xor32PtrNonAligned mask0 p0 n = go mask0 p0
+  where
+    p' = p0 `plusPtr` n
+    go m p
+      | p == p'   = return m
+      | otherwise = do
+          let m' = rotateL m 8
+          xor8Ptr1 (fromIntegral m') p
+          go m' (p `plusPtr` 1)
+
+{-# INLINE xor32PtrAligned #-}
+xor32PtrAligned :: Word32 -> Ptr Word32 -> Int -> IO ()
+xor32PtrAligned _ _ 0 = return ()
+xor32PtrAligned mask0be p0 n
+  = assert (p0 == alignPtr p0 4 && n `rem` 4 == 0) $ go p0
+  where
+    p' = p0 `plusPtr` n
+    go p
+      | p == p'   = return ()
+      | otherwise = do { xor32Ptr1 mask0 p; go (p `plusPtr` 4) }
+
+    mask0 = case targetByteOrder of
+              LittleEndian -> {- byteSwap32 -} mask0be
+              BigEndian    -> mask0be
+
+----------------------------------------------------------------------------
+
+xor8Ptr1 :: Word8 -> Ptr Word8 -> IO ()
+xor8Ptr1 msk ptr  = do { x <- peek ptr; poke ptr (xor msk x) }
+
+-- xor16Ptr1 :: Word16 -> Ptr Word16 -> IO ()
+-- xor16Ptr1 msk ptr = do { x <- peek ptr; poke ptr (xor msk x) }
+
+xor32Ptr1 :: Word32 -> Ptr Word32 -> IO ()
+xor32Ptr1 msk ptr = do { x <- peek ptr; poke ptr (xor msk x) }
+
diff --git a/src-test/Main.hs b/src-test/Main.hs
new file mode 100644
--- /dev/null
+++ b/src-test/Main.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE Haskell2010       #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Copyright: © 2019-2020  Herbert Valerio Riedel
+-- SPDX-License-Identifier: GPL-2.0-or-later
+--
+module Main (main)  where
+
+import           Control.Monad
+import           Data.Bits
+import           Data.Int
+import           Data.Monoid             (mempty)
+import           Data.Word
+import qualified Foreign.Marshal         as F
+import qualified Foreign.Ptr             as F
+import           Text.Printf             (printf)
+
+import qualified Data.ByteString         as BS
+import qualified Data.ByteString.Lazy    as BL
+import qualified Data.ByteString.Short   as SBS
+
+import qualified Test.QuickCheck.Monadic as QCM
+import           Test.Tasty
+import           Test.Tasty.HUnit
+import           Test.Tasty.QuickCheck   as QC
+
+import qualified Data.XOR                as IUT
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Tests"
+  [ testGroup "hello"
+    [ testCase "REF"             $ xor32StrictByteString'ref  0x37fa213d "\x7f\x9f\x4d\x51\x58" @?= "Hello"
+    , testCase "S.ByteString"    $ IUT.xor32StrictByteString' 0x37fa213d "\x7f\x9f\x4d\x51\x58" @?= (0xfa213d37,"Hello")
+    , testCase "L.ByteString"    $ IUT.xor32LazyByteString    0x37fa213d "\x7f\x9f\x4d\x51\x58" @?= "Hello"
+    , testCase "ShortByteString" $ IUT.xor32ShortByteString   0x37fa213d "\x7f\x9f\x4d\x51\x58" @?= "Hello"
+    ]
+
+  , testGroup "empty"
+    [ testProperty "S.ByteString"    $ \msk -> IUT.xor32StrictByteString msk mempty === Data.Monoid.mempty
+    , testProperty "L.ByteString"    $ \msk -> IUT.xor32LazyByteString   msk mempty === mempty
+    , testProperty "ShortByteString" $ \msk -> IUT.xor32ShortByteString  msk mempty === mempty
+    ]
+
+  , testGroup "zero-xor"
+    [ testProperty "S.ByteString" $ \xs -> let bs = BS.pack xs in IUT.xor32StrictByteString 0 bs === bs
+    , testProperty "L.ByteString" $ \xs -> let bs = BL.fromChunks (map BS.pack xs) in IUT.xor32LazyByteString 0 bs === BL.fromChunks [BS.pack (concat xs)]
+    , testProperty "ShortByteString" $ \xs -> let bs = SBS.pack xs in IUT.xor32ShortByteString 0 bs === bs
+    ]
+
+  , testProperty "xor32Ptr" $ \msk lb len8 -> QCM.monadicIO $ do
+       let bufsize, ofs, len :: Int
+           bufsize = 2048
+           ofs     = 512 + fromIntegral (lb :: Int8)
+           len     = fromIntegral (len8 :: Word8)
+
+       QCM.run $ F.allocaBytes bufsize $ \bufptr -> do
+
+         origbuf <- BS.packCStringLen (bufptr, bufsize)
+
+         let (origbufPre, tmp) = BS.splitAt ofs origbuf
+             (origbufMid, origbufPost) = BS.splitAt len tmp
+
+         _ <- IUT.xor32CStringLen msk (F.castPtr bufptr `F.plusPtr` ofs,len)
+         newbufIut <- BS.packCStringLen (bufptr, bufsize)
+
+         let newbufRef = BS.concat [origbufPre, xor32StrictByteString'ref msk origbufMid, origbufPost]
+
+         unless (BS.length newbufRef == bufsize) $ fail "internal error"
+
+         unless (newbufRef == newbufIut) $ do
+           putStrLn $ show (msk,bufsize, ofs, len)
+           putStrLn $ "orig " ++ concatMap (printf "%02x") (BS.unpack origbuf)
+           putStrLn $ "ref  " ++ concatMap (printf "%02x") (BS.unpack newbufRef)
+           putStrLn $ "iut  " ++ concatMap (printf "%02x") (BS.unpack newbufIut)
+
+           forM_ (zip3 [0..] (BS.unpack newbufRef) (BS.unpack newbufIut)) $ \(j,ref8,iut8) ->
+             unless (ref8 == iut8) $
+               printf "%d (%d): %02x %02x\n" (j::Int) (j-ofs) ref8 iut8
+
+         return $! newbufRef == newbufIut
+
+  , testGroup "ref-vs-iut 32-bit"
+    [ testProperty "S.ByteString" $ \xs msk ->
+        let bs = BS.pack xs
+        in IUT.xor32StrictByteString msk bs === xor32StrictByteString'ref msk bs
+    , testProperty "L.ByteString" $ \xs msk ->
+        let bs  = BL.fromChunks (map BS.pack xs)
+            bs' = BS.pack (concat xs)
+        in IUT.xor32LazyByteString msk bs === BL.fromStrict (xor32StrictByteString'ref msk bs')
+    , testProperty "ShortByteString" $ \xs msk ->
+        let bs = SBS.pack xs
+        in IUT.xor32ShortByteString msk bs === xor32ShortByteString'ref msk bs
+    ]
+
+  , testGroup "ref-vs-iut 8-bit"
+    [ testProperty "S.ByteString" $ \xs msk ->
+        let bs = BS.pack xs
+        in IUT.xor8StrictByteString msk bs === xor8StrictByteString'ref msk bs
+    , testProperty "L.ByteString" $ \xs msk ->
+        let bs  = BL.fromChunks (map BS.pack xs)
+        in IUT.xor8LazyByteString msk bs === xor8LazyByteString'ref msk bs
+    , testProperty "ShortByteString" $ \xs msk ->
+        let bs = SBS.pack xs
+        in IUT.xor8ShortByteString msk bs === xor8ShortByteString'ref msk bs
+    ]
+
+  , testGroup "self-inverse"
+    [ testProperty "S.ByteString" $ \xs msk ->
+        let bs = BS.pack xs
+        in IUT.xor32StrictByteString msk (IUT.xor32StrictByteString msk bs) === bs
+    , testProperty "L.ByteString" $ \xs msk ->
+        let bs  = BL.fromChunks (map BS.pack xs)
+            bs' = BL.fromChunks [BS.pack (concat xs)]
+        in IUT.xor32LazyByteString msk (IUT.xor32LazyByteString msk bs) === bs'
+    , testProperty "ShortByteString" $ \xs msk ->
+        let bs = SBS.pack xs
+        in IUT.xor32ShortByteString msk (IUT.xor32ShortByteString msk bs) === bs
+    ]
+  ]
+
+xor32StrictByteString'ref :: Word32 -> BS.ByteString -> BS.ByteString
+xor32StrictByteString'ref 0    = id
+xor32StrictByteString'ref msk0 = snd . BS.mapAccumL go msk0
+  where
+    go :: Word32 -> Word8 -> (Word32,Word8)
+    go msk b = let b'   = fromIntegral msk' `xor` b
+                   msk' = rotateL msk 8
+               in b' `seq` (msk',b')
+
+xor8StrictByteString'ref :: Word8 -> BS.ByteString -> BS.ByteString
+xor8StrictByteString'ref 0    = id
+xor8StrictByteString'ref msk0 = BS.map (xor msk0)
+
+liftSBS :: (t -> BS.ByteString -> BS.ByteString) -> t -> SBS.ShortByteString -> SBS.ShortByteString
+liftSBS op = \msk -> SBS.toShort . op msk . SBS.fromShort
+
+liftBL :: (t -> BS.ByteString -> BS.ByteString) -> t -> BL.ByteString -> BL.ByteString
+liftBL op = \msk -> BL.fromStrict . op msk . BL.toStrict
+
+xor32ShortByteString'ref :: Word32 -> SBS.ShortByteString -> SBS.ShortByteString
+xor32ShortByteString'ref = liftSBS xor32StrictByteString'ref
+
+xor8ShortByteString'ref :: Word8 -> SBS.ShortByteString -> SBS.ShortByteString
+xor8ShortByteString'ref = liftSBS xor8StrictByteString'ref
+
+xor8LazyByteString'ref :: Word8 -> BL.ByteString -> BL.ByteString
+xor8LazyByteString'ref = liftBL xor8StrictByteString'ref
diff --git a/src/Data/XOR.hs b/src/Data/XOR.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/XOR.hs
@@ -0,0 +1,373 @@
+{-# LANGUAGE BangPatterns  #-}
+{-# LANGUAGE Haskell2010   #-}
+{-# LANGUAGE MagicHash     #-}
+{-# LANGUAGE UnboxedTuples #-}
+
+{-# LANGUAGE Trustworthy   #-}
+
+-- |
+-- Copyright: © 2020  Herbert Valerio Riedel
+-- SPDX-License-Identifier: GPL-2.0-or-later
+--
+-- Apply XOR-masks to 'BS.ByteString's and memory regions.
+--
+module Data.XOR
+    ( -- * Apply 32-bit XOR mask
+      xor32StrictByteString
+    , xor32StrictByteString'
+    , xor32LazyByteString
+    , xor32ShortByteString
+    , xor32CStringLen
+
+      -- * Apply 8-bit XOR mask
+    , xor8StrictByteString
+    , xor8LazyByteString
+    , xor8ShortByteString
+    , xor8CStringLen
+
+    ) where
+
+-- base
+import           Control.Exception              (assert)
+import           Control.Monad                  (void)
+import           Control.Monad.ST               (ST, runST)
+import           Data.Bits
+import           Data.Tuple                     (swap)
+import           Endianness                     (ByteOrder (..), Word32, Word8, byteSwap32,
+                                                 targetByteOrder)
+import           Foreign.C                      (CStringLen)
+import           Foreign.ForeignPtr             (withForeignPtr)
+import           Foreign.Ptr                    (Ptr, alignPtr, castPtr, minusPtr, plusPtr)
+import           Foreign.Storable               (peek, poke)
+import           System.IO.Unsafe               (unsafeDupablePerformIO)
+
+import qualified GHC.Exts                       as X
+import qualified GHC.ST                         as X
+import qualified GHC.Word                       as X
+
+-- bytestring
+import qualified Data.ByteString                as BS
+import           Data.ByteString.Internal       (mallocByteString, memcpy)
+import qualified Data.ByteString.Internal       as BS (ByteString (..))
+import qualified Data.ByteString.Lazy.Internal  as BL (ByteString (..))
+import qualified Data.ByteString.Short          as SBS
+import           Data.ByteString.Short.Internal (ShortByteString (SBS))
+
+----------------------------------------------------------------------------
+
+{- high-level reference impl
+
+-- about 6-7 times slower
+xor32StrictByteString'ref :: Word32 -> BS.ByteString -> BS.ByteString
+xor32StrictByteString'ref 0    = id
+xor32StrictByteString'ref msk0 = snd . BS.mapAccumL go msk0
+  where
+    go :: Word32 -> Word8 -> (Word32,Word8)
+    go msk b = let b'   = fromIntegral msk' `xor` b
+                   msk' = rotateL msk 8
+               in b' `seq` (msk',b')
+
+-- about 3 times slower
+xor8StrictByteString'ref :: Word8 -> BS.ByteString -> BS.ByteString
+xor8StrictByteString'ref 0    = id
+xor8StrictByteString'ref msk0 = BS.map (xor msk0)
+
+-}
+
+-- | Apply 32-bit XOR mask (considered as four octets in big-endian order) to 'BS.ByteString'.
+--
+-- >>> xor32StrictByteString 0x37fa213d "\x7f\x9f\x4d\x51\x58"
+-- "Hello"
+--
+-- In other words, the 32-bit word @0x37fa213d@ is taken as the infinite series of octets @('cycle' [0x37,0xfa,0x21,0x3d])@ and 'xor'ed with the respective octets from the input 'BS.ByteString'.
+--
+-- The 'xor' laws give rise to the following laws:
+--
+-- prop> xor32StrictByteString m (xor32StrictByteString m x) == x
+--
+-- prop> xor32StrictByteString 0 x == x
+--
+-- prop> xor32StrictByteString m (xor32StrictByteString n x) == xor32StrictByteString (m `xor` n) x
+--
+-- This function is semantically equivalent to the (less efficient) implementation shown below
+--
+-- > xor32StrictByteString'ref :: Word32 -> BS.ByteString -> BS.ByteString
+-- > xor32StrictByteString'ref 0    = id
+-- > xor32StrictByteString'ref msk0 = snd . BS.mapAccumL go msk0
+-- >   where
+-- >     go :: Word32 -> Word8 -> (Word32,Word8)
+-- >     go msk b = let b'   = fromIntegral (msk' .&. 0xff) `xor` b
+-- >                    msk' = rotateL msk 8
+-- >                in (msk',b')
+--
+-- The 'xor32StrictByteString' implementation is about 6-7 times faster than the naive implementation above.
+xor32StrictByteString :: Word32 -> BS.ByteString -> BS.ByteString
+xor32StrictByteString 0 bs   = bs
+xor32StrictByteString _ bs   | BS.null bs = bs
+xor32StrictByteString msk bs = fst (xor32StrictByteString'' msk bs)
+
+-- | Convenience version of 'xor32StrictByteString' which also returns the rotated XOR-mask useful for chained masking.
+--
+-- >>> xor32StrictByteString' 0x37fa213d "\x7f\x9f\x4d\x51\x58"
+-- (0xfa213d37,"Hello")
+--
+xor32StrictByteString' :: Word32 -> BS.ByteString -> (Word32,BS.ByteString)
+xor32StrictByteString' 0 bs   = (0,bs)
+xor32StrictByteString' msk bs | BS.null bs = (msk,bs)
+xor32StrictByteString' msk bs = swap (xor32StrictByteString'' msk bs)
+
+-- | Variant of 'xor32StrictByteString' for masking lazy 'BL.ByteString's.
+--
+-- >>> xor32LazyByteString 0x37fa213d "\x7f\x9f\x4d\x51\x58"
+-- "Hello"
+--
+xor32LazyByteString :: Word32 -> BL.ByteString -> BL.ByteString
+xor32LazyByteString 0 = id
+xor32LazyByteString msk0 = go msk0
+  where
+    go _ BL.Empty = BL.Empty
+    go msk (BL.Chunk x xs) = BL.Chunk x' (go msk' xs)
+      where
+        (x',msk') = xor32StrictByteString'' msk x
+
+{-# INLINE xor32StrictByteString'' #-}
+-- internal
+xor32StrictByteString'' :: Word32 -> BS.ByteString -> (BS.ByteString,Word32)
+xor32StrictByteString'' msk0 (BS.PS x s l)
+    = unsafeCreate' l $ \p8 ->
+        withForeignPtr x $ \f -> do
+          memcpy p8 (f `plusPtr` s) (fromIntegral l)
+
+          case remPtr p8 4 of
+            0 -> do
+              let trailer = l `rem` 4
+                  lbytes = l - trailer
+              xor32PtrAligned msk0 (castPtr p8) lbytes
+              xor32PtrNonAligned msk0 (p8 `plusPtr` lbytes) trailer
+            _ ->
+              -- misaligned bytestring...
+              --
+              -- This should not happen, as newly allocated
+              -- bytestrings ought to be word-aligned; but if the
+              -- impossible does happen we have a semantically sound
+              -- codepath to jump to...
+              xor32Ptr msk0 p8 l
+
+
+
+-- | Apply 32-bit XOR mask (considered as four octets in big-endian order) to 'SBS.ShortByteString'. See also 'xor32StrictByteString'.
+--
+-- >>> xor32ShortByteString 0x37fa213d "\x7f\x9f\x4d\x51\x58"
+-- "Hello"
+--
+xor32ShortByteString :: Word32 -> SBS.ShortByteString -> SBS.ShortByteString
+xor32ShortByteString 0 sbs = sbs
+xor32ShortByteString _ sbs | SBS.null sbs = sbs
+xor32ShortByteString mask0be sbs = runST $ do
+    tmp <- newSBS len
+
+    let loop4 i
+          | i == len4  = return ()
+          | otherwise  = writeWord32Array tmp i (indexWord32Array sbs i `xor` mask0) >> loop4 (i+1)
+
+    loop4 0
+
+    let writeXor8 ofs msk8 = writeWord8Array tmp ofs (indexWord8Array sbs ofs `xor` msk8)
+
+    case len1 of
+      0 -> return ()
+      1 -> do
+        writeXor8 (len-1) (fromIntegral (shiftR mask0be 24))
+      2 -> do
+        writeXor8 (len-2) (fromIntegral (shiftR mask0be 24))
+        writeXor8 (len-1) (fromIntegral (shiftR mask0be 16))
+      3 -> do
+        writeXor8 (len-3) (fromIntegral (shiftR mask0be 24))
+        writeXor8 (len-2) (fromIntegral (shiftR mask0be 16))
+        writeXor8 (len-1) (fromIntegral (shiftR mask0be  8))
+      _ -> undefined -- impossible
+
+    unsafeFreezeSBS tmp
+  where
+    len = SBS.length sbs
+    (len4,len1) = quotRem len 4
+
+    mask0 = case targetByteOrder of
+              LittleEndian -> byteSwap32 mask0be
+              BigEndian    -> mask0be
+
+
+{-# INLINEABLE xor32CStringLen #-}
+-- | Apply 32-bit XOR mask (considered as four octets in big-endian order) to memory region expressed as base-pointer and size. The returned value is the input mask rotated by the word-size remained of the memory region size (useful for chained xor-masking of multiple memory-fragments).
+xor32CStringLen :: Word32 -> CStringLen -> IO Word32
+xor32CStringLen m (p,l) = xor32Ptr m (castPtr p) l
+
+{-# INLINEABLE xor32Ptr #-}
+xor32Ptr :: Word32 -> Ptr Word8 -> Int -> IO Word32
+xor32Ptr 0      !_  !_ = return 0
+xor32Ptr !mask0 !_   0 = return mask0
+xor32Ptr !mask0 !p0 !n
+  | n < 4 = xor32PtrNonAligned mask0 p0 n
+  | n < 0 = fail "xor32Ptr: negative size argument not supported"
+xor32Ptr !mask0 !p0 !n
+  | assert (p0 <= p1 && p1 <= p2 && p2 <= p3 && n0 < 4 && n2 < 4) False = undefined -- assert invariants
+  | n1 == 0 = xor32PtrNonAligned mask0 p0 n
+  | n0 == 0 = do
+      xor32PtrAligned    mask0 p1 n1
+      xor32PtrNonAligned mask0 p2 n2
+  | otherwise = do
+      mask1 <- xor32PtrNonAligned mask0 p0 n0
+      xor32PtrAligned    mask1 p1 n1
+      xor32PtrNonAligned mask1 p2 n2
+  where
+    -- Invariants: p0 <= p1 <= p2 <= p3
+    --             0  <= n0  < 4
+    --             0  <= n1
+    --             0  <= n2  < 4
+    --             n  == n0+n1+n2 >= 4
+    p1 = castPtr (alignPtr p0 d)
+    p2 = alignPtrDown p3 d
+    p3 = plusPtr p0 n
+    d  = 4
+
+    n0 = p1 `minusPtr` p0
+    n1 = p2 `minusPtr` p1
+    n2 = p3 `minusPtr` p2
+
+-- internal
+xor32PtrNonAligned :: Word32 -> Ptr Word8 -> Int -> IO Word32
+xor32PtrNonAligned mask0 _ 0 = return mask0
+xor32PtrNonAligned mask0 p 1 = do
+  let mask1 = rotateL mask0 8
+  xor8Ptr1 (fromIntegral mask1) p
+  return mask1
+xor32PtrNonAligned mask0 p 2 = do
+  xor8Ptr1 (fromIntegral (mask0 `shiftR` 24)) p
+  let mask1 = mask0 `rotateL` 16
+  xor8Ptr1 (fromIntegral mask1) (p `plusPtr` 1)
+  return mask1
+xor32PtrNonAligned mask0 p 3 = do
+  xor8Ptr1 (fromIntegral (mask0 `shiftR` 24)) p
+  xor8Ptr1 (fromIntegral (mask0 `shiftR` 16)) (p `plusPtr` 1)
+  let mask1 = mask0 `rotateL` 24
+  xor8Ptr1 (fromIntegral mask1) (p `plusPtr` 2)
+  return mask1
+xor32PtrNonAligned mask0 p0 n = go mask0 p0
+  where
+    p' = p0 `plusPtr` n
+    go m p
+      | p == p'   = return m
+      | otherwise = do
+          let m' = rotateL m 8
+          xor8Ptr1 (fromIntegral m') p
+          go m' (p `plusPtr` 1)
+
+-- internal
+xor32PtrAligned :: Word32 -> Ptr Word32 -> Int -> IO ()
+xor32PtrAligned _ _ 0 = return ()
+xor32PtrAligned mask0be p0 n
+  = assert (p0 `remPtr` 4 == 0 && n `rem` 4 == 0) $ go p0
+  where
+    p' = p0 `plusPtr` n
+    go p
+      | p == p'   = return ()
+      | otherwise = do { xor32Ptr1 mask0 p; go (p `plusPtr` 4) }
+
+    mask0 = case targetByteOrder of
+              LittleEndian -> byteSwap32 mask0be
+              BigEndian    -> mask0be
+
+----------------------------------------------------------------------------
+
+remPtr :: Ptr a -> Int -> Int
+remPtr (X.Ptr x) (X.I# d) = X.I# (X.remAddr# x d)
+
+alignPtrDown :: Ptr a -> Int -> Ptr a
+alignPtrDown p i
+  = case remPtr p i of
+      0 -> p
+      n -> plusPtr p (negate n)
+
+xor8Ptr1 :: Word8 -> Ptr Word8 -> IO ()
+xor8Ptr1 msk ptr  = do { x <- peek ptr; poke ptr (xor msk x) }
+
+-- xor16Ptr1 :: Word16 -> Ptr Word16 -> IO ()
+-- xor16Ptr1 msk ptr = do { x <- peek ptr; poke ptr (xor msk x) }
+
+xor32Ptr1 :: Word32 -> Ptr Word32 -> IO ()
+xor32Ptr1 msk ptr = do { x <- peek ptr; poke ptr (xor msk x) }
+
+{-# INLINE unsafeCreate' #-}
+unsafeCreate' :: Int -> (Ptr Word8 -> IO a) -> (BS.ByteString, a)
+unsafeCreate' l0 f0 = unsafeDupablePerformIO (create' l0 f0)
+  where
+    {-# INLINE create' #-}
+    create' :: Int -> (Ptr Word8 -> IO a) -> IO (BS.ByteString, a)
+    create' l f = do
+        fp <- mallocByteString l
+        res <- withForeignPtr fp $ \p -> f p
+        return (BS.PS fp 0 l, res)
+
+----------------------------------------------------------------------------
+-- single octet masks -- trivially mapped to 32-bit versions
+
+expandW8ToW32 :: Word8 -> Word32
+expandW8ToW32 x = x' .|. (x' `shiftL` 16)
+  where
+    x' = fromIntegral x .|. (fromIntegral x `shiftL` 8)
+
+
+-- | Apply 8-bit XOR mask to each octet of a 'BS.ByteString'.
+--
+-- >>> xor8StrictByteString 0x20 "Hello"
+-- "hELLO"
+--
+-- This function is a faster implementation of the semantically equivalent function shown below:
+--
+-- > xor8StrictByteString'ref :: Word8 -> BS.ByteString -> BS.ByteString
+-- > xor8StrictByteString'ref 0    = id
+-- > xor8StrictByteString'ref msk0 = BS.map (xor msk0)
+--
+xor8StrictByteString :: Word8 -> BS.ByteString -> BS.ByteString
+xor8StrictByteString x = xor32StrictByteString (expandW8ToW32 x)
+
+-- | Apply 8-bit XOR mask to each octet of a lazy 'BL.ByteString'.
+--
+-- See also 'xor8StrictByteString'
+xor8LazyByteString :: Word8 -> BL.ByteString -> BL.ByteString
+xor8LazyByteString x = xor32LazyByteString (expandW8ToW32 x)
+
+-- | Apply 8-bit XOR mask to each octet of a 'SBS.ShortByteString'.
+--
+-- See also 'xor8StrictByteString'
+xor8ShortByteString :: Word8 -> SBS.ShortByteString -> SBS.ShortByteString
+xor8ShortByteString x = xor32ShortByteString (expandW8ToW32 x)
+
+-- | Apply 8-bit XOR mask to each octet of a memory region expressed as start address and length in bytes.
+--
+-- See also 'xor8StrictByteString'
+xor8CStringLen :: Word8 -> CStringLen -> IO ()
+xor8CStringLen x (p,l) = void (xor32Ptr (expandW8ToW32 x) (castPtr p) l)
+
+----------------------------------------------------------------------------
+-- The missing mutable ShortByteString abstraction
+
+data MShortByteString s = MSBS (X.MutableByteArray# s)
+
+newSBS :: Int -> ST s (MShortByteString s)
+newSBS (X.I# len#) = X.ST $ \s0 -> case X.newByteArray# len# s0 of (# s, mba# #) -> (# s, MSBS mba# #)
+
+indexWord8Array :: ShortByteString -> Int -> Word8
+indexWord8Array (SBS ba#) (X.I# i#) = X.W8# (X.indexWord8Array# ba# i#)
+
+writeWord8Array :: MShortByteString s -> Int -> Word8 -> ST s ()
+writeWord8Array (MSBS mba#) (X.I# i#) (X.W8# w#) = X.ST $ \s0 -> case X.writeWord8Array# mba# i# w# s0 of s -> (# s, () #)
+
+indexWord32Array :: ShortByteString -> Int -> Word32
+indexWord32Array (SBS ba#) (X.I# i#) = X.W32# (X.indexWord32Array# ba# i#)
+
+writeWord32Array :: MShortByteString s -> Int -> Word32 -> ST s ()
+writeWord32Array (MSBS mba#) (X.I# i#) (X.W32# w#) = X.ST $ \s0 -> case X.writeWord32Array# mba# i# w# s0 of s -> (# s, () #)
+
+unsafeFreezeSBS :: MShortByteString s -> ST s ShortByteString
+unsafeFreezeSBS (MSBS mba#) = X.ST $ \s0 -> case X.unsafeFreezeByteArray# mba# s0 of (# s, ba# #) -> (# s, SBS ba# #)
diff --git a/src/Endianness.hs b/src/Endianness.hs
new file mode 100644
--- /dev/null
+++ b/src/Endianness.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE CPP         #-}
+{-# LANGUAGE Haskell2010 #-}
+
+-- |
+-- Copyright: © 2019-2020  Herbert Valerio Riedel
+-- SPDX-License-Identifier: GPL-2.0-or-later
+--
+module Endianness
+    ( Word8, Word16, Word32, Word64
+    , ByteOrder(LittleEndian,BigEndian), targetByteOrder
+
+    , byteSwap16
+    , byteSwap32
+    , byteSwap64
+
+    , pokeWord16be
+    , pokeWord32be
+    , pokeWord64be
+    , peekWord16be
+    , peekWord32be
+    , peekWord64be
+
+    ) where
+
+import           Data.Word        (Word16, Word32, Word64, Word8)
+import           Foreign.Ptr
+import           Foreign.Storable
+import           GHC.ByteOrder    (ByteOrder (..), targetByteOrder)
+
+#if MIN_VERSION_base(4,7,0)
+import           Data.Word        (byteSwap16, byteSwap32, byteSwap64)
+#else
+import           Data.Bits
+
+-- supply missing byteSwap operations
+
+byteSwap16 :: Word16 -> Word16
+byteSwap16 = (`rotateL` 8)
+
+byteSwap32 :: Word32 -> Word32
+byteSwap32 x
+  = (x                  `shiftR` 24)  .|.
+    ((x .&. 0x00ff0000) `shiftR`  8)  .|.
+    ((x .&. 0x0000ff00) `shiftL`  8)  .|.
+    (x                  `shiftL` 24)
+
+byteSwap64 :: Word64 -> Word64
+byteSwap64 x = xh .|. (xl `shiftL` 32)
+  where
+    xl = fromIntegral (byteSwap32 (fromIntegral x))
+    xh = fromIntegral (byteSwap32 (fromIntegral (x `shiftR` 32)))
+#endif
+
+pokeWord16be :: Ptr Word16 -> Word16 -> IO ()
+pokeWord16be = case targetByteOrder of
+  BigEndian    -> poke
+  LittleEndian -> \p w -> poke p (byteSwap16 w)
+
+pokeWord32be :: Ptr Word32 -> Word32 -> IO ()
+pokeWord32be = case targetByteOrder of
+  BigEndian    -> poke
+  LittleEndian -> \p w -> poke p (byteSwap32 w)
+
+pokeWord64be :: Ptr Word64 -> Word64 -> IO ()
+pokeWord64be = case targetByteOrder of
+  BigEndian    -> poke
+  LittleEndian -> \p w -> poke p (byteSwap64 w)
+
+
+peekWord16be :: Ptr Word16 -> IO Word16
+peekWord16be = case targetByteOrder of
+  BigEndian    -> peek
+  LittleEndian -> \p -> fmap byteSwap16 (peek p)
+
+peekWord32be :: Ptr Word32 -> IO Word32
+peekWord32be = case targetByteOrder of
+  BigEndian    -> peek
+  LittleEndian -> \p -> fmap byteSwap32 (peek p)
+
+peekWord64be :: Ptr Word64 -> IO Word64
+peekWord64be = case targetByteOrder of
+  BigEndian    -> peek
+  LittleEndian -> \p -> fmap byteSwap64 (peek p)
diff --git a/xor.cabal b/xor.cabal
new file mode 100644
--- /dev/null
+++ b/xor.cabal
@@ -0,0 +1,89 @@
+cabal-version:       2.2
+name:                xor
+version:             0.0.1.0
+
+category:            Data, Codec
+author:              Herbert Valerio Riedel
+maintainer:          hvr@gnu.org
+bug-reports:         https://github.com/hvr/xor/issues
+
+copyright:           © 2020  Herbert Valerio Riedel
+license:             GPL-2.0-or-later
+license-file:        LICENSE.GPLv2
+
+synopsis: Efficient XOR masking
+description:
+  This package provides efficient implementations of routines for applying <https://en.wikipedia.org/wiki/Bitwise_operation#XOR bitwise XOR> masks to binary data.
+  .
+  The currently supported operations (see "Data.XOR" API documentation for more details) are applying
+  .
+  * 8-bit  wide XOR masks or
+  * 32-bit wide XOR masks
+  .
+  to binary data represented by
+  .
+  * Strict @ByteString@s,
+  * Lazy @ByteString@s,
+  * @ShortByteString@s (i.e. @ByteArray#@s), or
+  * @CStringLen@ (i.e. @Ptr@s).
+  .
+  The performance is comparable to portable ISO C99 implementations but this library is implemented as pure Haskell and is thereby compatible with compile targets such as <https://github.com/ghcjs/ghcjs GHCJS>.
+
+source-repository head
+  type:     git
+  location: https://github.com/hvr/xor.git
+
+common defaults
+  default-language:    Haskell2010
+  other-extensions:    CPP
+                       BangPatterns
+                       UnboxedTuples
+                       MagicHash
+
+  build-depends:
+    , base            >= 4.5 && < 4.15
+    , bytestring     ^>= 0.10.4
+    , ghc-byteorder  ^>= 4.11.0.0
+
+  ghc-options: -Wall
+
+library
+  import: defaults
+
+  hs-source-dirs: src
+  exposed-modules:
+    Data.XOR
+
+  other-modules:
+    Endianness
+
+benchmark bench
+  import: defaults
+
+  type: exitcode-stdio-1.0
+
+  hs-source-dirs:      src-bench
+  main-is:             Main.hs
+
+  build-depends:
+    , xor
+      -- dependencies specific to this component
+    , criterion ^>= 1.5.6
+
+test-suite test
+  import: defaults
+
+  type: exitcode-stdio-1.0
+
+  hs-source-dirs:      src-test
+  main-is:             Main.hs
+
+  build-depends:
+
+  build-depends:
+    , xor
+      -- dependencies specific to this component
+    , tasty            ^>= 1.2.3
+    , tasty-hunit      ^>= 0.10
+    , tasty-quickcheck ^>= 0.10
+    , QuickCheck       ^>= 2.14
