bech32

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

commit 6677da490cbe01466cf819e03e874485d0d1b4a8
parent 2db858792e9d818292d80b4fc19eaf6ab99dfb72
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri,  3 Jan 2025 12:03:04 -0330

lib: better structure

Diffstat:
Mlib/Data/ByteString/Base32.hs | 91++++---------------------------------------------------------------------------
Mlib/Data/ByteString/Bech32.hs | 12++++++------
Alib/Data/ByteString/Bech32/Internal.hs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Data/ByteString/Bech32m.hs | 12++++++------
Mppad-bech32.cabal | 1+
5 files changed, 127 insertions(+), 99 deletions(-)

diff --git a/lib/Data/ByteString/Base32.hs b/lib/Data/ByteString/Base32.hs @@ -9,15 +9,6 @@ module Data.ByteString.Base32 ( encode , decode - , as_word5 - , as_base32 - - -- XX put this in another module - -- not actually base32-related, but convenient to put here - , Encoding(..) - , create_checksum - , verify - , valid_hrp ) where import Control.Monad (guard) @@ -27,7 +18,6 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as BSB import qualified Data.ByteString.Builder.Extra as BE import qualified Data.ByteString.Unsafe as BU -import qualified Data.Primitive.PrimArray as PA import Data.Word (Word8, Word32, Word64) fi :: (Integral a, Num b) => a -> b @@ -83,7 +73,9 @@ arrange w32 w8 = {-# INLINE arrange #-} -- adapted from emilypi's 'base32' library -encode :: BS.ByteString -> BS.ByteString +encode + :: BS.ByteString -- ^ base256-encoded bytestring + -> BS.ByteString -- ^ base32-encoded bytestring encode dat = toStrict (go dat) where bech32_char = fi . BS.index bech32_charset . fi @@ -266,7 +258,7 @@ finalize bs l = BS.length bs word5 i = BS.elemIndex (fi (BU.unsafeIndex bs i)) bech32_charset --- length 8 guaranteed +-- assumes length 8 input decode_chunk :: BS.ByteString -> Maybe BSB.Builder decode_chunk bs = do let word5 i = BS.elemIndex (fi (BU.unsafeIndex bs i)) bech32_charset @@ -294,78 +286,3 @@ decode_chunk bs = do pure $ BSB.word32BE w32 <> BSB.word8 w8 --- XX move all of the below to another module - -_BECH32M_CONST :: Word32 -_BECH32M_CONST = 0x2bc830a3 - --- naive base32 -> word5 -as_word5 :: BS.ByteString -> BS.ByteString -as_word5 = BS.map f where - f b = case BS.elemIndex (fi b) bech32_charset of - Nothing -> error "ppad-bech32 (as_word5): input not bech32-encoded" - Just w -> fi w - --- naive word5 -> base32 -as_base32 :: BS.ByteString -> BS.ByteString -as_base32 = BS.map (BS.index bech32_charset . fi) - -polymod :: BS.ByteString -> Word32 -polymod = BS.foldl' alg 1 where - generator = PA.primArrayFromListN 5 - [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] - - alg !chk v = - let !b = chk `B.shiftR` 25 - c = (chk .&. 0x1ffffff) `B.shiftL` 5 `B.xor` fi v - in loop_gen 0 b c - - loop_gen i b !chk - | i > 4 = chk - | otherwise = - let sor | B.testBit (b `B.shiftR` i) 0 = - PA.indexPrimArray generator i - | otherwise = 0 - in loop_gen (succ i) b (chk `B.xor` sor) - -valid_hrp :: BS.ByteString -> Bool -valid_hrp hrp - | l == 0 || l > 83 = False - | otherwise = BS.all (\b -> (b > 32) && (b < 127)) hrp - where - l = BS.length hrp - -hrp_expand :: BS.ByteString -> BS.ByteString -hrp_expand bs = toStrict - $ BSB.byteString (BS.map (`B.shiftR` 5) bs) - <> BSB.word8 0 - <> BSB.byteString (BS.map (.&. 0b11111) bs) - -data Encoding = - Bech32 - | Bech32m - -create_checksum :: Encoding -> BS.ByteString -> BS.ByteString -> BS.ByteString -create_checksum enc hrp dat = - let pre = hrp_expand hrp <> dat - pay = toStrict $ - BSB.byteString pre - <> BSB.byteString "\NUL\NUL\NUL\NUL\NUL\NUL" - pm = polymod pay `B.xor` case enc of - Bech32 -> 1 - Bech32m -> _BECH32M_CONST - - code i = (fi (pm `B.shiftR` fi i) .&. 0b11111) - - in BS.map code "\EM\DC4\SI\n\ENQ\NUL" -- BS.pack [25, 20, 15, 10, 5, 0] - -verify :: Encoding -> BS.ByteString -> Bool -verify enc b32 = case BS.elemIndexEnd 0x31 b32 of - Nothing -> False - Just idx -> - let (hrp, BU.unsafeDrop 1 -> dat) = BS.splitAt idx b32 - bs = hrp_expand hrp <> as_word5 dat - in polymod bs == case enc of - Bech32 -> 1 - Bech32m -> _BECH32M_CONST - diff --git a/lib/Data/ByteString/Bech32.hs b/lib/Data/ByteString/Bech32.hs @@ -23,7 +23,7 @@ import Control.Monad (guard) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Base32 as B32 -import Data.ByteString.Base32 (Encoding(..)) +import qualified Data.ByteString.Bech32.Internal as BI import qualified Data.ByteString.Builder as BSB import qualified Data.ByteString.Builder.Extra as BE import qualified Data.Char as C (toLower) @@ -35,7 +35,7 @@ toStrict = BS.toStrict {-# INLINE toStrict #-} create_checksum :: BS.ByteString -> BS.ByteString -> BS.ByteString -create_checksum = B32.create_checksum Bech32 +create_checksum = BI.create_checksum BI.Bech32 -- | Encode a base256 human-readable part and input as bech32. -- @@ -47,13 +47,13 @@ encode -> BS.ByteString -- ^ base256-encoded data part -> Maybe BS.ByteString -- ^ bech32-encoded bytestring encode hrp (B32.encode -> dat) = do - guard (B32.valid_hrp hrp) - let check = create_checksum hrp (B32.as_word5 dat) + guard (BI.valid_hrp hrp) + let check = create_checksum hrp (BI.as_word5 dat) res = toStrict $ BSB.byteString (B8.map C.toLower hrp) <> BSB.word8 49 -- 1 <> BSB.byteString dat - <> BSB.byteString (B32.as_base32 check) + <> BSB.byteString (BI.as_base32 check) guard (BS.length res < 91) pure res @@ -66,5 +66,5 @@ encode hrp (B32.encode -> dat) = do verify :: BS.ByteString -- ^ bech32-encoded bytestring -> Bool -verify = B32.verify Bech32 +verify = BI.verify BI.Bech32 diff --git a/lib/Data/ByteString/Bech32/Internal.hs b/lib/Data/ByteString/Bech32/Internal.hs @@ -0,0 +1,110 @@ +{-# OPTIONS_HADDOCK hide, prune #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE BinaryLiterals #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ViewPatterns #-} + +module Data.ByteString.Bech32.Internal ( + as_word5 + , as_base32 + , Encoding(..) + , create_checksum + , verify + , valid_hrp + ) where + +import Data.Bits ((.&.)) +import qualified Data.Bits as B +import qualified Data.ByteString as BS +import qualified Data.ByteString.Builder as BSB +import qualified Data.ByteString.Builder.Extra as BE +import qualified Data.ByteString.Unsafe as BU +import qualified Data.Primitive.PrimArray as PA +import Data.Word (Word32) + +fi :: (Integral a, Num b) => a -> b +fi = fromIntegral +{-# INLINE fi #-} + +-- realization for small builders +toStrict :: BSB.Builder -> BS.ByteString +toStrict = BS.toStrict + . BE.toLazyByteStringWith (BE.safeStrategy 128 BE.smallChunkSize) mempty +{-# INLINE toStrict #-} + +_BECH32M_CONST :: Word32 +_BECH32M_CONST = 0x2bc830a3 + +bech32_charset :: BS.ByteString +bech32_charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +-- naive base32 -> word5 +as_word5 :: BS.ByteString -> BS.ByteString +as_word5 = BS.map f where + f b = case BS.elemIndex (fi b) bech32_charset of + Nothing -> error "ppad-bech32 (as_word5): input not bech32-encoded" + Just w -> fi w + +-- naive word5 -> base32 +as_base32 :: BS.ByteString -> BS.ByteString +as_base32 = BS.map (BS.index bech32_charset . fi) + +polymod :: BS.ByteString -> Word32 +polymod = BS.foldl' alg 1 where + generator = PA.primArrayFromListN 5 + [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + + alg !chk v = + let !b = chk `B.shiftR` 25 + c = (chk .&. 0x1ffffff) `B.shiftL` 5 `B.xor` fi v + in loop_gen 0 b c + + loop_gen i b !chk + | i > 4 = chk + | otherwise = + let sor | B.testBit (b `B.shiftR` i) 0 = + PA.indexPrimArray generator i + | otherwise = 0 + in loop_gen (succ i) b (chk `B.xor` sor) + +valid_hrp :: BS.ByteString -> Bool +valid_hrp hrp + | l == 0 || l > 83 = False + | otherwise = BS.all (\b -> (b > 32) && (b < 127)) hrp + where + l = BS.length hrp + +hrp_expand :: BS.ByteString -> BS.ByteString +hrp_expand bs = toStrict + $ BSB.byteString (BS.map (`B.shiftR` 5) bs) + <> BSB.word8 0 + <> BSB.byteString (BS.map (.&. 0b11111) bs) + +data Encoding = + Bech32 + | Bech32m + +create_checksum :: Encoding -> BS.ByteString -> BS.ByteString -> BS.ByteString +create_checksum enc hrp dat = + let pre = hrp_expand hrp <> dat + pay = toStrict $ + BSB.byteString pre + <> BSB.byteString "\NUL\NUL\NUL\NUL\NUL\NUL" + pm = polymod pay `B.xor` case enc of + Bech32 -> 1 + Bech32m -> _BECH32M_CONST + + code i = (fi (pm `B.shiftR` fi i) .&. 0b11111) + + in BS.map code "\EM\DC4\SI\n\ENQ\NUL" -- BS.pack [25, 20, 15, 10, 5, 0] + +verify :: Encoding -> BS.ByteString -> Bool +verify enc b32 = case BS.elemIndexEnd 0x31 b32 of + Nothing -> False + Just idx -> + let (hrp, BU.unsafeDrop 1 -> dat) = BS.splitAt idx b32 + bs = hrp_expand hrp <> as_word5 dat + in polymod bs == case enc of + Bech32 -> 1 + Bech32m -> _BECH32M_CONST + diff --git a/lib/Data/ByteString/Bech32m.hs b/lib/Data/ByteString/Bech32m.hs @@ -23,7 +23,7 @@ import Control.Monad (guard) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Base32 as B32 -import Data.ByteString.Base32 (Encoding(..)) +import qualified Data.ByteString.Bech32.Internal as BI import qualified Data.ByteString.Builder as BSB import qualified Data.ByteString.Builder.Extra as BE import qualified Data.Char as C (toLower) @@ -35,7 +35,7 @@ toStrict = BS.toStrict {-# INLINE toStrict #-} create_checksum :: BS.ByteString -> BS.ByteString -> BS.ByteString -create_checksum = B32.create_checksum Bech32m +create_checksum = BI.create_checksum BI.Bech32m -- | Encode a base256 human-readable part and input as bech32m. -- @@ -47,13 +47,13 @@ encode -> BS.ByteString -- ^ base256-encoded data part -> Maybe BS.ByteString -- ^ bech32m-encoded bytestring encode hrp (B32.encode -> dat) = do - guard (B32.valid_hrp hrp) - let check = create_checksum hrp (B32.as_word5 dat) + guard (BI.valid_hrp hrp) + let check = create_checksum hrp (BI.as_word5 dat) res = toStrict $ BSB.byteString (B8.map C.toLower hrp) <> BSB.word8 49 -- 1 <> BSB.byteString dat - <> BSB.byteString (B32.as_base32 check) + <> BSB.byteString (BI.as_base32 check) guard (BS.length res < 91) pure res @@ -66,5 +66,5 @@ encode hrp (B32.encode -> dat) = do verify :: BS.ByteString -- ^ bech32m-encoded bytestring -> Bool -verify = B32.verify Bech32m +verify = BI.verify BI.Bech32m diff --git a/ppad-bech32.cabal b/ppad-bech32.cabal @@ -25,6 +25,7 @@ library -Wall exposed-modules: Data.ByteString.Base32 + , Data.ByteString.Bech32.Internal , Data.ByteString.Bech32 , Data.ByteString.Bech32m build-depends: