diff --git a/FPE/FF1.hs b/FPE/FF1.hs
new file mode 100644
--- /dev/null
+++ b/FPE/FF1.hs
@@ -0,0 +1,92 @@
+module FPE.FF1 (encrypt, decrypt, BlockCipher, Crypter, Tweak) where
+
+import Data.Bits
+import Control.Arrow
+import Control.Monad
+import Data.Tuple (swap)
+import Math.NumberTheory.Logarithms
+import Data.Vector.Generic (Vector)
+import qualified Data.Vector.Generic as V
+import qualified Data.ByteString as S
+import qualified Data.ByteString.Lazy as L
+
+type BlockCipher = S.ByteString -> S.ByteString
+type Tweak = S.ByteString
+type Crypter v a = BlockCipher -> Int -> Tweak -> v a -> v a
+
+
+--  Number of bytes to store a message of given length and radix.
+--  Defined in FF1 step 3 using (redundant) double ceiling.
+bytesFor :: Int -> Int -> Int
+bytesFor radix len =
+   integerLog2 ((fromIntegral radix ^ len) - 1) `div` 8 + 1
+
+xorBytes :: S.ByteString -> S.ByteString -> S.ByteString
+xorBytes a b = S.pack $ S.zipWith xor a b
+
+--  Conversion functions.
+
+vecToNum :: (Vector v a, Integral a) => Int -> v a -> Integer
+vecToNum radix = V.foldl go 0 where
+   go val c = val * fromIntegral radix + fromIntegral c
+
+numToVec :: (Vector v a, Integral a) => Int -> Int -> Integer -> v a
+numToVec radix len num = V.reverse $ V.fromListN len $
+   map (fromIntegral . (`mod` radix_)) $ iterate (`div` radix_) num
+      where radix_ = fromIntegral radix
+
+--  Same as above, but with a ByteString of fixed radix.
+--  Possibly we could use Vector Word8 instead of ByteStrings?
+
+bytesToNum :: Integral a => S.ByteString -> a
+bytesToNum = S.foldl (\val c -> val * 256 + fromIntegral c) 0
+{-# SPECIALIZE bytesToNum :: S.ByteString -> Integer #-}
+
+numToBytes :: Integral a => Int -> a -> S.ByteString
+numToBytes len num = S.reverse $ S.pack $ map fromIntegral $
+   take len $ iterate (`div` 256) num
+
+--  Cipherish functions.
+
+prf :: BlockCipher -> S.ByteString -> S.ByteString
+prf cipher = loop (S.replicate 16 0) where
+   loop y src = if S.null rest then y' else loop y' rest where
+      (x, rest) = S.splitAt 16 src
+      y' = cipher $ x `xorBytes` y
+
+--  Extends (or shortens) a block to arbitrary length using secure hashing.
+extend :: BlockCipher -> Int -> S.ByteString -> S.ByteString
+extend cipher len blk = L.toStrict $ L.take (fromIntegral len) $ L.fromChunks $
+   blk : [ cipher $ blk `xorBytes` numToBytes @Int 16 i | i <- [1..] ]
+
+
+--  Encrypt and decrypt.
+
+--  True for encryption, False for decryption.
+crypt :: (Vector v a, Integral a) => Bool -> Crypter v a
+crypt isEncrypt cipher radix tweak msg =
+   numToVec radix u finalA V.++ numToVec radix v finalB where
+      t = S.length tweak
+      n = V.length msg; u = n `div` 2; v = n - u
+      b = bytesFor radix v
+      d = 4*((b-1)`div`4) + 8
+      rpow = (fromIntegral radix ^)
+      bP = S.concat [
+            S.pack [1, 2, 1], numToBytes 3 radix,
+            S.pack [10, fromIntegral u], numToBytes 4 n, numToBytes 4 t]
+      pfxQ = tweak <> S.pack (replicate ((-t-b-1)`mod`16) 0)
+      (numA0, numB0) = join (***) (vecToNum radix) $ V.splitAt u msg
+      loop (numA, numB) i = (numB, numC) where
+         y = bytesToNum $ extend cipher d $ prf cipher $ S.concat [
+               bP, pfxQ, S.singleton i, numToBytes b numB]
+         op = if isEncrypt then (+ y) else subtract y
+         numC = op numA `mod` (if even i then rpow u else rpow v)
+      wrap = if isEncrypt then id else swap
+      (finalA, finalB) = wrap $
+         foldl loop (wrap (numA0, numB0)) $
+         if isEncrypt then [0..9] else [9,8..0]
+
+encrypt, decrypt :: (Vector v a, Integral a) => Crypter v a
+encrypt = crypt True
+decrypt = crypt False
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-2019 Galen Huntington
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
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/ff1test.hs b/ff1test.hs
new file mode 100644
--- /dev/null
+++ b/ff1test.hs
@@ -0,0 +1,30 @@
+import FPE.FF1 as FF1
+import Crypto.Cipher.Types
+import Crypto.Cipher.AES
+import Crypto.Error
+import Control.Monad (when)
+
+import qualified Data.Vector.Unboxed as V
+import qualified Data.ByteString as B
+
+
+--  The first two samples from
+--  https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/FF1samples.pdf
+
+CryptoPassed (key :: AES128) = cipherInit $ B.pack [
+  0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
+  0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c]
+
+main = do
+   let cipher = ecbEncrypt key
+   let tweak = mempty
+   let plain = V.fromList @Int [0..9]
+   let crypt = FF1.encrypt cipher 10 tweak plain
+   print crypt
+   when (V.toList crypt /= [2,4,3,3,4,7,7,4,8,4]) $ error "bad encrypt"
+   when (FF1.decrypt cipher 10 tweak crypt /= plain) $ error "bad decrypt"
+   let tweak = B.pack [ 0x39, 0x38 .. 0x30]
+   let crypt = FF1.encrypt cipher 10 tweak plain
+   print crypt
+   when (V.toList crypt /= [6,1,2,4,2,0,0,7,7,3]) $ error "bad tweak encrypt"
+
diff --git a/fpe.cabal b/fpe.cabal
new file mode 100644
--- /dev/null
+++ b/fpe.cabal
@@ -0,0 +1,56 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.1.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 8fbbdfc71a8796c3cda5555879b81de5510d7c2662c55142ea49394a54df5cdb
+
+name:           fpe
+version:        0.1.0
+synopsis:       Format-preserving encryption.
+category:       Cryptography
+homepage:       https://github.com/galenhuntington/fpe#readme
+bug-reports:    https://github.com/galenhuntington/fpe/issues
+maintainer:     Galen Huntington
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+
+source-repository head
+  type: git
+  location: https://github.com/galenhuntington/fpe
+
+library
+  exposed-modules:
+      FPE.FF1
+  other-modules:
+      Paths_fpe
+  hs-source-dirs:
+      ./.
+  default-extensions: TypeApplications ScopedTypeVariables
+  ghc-options: -Wall -Wredundant-constraints -Wno-name-shadowing -Wno-missing-signatures -Wno-unused-do-bind -Wno-orphans -Wcompat
+  build-depends:
+      base >=4.9 && <5
+    , bytestring >=0.10 && <0.11
+    , integer-logarithms >=1 && <1.1
+    , vector >=0.12 && <0.13
+  default-language: Haskell2010
+
+test-suite suite
+  type: exitcode-stdio-1.0
+  main-is: ff1test.hs
+  other-modules:
+      FPE.FF1
+      Paths_fpe
+  hs-source-dirs:
+      ./.
+  default-extensions: TypeApplications ScopedTypeVariables
+  ghc-options: -Wall -Wredundant-constraints -Wno-name-shadowing -Wno-missing-signatures -Wno-unused-do-bind -Wno-orphans -Wcompat
+  build-depends:
+      base >=4.9 && <5
+    , bytestring >=0.10 && <0.11
+    , cryptonite
+    , integer-logarithms >=1 && <1.1
+    , vector >=0.12 && <0.13
+  default-language: Haskell2010
