diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2015, David Luposchainsky
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of David Luposchainsky nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+[![Build Status](https://secure.travis-ci.org/quchen/pgp-wordlist.png?branch=master)](http://travis-ci.org/quchen/pgp-wordlist)
+
+Translate between binary data and a human-readable collection of words.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/pgp-wordlist.cabal b/pgp-wordlist.cabal
new file mode 100644
--- /dev/null
+++ b/pgp-wordlist.cabal
@@ -0,0 +1,70 @@
+-- Initial pgp-wordlist.cabal generated by cabal init.  For further
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                pgp-wordlist
+version:             0.1
+synopsis:            Translate between binary data and a human-readable
+                     collection of words.
+description:         Translate between binary data and a human-readable
+                     collection of words.
+                     .
+                     The PGP Word List consists of two phonetic alphabets, each
+                     with one word per possible byte value. A string of bytes
+                     is translated with these alphabets, alternating between
+                     them at each byte.
+                     .
+                     The PGP words corresponding to the bytes @5B 1D CA 6E@
+                     are "erase breakaway spellbind headwaters", for example.
+                     .
+                     For further information, see
+                     <http://en.wikipedia.org/wiki/PGP_word_list Wikipedia>.
+license:             BSD3
+license-file:        LICENSE
+author:              David Luposchainsky <dluposchainsky(λ)gmail.com>
+maintainer:          David Luposchainsky <dluposchainsky(λ)gmail.com>
+copyright:           David Luposchainsky <dluposchainsky(λ)gmail.com>
+homepage:            https://github.com/quchen/pgp-wordlist
+bug-reports:         https://github.com/quchen/pgp-wordlist/issues
+category:            Data, Serialization
+build-type:          Simple
+cabal-version:       >=1.10
+extra-source-files:  README.md
+tested-with:         GHC == 7.6.3
+                   , GHC == 7.8.4
+                   , GHC == 7.10.1
+
+
+
+source-repository head
+  type:     git
+  location: https://github.com/quchen/pgp-wordlist
+
+
+
+library
+    exposed-modules:     Data.Text.PgpWordlist
+                       , Data.Text.PgpWordlist.Internal.AltList
+                       , Data.Text.PgpWordlist.Internal.Tables
+    build-depends:       base >=4.6 && < 5
+                       , bimap >=0.3 && <0.4
+                       , bytestring >=0.10 && <0.11
+                       , text >=1.2 && <1.3
+    hs-source-dirs:      src
+    ghc-options:         -Wall
+    default-language:    Haskell2010
+
+
+
+test-suite tasty
+    main-is: test/Main.hs
+    ghc-options: -Wall
+    type: exitcode-stdio-1.0
+    default-language: Haskell2010
+    build-depends: pgp-wordlist
+                 , base
+                 , bytestring
+                 , HUnit
+                 , tasty >= 0.10
+                 , tasty-hunit >= 0.9
+                 , tasty-quickcheck
+                 , text
diff --git a/src/Data/Text/PgpWordlist.hs b/src/Data/Text/PgpWordlist.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/PgpWordlist.hs
@@ -0,0 +1,8 @@
+-- | Translate between binary data and a human-readable collection of words.
+module Data.Text.PgpWordlist (
+      toText
+    , fromText
+    , TranslationError(..)
+) where
+
+import Data.Text.PgpWordlist.Internal.Tables
diff --git a/src/Data/Text/PgpWordlist/Internal/AltList.hs b/src/Data/Text/PgpWordlist/Internal/AltList.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/PgpWordlist/Internal/AltList.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE CPP #-}
+
+-- | Defines a type of list that containins two alternating element types.
+module Data.Text.PgpWordlist.Internal.AltList (
+      AltList(..)
+
+    -- * Construction, deconstruction
+    , fromList
+    , toList
+
+    -- * Bifunctor
+    , first
+    , second
+    , bimap
+
+    -- * Bitraversable
+    , bisequence
+    , bitraverse
+) where
+
+
+
+#if __GLASGOW_HASKELL__ < 710
+import Control.Applicative
+#endif
+
+
+
+-- | List of elements of alternating element types.
+data AltList a b = Nil | Cons a (AltList b a)
+    deriving (Eq, Ord, Show)
+
+
+
+-- | Simple conversion function. Inverse of 'toList'.
+fromList :: [a] -> AltList a a
+fromList = foldr Cons Nil
+
+-- | Simple conversion function. Inverse of 'fromList'.
+toList :: AltList a a -> [a]
+toList Nil = []
+toList (Cons x xs) = x : toList xs
+
+
+
+-- | Map over the first component of an 'AltList'.
+--
+-- @
+-- 'first' f ≡ 'bimap' f 'id'
+-- @
+first :: (a -> a') -> AltList a b -> AltList a' b
+first _ Nil = Nil
+first f (Cons x Nil) = Cons (f x) Nil
+first f (Cons x (Cons y ys)) = Cons (f x) (Cons y (first f ys))
+
+
+
+-- | Map over the second component of an 'AltList'.
+--
+-- @
+-- 'second' g ≡ 'bimap' 'id' g
+-- @
+second :: (b -> b') -> AltList a b -> AltList a b'
+second _ Nil = Nil
+second _ (Cons x Nil) = Cons x Nil
+second g (Cons x (Cons y ys)) = Cons x (Cons (g y) (second g ys))
+
+
+
+-- | Map over the both components of an 'AltList'.
+--
+-- @
+-- 'bimap' f g ≡ 'second' g . 'first' f
+-- @
+bimap :: (a -> a') -> (b -> b') -> AltList a b -> AltList a' b'
+bimap _ _ Nil = Nil
+bimap f g (Cons x xs) = Cons (f x) (bimap g f xs)
+
+
+
+-- | Monomorphic bisequence.
+bisequence :: Applicative f => AltList (f a) (f b) -> f (AltList a b)
+bisequence Nil = pure Nil
+bisequence (Cons x xs) = Cons <$> x <*> bisequence xs
+
+-- | Monomorphic bitraverse.
+bitraverse :: Applicative f => (a -> f c) -> (b -> f d) -> AltList a b -> f (AltList c d)
+bitraverse _ _ Nil = pure Nil
+bitraverse f g (Cons x xs) = Cons <$> f x <*> bitraverse g f xs
diff --git a/src/Data/Text/PgpWordlist/Internal/Tables.hs b/src/Data/Text/PgpWordlist/Internal/Tables.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/PgpWordlist/Internal/Tables.hs
@@ -0,0 +1,367 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Core functionality for conversion between binary formats and PGP word
+--   lists.
+module Data.Text.PgpWordlist.Internal.Tables where
+
+
+
+import           Data.Text.PgpWordlist.Internal.AltList (AltList)
+import qualified Data.Text.PgpWordlist.Internal.AltList as Alt
+
+import           Data.Bimap                             (Bimap, (!))
+import qualified Data.Bimap                             as BM
+
+import qualified Data.ByteString.Lazy                   as BSL
+import           Data.Text                              (Text)
+import qualified Data.Text                              as T
+import           Data.Word
+
+
+
+-- | Abstract representation of a PGP word list.
+newtype PgpWordlist = PgpWordlist (AltList EvenWord OddWord)
+    deriving (Eq, Ord, Show)
+
+
+
+-- | Possible translation errors from a list of PGP words to binary format.
+data TranslationError =
+      BadWord Text         -- ^ Word is not recognized
+    | BadParity Text Word8 -- ^ Word is recognized, but from the wrong
+                           --   alphabet. Duplicates, omissions, and neighbour
+                           --   transpositions are often cause for this.
+    deriving (Eq, Ord, Show)
+
+
+
+-- | Inverse of 'fromText', modulo whitespace count.
+toText :: BSL.ByteString -> Text
+toText = T.intercalate " "
+       . Alt.toList
+       . Alt.bimap (unEvenWord . toEvenWord) (unOddWord . toOddWord)
+       . Alt.fromList
+       . BSL.unpack
+
+
+
+-- | Convert a text of whitespace-separated words to their binary
+--   representation. The whitespace splitting behaviour is given by 'T.words'.
+fromText :: Text -> Either TranslationError BSL.ByteString
+fromText = fmap (BSL.pack . Alt.toList)
+         . Alt.bitraverse fromEvenWord fromOddWord
+         . Alt.fromList
+         . T.words
+
+
+
+-- | Word that is supposed to occur in an even position
+newtype EvenWord = EvenWord { unEvenWord :: Text }
+    deriving (Eq, Ord, Show)
+
+-- | Look up the word corresponding to a byte.
+toEvenWord :: Word8 -> EvenWord
+toEvenWord = (evenMap !) -- evenMap is total, so the lookup is safe
+
+-- | Simple conversion, taking into account invalid words.
+fromEvenWord :: Text -> Either TranslationError Word8
+fromEvenWord word = case BM.lookupR (EvenWord word) evenMap of
+    Just i  -> Right i
+    Nothing -> Left (case BM.lookupR (OddWord word) oddMap of
+        Just j  -> BadParity word j
+        Nothing -> BadWord word)
+
+-- | Mapping from and to 'EvenWord's
+evenMap :: Bimap Word8 EvenWord
+evenMap = BM.fromList (map pick12 wordList)
+  where
+    pick12 :: (a,b,c) -> (a,b)
+    pick12 (i,e,_) = (i,e)
+
+
+
+-- | Word that is supposed to occur in an odd position
+newtype OddWord = OddWord { unOddWord :: Text }
+    deriving (Eq, Ord, Show)
+
+-- | Look up the word corresponding to a byte.
+toOddWord :: Word8 -> OddWord
+toOddWord = (oddMap !) -- oddMap is total, so the lookup is safe
+
+-- | Simple conversion, taking into account invalid words.
+fromOddWord :: Text -> Either TranslationError Word8
+fromOddWord word = case BM.lookupR (OddWord word) oddMap of
+    Just i  -> Right i
+    Nothing -> Left (case BM.lookupR (EvenWord word) evenMap of
+        Just j  -> BadParity word j
+        Nothing -> BadWord word)
+
+-- | Mapping from and to 'OddWord's
+oddMap :: Bimap Word8 OddWord
+oddMap = BM.fromList (map pick13 wordList)
+  where
+    pick13 :: (a,b,c) -> (a,c)
+    pick13 (i,_,o) = (i,o)
+
+
+
+wordList :: [(Word8, EvenWord, OddWord)]
+wordList = map (\(i,e,o) -> (i, EvenWord e, OddWord o)) table
+  where
+    table = [ (0x00, "aardvark" , "adroitness" )
+            , (0x01, "absurd"   , "adviser"    )
+            , (0x02, "accrue"   , "aftermath"  )
+            , (0x03, "acme"     , "aggregate"  )
+            , (0x04, "adrift"   , "alkali"     )
+            , (0x05, "adult"    , "almighty"   )
+            , (0x06, "afflict"  , "amulet"     )
+            , (0x07, "ahead"    , "amusement"  )
+            , (0x08, "aimless"  , "antenna"    )
+            , (0x09, "algol"    , "applicant"  )
+            , (0x0a, "allow"    , "apollo"     )
+            , (0x0b, "alone"    , "armistice"  )
+            , (0x0c, "ammo"     , "article"    )
+            , (0x0d, "ancient"  , "asteroid"   )
+            , (0x0e, "apple"    , "atlantic"   )
+            , (0x0f, "artist"   , "atmosphere" )
+            , (0x10, "assume"   , "autopsy"    )
+            , (0x11, "athens"   , "babylon"    )
+            , (0x12, "atlas"    , "backwater"  )
+            , (0x13, "aztec"    , "barbecue"   )
+            , (0x14, "baboon"   , "belowground")
+            , (0x15, "backfield", "bifocals"   )
+            , (0x16, "backward" , "bodyguard"  )
+            , (0x17, "banjo"    , "bookseller" )
+            , (0x18, "beaming"  , "borderline" )
+            , (0x19, "bedlamp"  , "bottomless" )
+            , (0x1a, "beehive"  , "bradbury"   )
+            , (0x1b, "beeswax"  , "bravado"    )
+            , (0x1c, "befriend" , "brazilian"  )
+            , (0x1d, "belfast"  , "breakaway"  )
+            , (0x1e, "berserk"  , "burlington" )
+            , (0x1f, "billiard" , "businessman")
+            , (0x20, "bison"    , "butterfat"  )
+            , (0x21, "blackjack", "camelot"    )
+            , (0x22, "blockade" , "candidate"  )
+            , (0x23, "blowtorch", "cannonball" )
+            , (0x24, "bluebird" , "capricorn"  )
+            , (0x25, "bombast"  , "caravan"    )
+            , (0x26, "bookshelf", "caretaker"  )
+            , (0x27, "brackish" , "celebrate"  )
+            , (0x28, "breadline", "cellulose"  )
+            , (0x29, "breakup"  , "certify"    )
+            , (0x2a, "brickyard", "chambermaid")
+            , (0x2b, "briefcase", "cherokee"   )
+            , (0x2c, "burbank"  , "chicago"    )
+            , (0x2d, "button"   , "clergyman"  )
+            , (0x2e, "buzzard"  , "coherence"  )
+            , (0x2f, "cement"   , "combustion" )
+            , (0x30, "chairlift", "commando"   )
+            , (0x31, "chatter"  , "company"    )
+            , (0x32, "checkup"  , "component"  )
+            , (0x33, "chisel"   , "concurrent" )
+            , (0x34, "choking"  , "confidence" )
+            , (0x35, "chopper"  , "conformist" )
+            , (0x36, "christmas", "congregate" )
+            , (0x37, "clamshell", "consensus"  )
+            , (0x38, "classic"  , "consulting" )
+            , (0x39, "classroom", "corporate"  )
+            , (0x3a, "cleanup"  , "corrosion"  )
+            , (0x3b, "clockwork", "councilman" )
+            , (0x3c, "cobra"    , "crossover"  )
+            , (0x3d, "commence" , "crucifix"   )
+            , (0x3e, "concert"  , "cumbersome" )
+            , (0x3f, "cowbell"  , "customer"   )
+            , (0x40, "crackdown", "dakota"     )
+            , (0x41, "cranky"   , "decadence"  )
+            , (0x42, "crowfoot" , "december"   )
+            , (0x43, "crucial"  , "decimal"    )
+            , (0x44, "crumpled" , "designing"  )
+            , (0x45, "crusade"  , "detector"   )
+            , (0x46, "cubic"    , "detergent"  )
+            , (0x47, "dashboard", "determine"  )
+            , (0x48, "deadbolt" , "dictator"   )
+            , (0x49, "deckhand" , "dinosaur"   )
+            , (0x4a, "dogsled"  , "direction"  )
+            , (0x4b, "dragnet"  , "disable"    )
+            , (0x4c, "drainage" , "disbelief"  )
+            , (0x4d, "dreadful" , "disruptive" )
+            , (0x4e, "drifter"  , "distortion" )
+            , (0x4f, "dropper"  , "document"   )
+            , (0x50, "drumbeat" , "embezzle"   )
+            , (0x51, "drunken"  , "enchanting" )
+            , (0x52, "dupont"   , "enrollment" )
+            , (0x53, "dwelling" , "enterprise" )
+            , (0x54, "eating"   , "equation"   )
+            , (0x55, "edict"    , "equipment"  )
+            , (0x56, "egghead"  , "escapade"   )
+            , (0x57, "eightball", "eskimo"     )
+            , (0x58, "endorse"  , "everyday"   )
+            , (0x59, "endow"    , "examine"    )
+            , (0x5a, "enlist"   , "existence"  )
+            , (0x5b, "erase"    , "exodus"     )
+            , (0x5c, "escape"   , "fascinate"  )
+            , (0x5d, "exceed"   , "filament"   )
+            , (0x5e, "eyeglass" , "finicky"    )
+            , (0x5f, "eyetooth" , "forever"    )
+            , (0x60, "facial"   , "fortitude"  )
+            , (0x61, "fallout"  , "frequency"  )
+            , (0x62, "flagpole" , "gadgetry"   )
+            , (0x63, "flatfoot" , "galveston"  )
+            , (0x64, "flytrap"  , "getaway"    )
+            , (0x65, "fracture" , "glossary"   )
+            , (0x66, "framework", "gossamer"   )
+            , (0x67, "freedom"  , "graduate"   )
+            , (0x68, "frighten" , "gravity"    )
+            , (0x69, "gazelle"  , "guitarist"  )
+            , (0x6a, "geiger"   , "hamburger"  )
+            , (0x6b, "glitter"  , "hamilton"   )
+            , (0x6c, "glucose"  , "handiwork"  )
+            , (0x6d, "goggles"  , "hazardous"  )
+            , (0x6e, "goldfish" , "headwaters" )
+            , (0x6f, "gremlin"  , "hemisphere" )
+            , (0x70, "guidance" , "hesitate"   )
+            , (0x71, "hamlet"   , "hideaway"   )
+            , (0x72, "highchair", "holiness"   )
+            , (0x73, "hockey"   , "hurricane"  )
+            , (0x74, "indoors"  , "hydraulic"  )
+            , (0x75, "indulge"  , "impartial"  )
+            , (0x76, "inverse"  , "impetus"    )
+            , (0x77, "involve"  , "inception"  )
+            , (0x78, "island"   , "indigo"     )
+            , (0x79, "jawbone"  , "inertia"    )
+            , (0x7a, "keyboard" , "infancy"    )
+            , (0x7b, "kickoff"  , "inferno"    )
+            , (0x7c, "kiwi"     , "informant"  )
+            , (0x7d, "klaxon"   , "insincere"  )
+            , (0x7e, "locale"   , "insurgent"  )
+            , (0x7f, "lockup"   , "integrate"  )
+            , (0x80, "merit"    , "intention"  )
+            , (0x81, "minnow"   , "inventive"  )
+            , (0x82, "miser"    , "istanbul"   )
+            , (0x83, "mohawk"   , "jamaica"    )
+            , (0x84, "mural"    , "jupiter"    )
+            , (0x85, "music"    , "leprosy"    )
+            , (0x86, "necklace" , "letterhead" )
+            , (0x87, "neptune"  , "liberty"    )
+            , (0x88, "newborn"  , "maritime"   )
+            , (0x89, "nightbird", "matchmaker" )
+            , (0x8a, "oakland"  , "maverick"   )
+            , (0x8b, "obtuse"   , "medusa"     )
+            , (0x8c, "offload"  , "megaton"    )
+            , (0x8d, "optic"    , "microscope" )
+            , (0x8e, "orca"     , "microwave"  )
+            , (0x8f, "payday"   , "midsummer"  )
+            , (0x90, "peachy"   , "millionaire")
+            , (0x91, "pheasant" , "miracle"    )
+            , (0x92, "physique" , "misnomer"   )
+            , (0x93, "playhouse", "molasses"   )
+            , (0x94, "pluto"    , "molecule"   )
+            , (0x95, "preclude" , "montana"    )
+            , (0x96, "prefer"   , "monument"   )
+            , (0x97, "preshrunk", "mosquito"   )
+            , (0x98, "printer"  , "narrative"  )
+            , (0x99, "prowler"  , "nebula"     )
+            , (0x9a, "pupil"    , "newsletter" )
+            , (0x9b, "puppy"    , "norwegian"  )
+            , (0x9c, "python"   , "october"    )
+            , (0x9d, "quadrant" , "ohio"       )
+            , (0x9e, "quiver"   , "onlooker"   )
+            , (0x9f, "quota"    , "opulent"    )
+            , (0xa0, "ragtime"  , "orlando"    )
+            , (0xa1, "ratchet"  , "outfielder" )
+            , (0xa2, "rebirth"  , "pacific"    )
+            , (0xa3, "reform"   , "pandemic"   )
+            , (0xa4, "regain"   , "pandora"    )
+            , (0xa5, "reindeer" , "paperweight")
+            , (0xa6, "rematch"  , "paragon"    )
+            , (0xa7, "repay"    , "paragraph"  )
+            , (0xa8, "retouch"  , "paramount"  )
+            , (0xa9, "revenge"  , "passenger"  )
+            , (0xaa, "reward"   , "pedigree"   )
+            , (0xab, "rhythm"   , "pegasus"    )
+            , (0xac, "ribcage"  , "penetrate"  )
+            , (0xad, "ringbolt" , "perceptive" )
+            , (0xae, "robust"   , "performance")
+            , (0xaf, "rocker"   , "pharmacy"   )
+            , (0xb0, "ruffled"  , "phonetic"   )
+            , (0xb1, "sailboat" , "photograph" )
+            , (0xb2, "sawdust"  , "pioneer"    )
+            , (0xb3, "scallion" , "pocketful"  )
+            , (0xb4, "scenic"   , "politeness" )
+            , (0xb5, "scorecard", "positive"   )
+            , (0xb6, "scotland" , "potato"     )
+            , (0xb7, "seabird"  , "processor"  )
+            , (0xb8, "select"   , "provincial" )
+            , (0xb9, "sentence" , "proximate"  )
+            , (0xba, "shadow"   , "puberty"    )
+            , (0xbb, "shamrock" , "publisher"  )
+            , (0xbc, "showgirl" , "pyramid"    )
+            , (0xbd, "skullcap" , "quantity"   )
+            , (0xbe, "skydive"  , "racketeer"  )
+            , (0xbf, "slingshot", "rebellion"  )
+            , (0xc0, "slowdown" , "recipe"     )
+            , (0xc1, "snapline" , "recover"    )
+            , (0xc2, "snapshot" , "repellent"  )
+            , (0xc3, "snowcap"  , "replica"    )
+            , (0xc4, "snowslide", "reproduce"  )
+            , (0xc5, "solo"     , "resistor"   )
+            , (0xc6, "southward", "responsive" )
+            , (0xc7, "soybean"  , "retraction" )
+            , (0xc8, "spaniel"  , "retrieval"  )
+            , (0xc9, "spearhead", "retrospect" )
+            , (0xca, "spellbind", "revenue"    )
+            , (0xcb, "spheroid" , "revival"    )
+            , (0xcc, "spigot"   , "revolver"   )
+            , (0xcd, "spindle"  , "sandalwood" )
+            , (0xce, "spyglass" , "sardonic"   )
+            , (0xcf, "stagehand", "saturday"   )
+            , (0xd0, "stagnate" , "savagery"   )
+            , (0xd1, "stairway" , "scavenger"  )
+            , (0xd2, "standard" , "sensation"  )
+            , (0xd3, "stapler"  , "sociable"   )
+            , (0xd4, "steamship", "souvenir"   )
+            , (0xd5, "sterling" , "specialist" )
+            , (0xd6, "stockman" , "speculate"  )
+            , (0xd7, "stopwatch", "stethoscope")
+            , (0xd8, "stormy"   , "stupendous" )
+            , (0xd9, "sugar"    , "supportive" )
+            , (0xda, "surmount" , "surrender"  )
+            , (0xdb, "suspense" , "suspicious" )
+            , (0xdc, "sweatband", "sympathy"   )
+            , (0xdd, "swelter"  , "tambourine" )
+            , (0xde, "tactics"  , "telephone"  )
+            , (0xdf, "talon"    , "therapist"  )
+            , (0xe0, "tapeworm" , "tobacco"    )
+            , (0xe1, "tempest"  , "tolerance"  )
+            , (0xe2, "tiger"    , "tomorrow"   )
+            , (0xe3, "tissue"   , "torpedo"    )
+            , (0xe4, "tonic"    , "tradition"  )
+            , (0xe5, "topmost"  , "travesty"   )
+            , (0xe6, "tracker"  , "trombonist" )
+            , (0xe7, "transit"  , "truncated"  )
+            , (0xe8, "trauma"   , "typewriter" )
+            , (0xe9, "treadmill", "ultimate"   )
+            , (0xea, "trojan"   , "undaunted"  )
+            , (0xeb, "trouble"  , "underfoot"  )
+            , (0xec, "tumor"    , "unicorn"    )
+            , (0xed, "tunnel"   , "unify"      )
+            , (0xee, "tycoon"   , "universe"   )
+            , (0xef, "uncut"    , "unravel"    )
+            , (0xf0, "unearth"  , "upcoming"   )
+            , (0xf1, "unwind"   , "vacancy"    )
+            , (0xf2, "uproot"   , "vagabond"   )
+            , (0xf3, "upset"    , "vertigo"    )
+            , (0xf4, "upshot"   , "virginia"   )
+            , (0xf5, "vapor"    , "visitor"    )
+            , (0xf6, "village"  , "vocalist"   )
+            , (0xf7, "virus"    , "voyager"    )
+            , (0xf8, "vulcan"   , "warranty"   )
+            , (0xf9, "waffle"   , "waterloo"   )
+            , (0xfa, "wallet"   , "whimsical"  )
+            , (0xfb, "watchword", "wichita"    )
+            , (0xfc, "wayside"  , "wilmington" )
+            , (0xfd, "willow"   , "wyoming"    )
+            , (0xfe, "woodlark" , "yesteryear" )
+            , (0xff, "zulu"     , "yucatan"    )
+            ]
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,119 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ParallelListComp #-}
+
+module Main where
+
+
+
+import           Data.Text.PgpWordlist.Internal.Tables
+
+import qualified Data.ByteString.Lazy                  as BSL
+import           Test.Tasty
+import qualified Test.Tasty.HUnit                      as HU
+import qualified Test.Tasty.QuickCheck                 as QC
+import           Text.Printf
+
+
+
+main :: IO ()
+main = defaultMain tree
+
+
+
+tree :: TestTree
+tree = testGroup "Tests"
+    [ allBytesConvertible
+    , examplesBackAndForth
+    , randomRoundtrips
+    ]
+
+
+
+allBytesConvertible :: TestTree
+allBytesConvertible = testGroup "All bytes have a corresponding word" tests
+  where
+    tests = map evenTest [0..] ++ map oddTest [0..]
+    evenTest i = HU.testCase (printf "%02x is convertible to an even word" i)
+                             (evenTestAssertion i)
+    oddTest  i = HU.testCase (printf "%02x is convertible to an odd word" i)
+                             (oddTestAssertion i)
+    evenTestAssertion i = HU.assertBool "" (toEvenWord i `seq` True)
+    oddTestAssertion  i = HU.assertBool "" (toOddWord  i `seq` True)
+
+
+
+examplesBackAndForth :: TestTree
+examplesBackAndForth = testGroup "Conversion beween various examples" tests
+  where
+    tests = concat
+        [ [ HU.testCase (printf "#%2d: %s -> PGP words" i (bsToHex bytes))
+                        (HU.assertEqual "" (toText bytes) pgpWords)
+          , HU.testCase (printf "#%2d: PGP words -> %s" i (bsToHex bytes))
+                        (HU.assertEqual "" (Right bytes) (fromText pgpWords))
+          ]
+        | (bytes, pgpWords) <- testCases
+        | i <- [0..] :: [Int]
+        ]
+    bsToHex :: BSL.ByteString -> String
+    bsToHex = concatMap (printf "%02x") . BSL.unpack
+
+    testCases = map (\(x,y) -> (BSL.pack x, y))
+        [ ( [0xce, 0xe7, 0x1c, 0xbf, 0xe6, 0x59, 0x49, 0x48]
+          , "spyglass truncated befriend rebellion tracker examine deckhand dictator"
+          )
+        , ( [0x06, 0x7a, 0xd2, 0xfc, 0xb7, 0x74, 0x48, 0x18]
+          , "afflict infancy standard wilmington seabird hydraulic deadbolt borderline"
+          )
+        , ( [0xb3, 0xc6, 0x75, 0x64, 0xa7, 0x9e, 0x0a, 0xbb]
+          , "scallion responsive indulge getaway repay onlooker allow publisher"
+          )
+        , ( [0x84, 0x98, 0xd3, 0x33, 0xf1, 0xe4, 0x2a, 0x92]
+          , "mural narrative stapler concurrent unwind tradition brickyard misnomer"
+          )
+        , ( [0xb2, 0x8d, 0x47, 0xf8, 0x8c, 0x57, 0xb5, 0x09]
+          , "sawdust microscope dashboard warranty offload eskimo scorecard applicant"
+          )
+        , ( [0x2e, 0x26, 0xf1, 0x24, 0x65, 0xb9, 0xae, 0xd7]
+          , "buzzard caretaker unwind capricorn fracture proximate robust stethoscope"
+          )
+        , ( [0x72, 0x19, 0x07, 0xf2, 0xca, 0x6f, 0xe4, 0xee]
+          , "highchair bottomless ahead vagabond spellbind hemisphere tonic universe"
+          )
+        , ( [0x52, 0x8f, 0x47, 0x3a, 0xbd, 0xf3, 0x64, 0x5b]
+          , "dupont midsummer dashboard corrosion skullcap vertigo flytrap exodus"
+          )
+        , ( [0xe9, 0x75, 0x73, 0x8c, 0x03, 0xc1, 0x49, 0x7f]
+          , "treadmill impartial hockey megaton acme recover deckhand integrate"
+          )
+        , ( [0xfd, 0xce, 0xd3, 0x8f, 0xfa, 0x3d, 0xd8, 0xf2]
+          , "willow sardonic stapler midsummer wallet crucifix stormy vagabond"
+          )
+        , ( [0x2d, 0x56, 0x8c, 0x11, 0x95, 0x47, 0x4f, 0x6f]
+          , "button escapade offload babylon preclude determine dropper hemisphere"
+          )
+        , ( [0xe4, 0x05, 0x46, 0xfe, 0xc9, 0xf4, 0x41, 0x9a]
+          , "tonic almighty cubic yesteryear spearhead virginia cranky newsletter"
+          )
+        , ( [0xb2, 0x68, 0xfc, 0xe4, 0x2c, 0xf5, 0xe5, 0x04]
+          , "sawdust gravity wayside tradition burbank visitor topmost alkali"
+          )
+        , ( [0xb4, 0xfb, 0xec, 0xe3, 0xc8, 0xe4, 0x29, 0xa5]
+          , "scenic wichita tumor torpedo spaniel tradition breakup paperweight"
+          )
+        , ( [0xbb, 0x96, 0x68, 0xc6, 0x33, 0x11, 0x4f, 0x7c]
+          , "shamrock monument frighten responsive chisel babylon dropper informant"
+          )
+        , ( [0x51, 0x5e, 0x7f, 0xe3, 0x1b, 0xc0, 0x6f, 0xc7]
+          , "drunken finicky lockup torpedo beeswax recipe gremlin retraction"
+          )
+        ]
+
+randomRoundtrips :: TestTree
+randomRoundtrips = makeGroup tests
+  where
+    makeGroup = localOption (QC.QuickCheckMaxSize 1024)
+              . testGroup "Random roundtrips"
+    tests = [ QC.testProperty "Bytes -> PGP words -> Bytes" $
+                \bytes -> let bs = BSL.pack bytes
+                          in  fromText (toText bs) == Right bs
+            ]
