nostr-1.3.0.0: lib/Nostr/Crypto.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE DeriveGeneric #-}
{-|
Module : Nostr.Crypto
Description : Cryptographic operations for Nostr events using BIP-340 Schnorr signatures
Copyright : (c) Emre YILMAZ, 2026
License : MIT
Maintainer : z@emre.xyz
This module provides cryptographic functions for Nostr events,
including key generation, event ID computation, and BIP-340 Schnorr signatures.
-}
module Nostr.Crypto
( -- * Key Management
SecKey
, generateKeyPair
, pubKeyFromSecKey
, exportSecKey
, exportPubKey
, secKeyFromBytes
, Keys(..)
, validateKeys
-- * Event Operations
, computeEventId
, signEvent
, verifySignature
, validateEvent
, serializeEventForId
-- * Utilities
, bytesToHex
, hexToBytes
) where
import qualified Crypto.Hash.Algorithms as Hash
import qualified Crypto.Hash as Hash
import qualified Crypto.Curve.Secp256k1 as Secp
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as B16
import Data.ByteString (ByteString)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Aeson (encode, toJSON)
import GHC.Generics (Generic) -- Added this line
-- import Data.Word (Word64) -- Hiding this to import from GHC.Word
import qualified System.Entropy as E
import Data.Bits (shiftR)
import Data.Word (Word8)
import Data.Word.Wider (Wider(..))
import Data.Word.Limb (Limb(..))
import GHC.Word (Word64(..), Word(..))
import Nostr.Event hiding (validateEvent)
-- | Secret key (private key) for signing - 32-byte wide word
type SecKey = Wider
-- | Generate a new secp256k1 key pair for Schnorr signatures
generateKeyPair :: IO (SecKey, PubKey)
generateKeyPair = do
-- Generate 32 random bytes for the secret key
secKeyBytes <- E.getEntropy 32
case secKeyFromBytes secKeyBytes of
Just secKey -> do
pubKey <- pubKeyFromSecKey secKey
return (secKey, pubKey)
Nothing -> error "Failed to create secret key from random bytes"
-- | Derive public key from secret key (x-only 32-byte for BIP-340)
pubKeyFromSecKey :: SecKey -> IO PubKey
pubKeyFromSecKey secKey = do
-- ppad-secp256k1 uses derive_pub to create public key from secret key
-- It returns Maybe Pub (Projective point)
case Secp.derive_pub secKey of
Just pubKey -> do
-- Serialize to compressed format (33 bytes) and drop the first byte
-- to get the 32-byte x-only public key as per BIP-340
let compressed = Secp.serialize_point pubKey
let pubKeyBytes = BS.drop 1 compressed
case mkPubKey (bytesToHex pubKeyBytes) of
Right pk -> return pk
Left err -> error $ "Failed to create PubKey: " ++ T.unpack err
Nothing -> error "Failed to derive public key"
-- | Create a SecKey from raw bytes (32 bytes)
secKeyFromBytes :: ByteString -> Maybe SecKey
secKeyFromBytes bs
| BS.length bs == 32 =
-- Convert 32 bytes to Wider Word64
let (w0, r1) = parseWord64 (BS.take 8 bs)
(w1, r2) = parseWord64 (BS.take 8 r1)
(w2, r3) = parseWord64 (BS.take 8 r2)
(w3, _) = parseWord64 (BS.take 8 r3)
unW (W# w) = w
toLimb :: Word64 -> Limb
toLimb w = Limb (unW (fromIntegral w :: Word))
l0 = toLimb w0
l1 = toLimb w1
l2 = toLimb w2
l3 = toLimb w3
in Just $ Wider (# l0, l1, l2, l3 #)
| otherwise = Nothing
where
parseWord64 :: ByteString -> (Word64, ByteString)
parseWord64 b =
let ws = BS.unpack b
w = foldr (\byte acc -> acc * 256 + fromIntegral byte) 0 ws
in (w, BS.drop 8 b)
-- ... (keep existing imports)
-- | Keys struct to hold key pair and optional relay info
data Keys = Keys
{ keysSecKey :: SecKey
, keysPubKey :: PubKey
, keysRelay :: Maybe Text
} deriving (Show, Generic)
-- | Equality check for Keys (compares PubKey and Relay only, ignores SecKey)
instance Eq Keys where
(Keys _ p1 r1) == (Keys _ p2 r2) = p1 == p2 && r1 == r2
-- | Validate that the public key matches the secret key
validateKeys :: Keys -> IO Bool
validateKeys (Keys sec pub _) = do
derived <- pubKeyFromSecKey sec
return $ derived == pub
-- | Export secret key as hex string
exportSecKey :: SecKey -> Text
exportSecKey (Wider (# l0, l1, l2, l3 #)) =
let
unL (Limb w) = fromIntegral (W# w) :: Word64
w0 = unL l0
w1 = unL l1
w2 = unL l2
w3 = unL l3
word64ToBytes :: Word64 -> [Word8]
word64ToBytes w =
[ fromIntegral (w `shiftR` 56)
, fromIntegral (w `shiftR` 48)
, fromIntegral (w `shiftR` 40)
, fromIntegral (w `shiftR` 32)
, fromIntegral (w `shiftR` 24)
, fromIntegral (w `shiftR` 16)
, fromIntegral (w `shiftR` 8)
, fromIntegral w
]
allBytes = concatMap word64ToBytes [w0, w1, w2, w3]
in bytesToHex $ BS.pack allBytes
-- | Export public key (already in Text format)
exportPubKey :: PubKey -> Text
exportPubKey = unPubKey
-- ============================================================================
-- Event ID Computation
-- ============================================================================
-- | Compute SHA-256 hash
sha256 :: ByteString -> ByteString
sha256 = BA.convert . Hash.hash @ByteString @Hash.SHA256
-- | Serialize event for ID computation
-- Returns JSON array: [0, pubkey, created_at, kind, tags, content]
serializeEventForId :: Event -> ByteString
serializeEventForId event =
let arr =
[ toJSON (0 :: Int)
, toJSON (unPubKey (eventPubkey event))
, toJSON (eventCreatedAt event)
, toJSON (eventKind event)
, toJSON (eventTags event)
, toJSON (eventContent event)
]
in BS.toStrict $ encode arr
-- | Compute event ID using SHA-256
computeEventId :: Event -> EventId
computeEventId event =
let serialized = serializeEventForId event
hash = sha256 serialized
hexHash = bytesToHex hash
in case mkEventId hexHash of
Right eid -> eid
Left err -> error $ "Failed to create EventId: " ++ T.unpack err
-- ============================================================================
-- BIP-340 Schnorr Signatures
-- ============================================================================
-- | Sign an event and set its id and sig fields using BIP-340 Schnorr signatures
signEvent :: SecKey -> Event -> IO (Either Text Event)
signEvent secKey event = do
-- First compute the event ID
let eventWithId = event { eventId = computeEventId event }
-- Get the event ID bytes for signing (must be 32 bytes for Schnorr)
case hexToBytes (unEventId (eventId eventWithId)) of
Left err -> return $ Left err
Right idBytes -> do
-- Generate 32 bytes of auxiliary randomness (recommended by BIP-340)
auxRand <- E.getEntropy 32
-- Sign with BIP-340 Schnorr
case Secp.sign_schnorr secKey idBytes auxRand of
Nothing -> return $ Left "Failed to create Schnorr signature"
Just sigBytes -> do
let sigHex = bytesToHex sigBytes
-- Create Signature with proper validation (should be 64 bytes = 128 hex chars)
case mkSignature sigHex of
Right signature -> return $ Right $ eventWithId { eventSig = signature }
Left err -> return $ Left err
-- | Verify event signature using BIP-340 Schnorr verification
verifySignature :: Event -> IO Bool
verifySignature event = do
case hexToBytes (unEventId (eventId event)) of
Left _ -> return False
Right idBytes ->
case hexToBytes (unPubKey (eventPubkey event)) of
Left _ -> return False
Right pubKeyBytes ->
case hexToBytes (unSignature (eventSig event)) of
Left _ -> return False
Right sigBytes -> do
-- Parse public key from x-only bytes
-- BIP-340 uses implicit even Y coordinate (prefix 0x02)
let compressedPubKey = BS.cons 0x02 pubKeyBytes
case Secp.parse_point compressedPubKey of
Nothing -> return False
Just pubKeyPoint ->
-- Verify Schnorr signature
return $ Secp.verify_schnorr idBytes pubKeyPoint sigBytes
-- | Validate event by checking both ID integrity and signature
validateEvent :: Event -> IO Bool
validateEvent event = do
let computedId = computeEventId event
if computedId /= eventId event
then return False
else verifySignature event
-- ============================================================================
-- Utility Functions
-- ============================================================================
-- | Convert bytes to lowercase hex string
bytesToHex :: ByteString -> Text
bytesToHex = TE.decodeUtf8 . B16.encode
-- | Convert hex string to bytes
hexToBytes :: Text -> Either Text ByteString
hexToBytes hexText =
case B16.decode (TE.encodeUtf8 hexText) of
Right bs -> Right bs
Left err -> Left $ "Invalid hex string: " <> T.pack err