diff --git a/src/Tahoe/SDMF/Internal/Converting.hs b/src/Tahoe/SDMF/Internal/Converting.hs
new file mode 100644
index 0000000000000000000000000000000000000000..84672ac9ff13d03f308aab2d88d842f201774170
--- /dev/null
+++ b/src/Tahoe/SDMF/Internal/Converting.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+{- | Conversion between types with a known level of safety.  *Heavily* inspired
+ by `witch` (which has dependencies that make it hard for us to use just yet).
+-}
+module Tahoe.SDMF.Internal.Converting where
+
+import Data.Int (Int64)
+import Data.Word (Word16, Word32, Word64, Word8)
+
+-- | Precise, infallible conversion between two types.
+class From a b where
+    from :: a -> b
+
+-- | Precise, fallible conversion between two types.
+class TryFrom a b m where
+    tryFrom ::
+        -- | An error message for context if the conversion fails.
+        String ->
+        -- | The value to convert.
+        a ->
+        m b
+
+instance MonadFail m => TryFrom Int Word32 m where
+    tryFrom msg n
+        | n < 0 = fail msg
+        | n > maxWord32 = fail msg
+        | otherwise = pure $ fromIntegral n
+      where
+        maxWord32 = from @Word32 @Int maxBound
+
+instance MonadFail m => TryFrom Int Word64 m where
+    tryFrom msg n
+        | n < 0 = fail msg
+        | otherwise = pure $ fromIntegral n
+
+instance MonadFail m => TryFrom Int64 Word64 m where
+    tryFrom msg n
+        | n < 0 = fail msg
+        | otherwise = pure $ fromIntegral n
+
+instance From Word16 Int where
+    from = fromIntegral
+
+instance From Word8 Int where
+    from = fromIntegral
+
+instance From Word8 Word16 where
+    from = fromIntegral
+
+instance From Word32 Word64 where
+    from = fromIntegral
+
+instance From Word32 Int where
+    from = fromIntegral
+
+instance From Int64 Int where
+    from = fromIntegral
+
+instance From Int Int64 where
+    from = fromIntegral
+
+instance MonadFail m => TryFrom Word64 Int m where
+    tryFrom msg n
+        | n > maxInt = fail msg
+        | otherwise = pure $ fromIntegral n
+      where
+        maxInt = fromIntegral (maxBound :: Int) :: Word64
+
+instance MonadFail m => TryFrom Word16 Word8 m where
+    tryFrom msg n
+        | n > maxWord8 = fail msg
+        | otherwise = pure $ fromIntegral n
+      where
+        maxWord8 = from @Word8 @Word16 maxBound
+
+instance MonadFail m => TryFrom Word64 Int64 m where
+    tryFrom msg n
+        | n > maxInt64 = fail msg
+        | otherwise = pure $ fromIntegral n
+      where
+        maxInt64 = fromIntegral (maxBound :: Int64) :: Word64
+
+{- | Like `from` but with the order of the input/output type parameters
+ reversed.
+-}
+into :: forall b a. From a b => a -> b
+into = from
+
+{- | Like `tryFrom` but with the order of the input/output type parameters
+ reverse.
+-}
+tryInto :: forall b a m. TryFrom a b m => String -> a -> m b
+tryInto = tryFrom
diff --git a/tahoe-ssk.cabal b/tahoe-ssk.cabal
index 92b75c449fc018cad0623f023f7ebafa64d5f9bf..ba613c54768b953bbb1fe36fb25cb64520022d75 100644
--- a/tahoe-ssk.cabal
+++ b/tahoe-ssk.cabal
@@ -68,6 +68,7 @@ common language
     OverloadedStrings
     PackageImports
     RecordWildCards
+    TypeApplications
 
   default-language:   Haskell2010
 
@@ -78,6 +79,7 @@ library
   exposed-modules:
     Tahoe.SDMF
     Tahoe.SDMF.Internal.Capability
+    Tahoe.SDMF.Internal.Converting
     Tahoe.SDMF.Internal.Encoding
     Tahoe.SDMF.Internal.Encrypting
     Tahoe.SDMF.Internal.Keys