bech32

Pure Haskell bech32, bech32m encodings (docs.ppad.tech/bech32).
git clone git://git.ppad.tech/bech32.git
Log | Files | Refs | README | LICENSE

Bech32.hs (5882B)


      1 {-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
      2 
      3 -- official BIP173 reference
      4 -- https://github.com/sipa/bech32/tree/master/ref/haskell
      5 module Reference.Bech32
      6   ( bech32Encode
      7   , bech32Decode
      8   , toBase32
      9   , toBase256
     10   , segwitEncode
     11   , segwitDecode
     12   , Word5()
     13   , word5
     14   , fromWord5
     15   ) where
     16 
     17 import Control.Monad (guard)
     18 import qualified Data.Array as Arr
     19 import Data.Bits (Bits, unsafeShiftL, unsafeShiftR, (.&.), (.|.), xor, testBit)
     20 import qualified Data.ByteString as BS
     21 import qualified Data.ByteString.Char8 as BSC
     22 import Data.Char (toLower, toUpper)
     23 import Data.Foldable (foldl')
     24 import Data.Functor.Identity (Identity, runIdentity)
     25 import Data.Ix (Ix(..))
     26 import Data.Word (Word8)
     27 
     28 type HRP = BS.ByteString
     29 type Data = [Word8]
     30 
     31 (.>>.), (.<<.) :: Bits a => a -> Int -> a
     32 (.>>.) = unsafeShiftR
     33 (.<<.) = unsafeShiftL
     34 
     35 newtype Word5 = UnsafeWord5 Word8
     36               deriving (Eq, Ord)
     37 
     38 instance Ix Word5 where
     39   range (UnsafeWord5 m, UnsafeWord5 n) = map UnsafeWord5 $ range (m, n)
     40   index (UnsafeWord5 m, UnsafeWord5 n) (UnsafeWord5 i) = index (m, n) i
     41   inRange (m,n) i = m <= i && i <= n
     42 
     43 word5 :: Integral a => a -> Word5
     44 word5 x = UnsafeWord5 ((fromIntegral x) .&. 31)
     45 {-# INLINE word5 #-}
     46 {-# SPECIALIZE INLINE word5 :: Word8 -> Word5 #-}
     47 
     48 fromWord5 :: Num a => Word5 -> a
     49 fromWord5 (UnsafeWord5 x) = fromIntegral x
     50 {-# INLINE fromWord5 #-}
     51 {-# SPECIALIZE INLINE fromWord5 :: Word5 -> Word8 #-}
     52 
     53 charset :: Arr.Array Word5 Char
     54 charset = Arr.listArray (UnsafeWord5 0, UnsafeWord5 31) "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
     55 
     56 charsetMap :: Char -> Maybe Word5
     57 charsetMap c | inRange (Arr.bounds inv) upperC = inv Arr.! upperC
     58              | otherwise = Nothing
     59   where
     60     upperC = toUpper c
     61     inv = Arr.listArray ('0', 'Z') (repeat Nothing) Arr.// (map swap (Arr.assocs charset))
     62     swap (a, b) = (toUpper b, Just a)
     63 
     64 bech32Polymod :: [Word5] -> Word
     65 bech32Polymod values = foldl' go 1 values .&. 0x3fffffff
     66   where
     67     go chk value = foldl' xor chk' [g | (g, i) <- zip generator [25..], testBit chk i]
     68       where
     69         generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
     70         chk' = chk .<<. 5 `xor` (fromWord5 value)
     71 
     72 bech32HRPExpand :: HRP -> [Word5]
     73 bech32HRPExpand hrp = map (UnsafeWord5 . (.>>. 5)) (BS.unpack hrp) ++ [UnsafeWord5 0] ++ map word5 (BS.unpack hrp)
     74 
     75 bech32CreateChecksum :: HRP -> [Word5] -> [Word5]
     76 bech32CreateChecksum hrp dat = [word5 (polymod .>>. i) | i <- [25,20..0]]
     77   where
     78     values = bech32HRPExpand hrp ++ dat
     79     polymod = bech32Polymod (values ++ map UnsafeWord5 [0, 0, 0, 0, 0, 0]) `xor` 1
     80 
     81 bech32VerifyChecksum :: HRP -> [Word5] -> Bool
     82 bech32VerifyChecksum hrp dat = bech32Polymod (bech32HRPExpand hrp ++ dat) == 1
     83 
     84 bech32Encode :: HRP -> [Word5] -> Maybe BS.ByteString
     85 bech32Encode hrp dat = do
     86     guard $ checkHRP hrp
     87     let dat' = dat ++ bech32CreateChecksum hrp dat
     88         rest = map (charset Arr.!) dat'
     89         result = BSC.concat [BSC.map toLower hrp, BSC.pack "1", BSC.pack rest]
     90     guard $ BS.length result <= 90
     91     return result
     92 
     93 checkHRP :: BS.ByteString -> Bool
     94 checkHRP hrp = not (BS.null hrp) && BS.all (\char -> char >= 33 && char <= 126) hrp
     95 
     96 bech32Decode :: BS.ByteString -> Maybe (HRP, [Word5])
     97 bech32Decode bech32 = do
     98     guard $ BS.length bech32 <= 90
     99     guard $ BSC.map toUpper bech32 == bech32 || BSC.map toLower bech32 == bech32
    100     let (hrp, dat) = BSC.breakEnd (== '1') $ BSC.map toLower bech32
    101     guard $ BS.length dat >= 6
    102     hrp' <- BSC.stripSuffix (BSC.pack "1") hrp
    103     guard $ checkHRP hrp'
    104     dat' <- mapM charsetMap $ BSC.unpack dat
    105     guard $ bech32VerifyChecksum hrp' dat'
    106     return (hrp', take (BS.length dat - 6) dat')
    107 
    108 type Pad f = Int -> Int -> Word -> [[Word]] -> f [[Word]]
    109 
    110 yesPadding :: Pad Identity
    111 yesPadding _ 0 _ result = return result
    112 yesPadding _ _ padValue result = return $ [padValue] : result
    113 {-# INLINE yesPadding #-}
    114 
    115 noPadding :: Pad Maybe
    116 noPadding frombits bits padValue result = do
    117     guard $ bits < frombits && padValue == 0
    118     return result
    119 {-# INLINE noPadding #-}
    120 
    121 -- Big endian conversion of a bytestring from base 2^frombits to base 2^tobits.
    122 -- frombits and twobits must be positive and 2^frombits and 2^tobits must be smaller than the size of Word.
    123 -- Every value in dat must be strictly smaller than 2^frombits.
    124 convertBits :: Functor f => [Word] -> Int -> Int -> Pad f -> f [Word]
    125 convertBits dat frombits tobits pad = fmap (concat . reverse) $ go dat 0 0 []
    126   where
    127     go [] acc bits result =
    128         let padValue = (acc .<<. (tobits - bits)) .&. maxv
    129         in pad frombits bits padValue result
    130     go (value:dat') acc bits result = go dat' acc' (bits' `rem` tobits) (result':result)
    131       where
    132         acc' = (acc .<<. frombits) .|. fromIntegral value
    133         bits' = bits + frombits
    134         result' = [(acc' .>>. b) .&. maxv | b <- [bits'-tobits,bits'-2*tobits..0]]
    135     maxv = (1 .<<. tobits) - 1
    136 {-# INLINE convertBits #-}
    137 
    138 toBase32 :: [Word8] -> [Word5]
    139 toBase32 dat = map word5 $ runIdentity $ convertBits (map fromIntegral dat) 8 5 yesPadding
    140 
    141 toBase256 :: [Word5] -> Maybe [Word8]
    142 toBase256 dat = fmap (map fromIntegral) $ convertBits (map fromWord5 dat) 5 8 noPadding
    143 
    144 segwitCheck :: Word8 -> Data -> Bool
    145 segwitCheck witver witprog =
    146     witver <= 16 &&
    147     if witver == 0
    148     then length witprog == 20 || length witprog == 32
    149     else length witprog >= 2 && length witprog <= 40
    150 
    151 segwitDecode :: HRP -> BS.ByteString -> Maybe (Word8, Data)
    152 segwitDecode hrp addr = do
    153     (hrp', dat) <- bech32Decode addr
    154     guard $ (hrp == hrp') && not (null dat)
    155     let (UnsafeWord5 witver : datBase32) = dat
    156     decoded <- toBase256 datBase32
    157     guard $ segwitCheck witver decoded
    158     return (witver, decoded)
    159 
    160 segwitEncode :: HRP -> Word8 -> Data -> Maybe BS.ByteString
    161 segwitEncode hrp witver witprog = do
    162     guard $ segwitCheck witver witprog
    163     bech32Encode hrp $ UnsafeWord5 witver : toBase32 witprog