natskell-1.0.0.1: internal/Policy/Auth/NKey/Codec.hs
module Auth.NKey.Codec
( encodeSignature
, signNonceWithSeedRaw
, validateUserPublicKey
) where
import qualified Crypto.Error as Crypto
import qualified Crypto.PubKey.Ed25519 as Ed25519
import qualified Data.Bits as Bits
import qualified Data.ByteArray as ByteArray
import qualified Data.ByteArray.Encoding as Encoding
import qualified Data.ByteString as BS
import qualified Data.List as List
import Data.Word (Word16, Word8)
signNonceWithSeedRaw
:: BS.ByteString
-> BS.ByteString
-> Either String (BS.ByteString, BS.ByteString)
signNonceWithSeedRaw seed nonce = do
seedBytes <- decodeSeed seed
secretKey <- toSecretKey seedBytes
let publicKey = Ed25519.toPublic secretKey
signature = Ed25519.sign secretKey publicKey nonce
signatureBytes = ByteArray.convert signature
publicBytes = ByteArray.convert publicKey
pure (encodePublicKey publicBytes, signatureBytes)
encodeSignature :: BS.ByteString -> BS.ByteString
encodeSignature = Encoding.convertToBase Encoding.Base64URLUnpadded
validateUserPublicKey :: BS.ByteString -> Either String ()
validateUserPublicKey encoded = do
raw <- decodeBase32Canonical encoded
payload <- verifyChecksum "public key" raw
if BS.length payload /= 33
then Left "public key length is invalid"
else
if BS.head payload /= prefixByteUser
then Left "public key is not a user nkey"
else Right ()
decodeSeed :: BS.ByteString -> Either String BS.ByteString
decodeSeed encoded = do
raw <- decodeBase32Canonical (trimAscii encoded)
payload <- verifyChecksum "seed" raw
if BS.length payload /= 34
then Left "seed length is invalid"
else do
let prefix0 = BS.index payload 0
prefix1 = BS.index payload 1
seedPrefix = prefix0 Bits..&. 0xF8
publicPrefix =
((prefix0 Bits..&. 0x07) `Bits.shiftL` 5)
Bits..|. (prefix1 `Bits.shiftR` 3)
if seedPrefix /= prefixByteSeed
then Left "seed prefix mismatch"
else
if publicPrefix /= prefixByteUser
then Left "seed is not a user nkey"
else Right (BS.drop 2 payload)
decodeBase32Canonical :: BS.ByteString -> Either String BS.ByteString
decodeBase32Canonical encoded = do
let paddingLength = (8 - BS.length encoded `mod` 8) `mod` 8
padded = encoded <> BS.replicate paddingLength 61
decoded <-
case Encoding.convertFromBase Encoding.Base32 padded of
Left err -> Left ("invalid base32 encoding: " ++ err)
Right bytes -> Right bytes
let canonical = BS.takeWhile (/= 61) (Encoding.convertToBase Encoding.Base32 decoded)
if canonical == encoded
then Right decoded
else Left "base32 encoding is not canonical"
verifyChecksum :: String -> BS.ByteString -> Either String BS.ByteString
verifyChecksum label raw
| BS.length raw < 3 = Left (label ++ " is too short")
| otherwise = do
let (payload, checksumBytes) = BS.splitAt (BS.length raw - 2) raw
expected <- decodeChecksum checksumBytes
if crc16 payload == expected
then Right payload
else Left (label ++ " checksum mismatch")
toSecretKey :: BS.ByteString -> Either String Ed25519.SecretKey
toSecretKey raw =
case Ed25519.secretKey raw of
Crypto.CryptoPassed key -> Right key
Crypto.CryptoFailed _ -> Left "seed could not be parsed as a secret key"
encodePublicKey :: BS.ByteString -> BS.ByteString
encodePublicKey raw =
let payload = BS.cons prefixByteUser raw
checksum = crc16 payload
in Encoding.convertToBase Encoding.Base32 (payload <> encodeChecksum checksum)
encodeChecksum :: Word16 -> BS.ByteString
encodeChecksum value =
BS.pack
[ fromIntegral (value Bits..&. 0xFF)
, fromIntegral (value `Bits.shiftR` 8)
]
decodeChecksum :: BS.ByteString -> Either String Word16
decodeChecksum bytes
| BS.length bytes /= 2 = Left "checksum length is invalid"
| otherwise =
let b0 = fromIntegral (BS.index bytes 0)
b1 = fromIntegral (BS.index bytes 1)
in Right (b0 Bits..|. (b1 `Bits.shiftL` 8))
crc16 :: BS.ByteString -> Word16
crc16 =
List.foldl' update 0 . BS.unpack
where
update crc byte =
List.foldl' step (crc `Bits.xor` (fromIntegral byte `Bits.shiftL` 8)) [0 .. 7]
step crc _ =
if Bits.testBit crc 15
then (crc `Bits.shiftL` 1) `Bits.xor` 0x1021
else crc `Bits.shiftL` 1
trimAscii :: BS.ByteString -> BS.ByteString
trimAscii =
dropWhileEndAscii isSpaceAscii . BS.dropWhile isSpaceAscii
dropWhileEndAscii :: (Word8 -> Bool) -> BS.ByteString -> BS.ByteString
dropWhileEndAscii predicate =
BS.reverse . BS.dropWhile predicate . BS.reverse
isSpaceAscii :: Word8 -> Bool
isSpaceAscii w =
w == 9 || w == 10 || w == 13 || w == 32
prefixByteSeed, prefixByteUser :: Word8
prefixByteSeed = 18 `Bits.shiftL` 3
prefixByteUser = 20 `Bits.shiftL` 3