55e693aa09
This commit fixes a panic that can result when a zpay32 payment request that is too short (and possibly invalid) is attempted to be decoded. To fix this bug, we now simply ensure that that after we decode the zbase32 encoding, the resulting set of bytes is _exactly_ the length we expect. A new error has been introduced to handle this case, and a simple test has been added which ensures proper handling of short payment requests. Fixes #127.
151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
package zpay32
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io"
|
|
|
|
"github.com/roasbeef/btcd/btcec"
|
|
"github.com/roasbeef/btcutil"
|
|
"github.com/tv42/zbase32"
|
|
)
|
|
|
|
// invoiceSize is the size of an encoded invoice without the added check-sum.
|
|
// The size of broken down as follows: 33-bytes (destination pub key), 32-bytes
|
|
// (payment hash), 8-bytes for the payment amount in satoshis.
|
|
const invoiceSize = 33 + 32 + 8
|
|
|
|
// ErrCheckSumMismatch is returned byt he Decode function fi when
|
|
// decoding an encoded invoice, the checksum doesn't match indicating
|
|
// an error somewhere in the bitstream.
|
|
var ErrCheckSumMismatch = errors.New("the checksum is incorrect")
|
|
|
|
// ErrDataTooShort is returned by the Decode function if when
|
|
// decoding an encoded payment request, the number of bytes decoded
|
|
// is too few for a valid invoice indicating invalid input.
|
|
var ErrDataTooShort = errors.New("the decoded data is too short")
|
|
|
|
// PaymentRequest is a bare-bones invoice for a payment within the Lightning
|
|
// Network. With the details of the invoice, the sender has all the data
|
|
// necessary to send a payment to the recipient.
|
|
type PaymentRequest struct {
|
|
// Destination is the public key of the node to be paid.
|
|
Destination *btcec.PublicKey
|
|
|
|
// PaymentHash is the has to use within the HTLC extended throughout
|
|
// the payment path to the destination.
|
|
PaymentHash [32]byte
|
|
|
|
// Amount is the amount to be sent to the destination expressed in
|
|
// satoshis.
|
|
Amount btcutil.Amount
|
|
}
|
|
|
|
// castagnoli is an initialized crc32 checksum generated which Castagnoli's
|
|
// polynomial.
|
|
var castagnoli = crc32.MakeTable(crc32.Castagnoli)
|
|
|
|
// checkSum calculates a 4-byte crc32 checksum of the passed data. The returned
|
|
// uint32 is serialized as a big-endian integer.
|
|
func checkSum(data []byte) []byte {
|
|
crc := crc32.New(castagnoli)
|
|
crc.Write(data)
|
|
return crc.Sum(nil)
|
|
}
|
|
|
|
// Encode encodes the passed payment request using zbase32 with an added 4-byte
|
|
// crc32 checksum. The resulting encoding is 77-bytes long and consists of 124
|
|
// ASCII characters.
|
|
// TODO(roasbeef): add version byte?
|
|
func Encode(payReq *PaymentRequest) string {
|
|
var (
|
|
invoiceBytes [invoiceSize]byte
|
|
n int
|
|
)
|
|
|
|
// First copy each of the elements of the payment request into the
|
|
// buffer. Creating a stream that resembles: dest || r_hash || amt
|
|
n += copy(invoiceBytes[:], payReq.Destination.SerializeCompressed())
|
|
n += copy(invoiceBytes[n:], payReq.PaymentHash[:])
|
|
binary.BigEndian.PutUint64(invoiceBytes[n:], uint64(payReq.Amount))
|
|
|
|
// Next, we append the checksum to the end of the buffer which covers
|
|
// the serialized payment request.
|
|
b := append(invoiceBytes[:], checkSum(invoiceBytes[:])...)
|
|
|
|
// Finally encode the raw bytes as a zbase32 encoded string.
|
|
return zbase32.EncodeToString(b)
|
|
}
|
|
|
|
// Decode attempts to decode the zbase32 encoded payment request. If the
|
|
// trailing checksum doesn't match, then an error is returned.
|
|
func Decode(payData string) (*PaymentRequest, error) {
|
|
if payData == "" {
|
|
return nil, fmt.Errorf("encoded payment request must be a " +
|
|
"non-empty string")
|
|
}
|
|
|
|
// First we decode the zbase32 encoded string into a series of raw
|
|
// bytes.
|
|
payReqBytes, err := zbase32.DecodeString(payData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if there are at least enough bytes to represent the invoice
|
|
// and the checksum.
|
|
if len(payReqBytes) < invoiceSize+crc32.Size {
|
|
return nil, ErrDataTooShort
|
|
}
|
|
|
|
// With the bytes decoded, we verify the checksum to ensure the
|
|
// payment request wasn't altered in its decoded form.
|
|
invoiceBytes := payReqBytes[:invoiceSize]
|
|
generatedSum := checkSum(invoiceBytes)
|
|
|
|
// If the checksums don't match, then we return an error to the
|
|
// possibly detected error.
|
|
encodedSum := payReqBytes[invoiceSize:]
|
|
if !bytes.Equal(encodedSum, generatedSum) {
|
|
return nil, ErrCheckSumMismatch
|
|
}
|
|
|
|
// Otherwise, we've verified the integrity of the encoded payment
|
|
// request and can safely decode the payReq, passing it back up to the
|
|
// caller.
|
|
invoiceReader := bytes.NewReader(invoiceBytes)
|
|
return decodePaymentRequest(invoiceReader)
|
|
}
|
|
|
|
func decodePaymentRequest(r io.Reader) (*PaymentRequest, error) {
|
|
var err error
|
|
|
|
i := &PaymentRequest{}
|
|
|
|
var pubKey [33]byte
|
|
if _, err := io.ReadFull(r, pubKey[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i.Destination, err = btcec.ParsePubKey(pubKey[:], btcec.S256())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := io.ReadFull(r, i.PaymentHash[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var amt [8]byte
|
|
if _, err := io.ReadFull(r, amt[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i.Amount = btcutil.Amount(binary.BigEndian.Uint64(amt[:]))
|
|
|
|
return i, nil
|
|
}
|