web/backend/secretapi/crypto.go

133 lines
3.9 KiB
Go

package main
import (
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
ethmath "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
func buildTypedData(cfg Config, rec designationRecord) apitypes.TypedData {
return apitypes.TypedData{
Types: apitypes.Types{
"EIP712Domain": []apitypes.Type{
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
"DesignationIntent": []apitypes.Type{
{Name: "designationCode", Type: "string"},
{Name: "designationToken", Type: "string"},
{Name: "nonce", Type: "string"},
{Name: "issuedAt", Type: "string"},
{Name: "origin", Type: "string"},
},
},
PrimaryType: "DesignationIntent",
Domain: apitypes.TypedDataDomain{
Name: cfg.DomainName,
Version: "1",
ChainId: mathHexOrDecimal256(cfg.ChainID),
VerifyingContract: cfg.VerifyingContract,
},
Message: apitypes.TypedDataMessage{
"designationCode": rec.Code,
"designationToken": rec.DisplayToken,
"nonce": rec.Nonce,
"issuedAt": rec.IssuedAt.UTC().Format(time.RFC3339Nano),
"origin": rec.Origin,
},
}
}
func recoverSignerAddress(typedData apitypes.TypedData, signatureHex string) (string, error) {
dataHash, _, err := apitypes.TypedDataAndHash(typedData)
if err != nil {
return "", fmt.Errorf("typed data hash: %w", err)
}
signatureHex = strings.TrimPrefix(strings.TrimSpace(signatureHex), "0x")
sig, err := hex.DecodeString(signatureHex)
if err != nil {
return "", fmt.Errorf("decode signature: %w", err)
}
if len(sig) != 65 {
return "", fmt.Errorf("invalid signature length: %d", len(sig))
}
if sig[64] >= 27 {
sig[64] -= 27
}
pubKey, err := crypto.SigToPub(dataHash, sig)
if err != nil {
return "", fmt.Errorf("recover pubkey: %w", err)
}
return strings.ToLower(crypto.PubkeyToAddress(*pubKey).Hex()), nil
}
func normalizeAddress(address string) (string, error) {
address = strings.TrimSpace(strings.ToLower(address))
if !common.IsHexAddress(address) {
return "", fmt.Errorf("invalid wallet address")
}
return strings.ToLower(common.HexToAddress(address).Hex()), nil
}
func payerProofMessage(designationCode, ownerWallet, payerWallet string, chainID int64) string {
return fmt.Sprintf(
"EDUT-PAYER-AUTH:%s:%s:%s:%d",
strings.TrimSpace(designationCode),
strings.ToLower(strings.TrimSpace(ownerWallet)),
strings.ToLower(strings.TrimSpace(payerWallet)),
chainID,
)
}
func recoverPersonalSignAddress(message string, signatureHex string) (string, error) {
signatureHex = strings.TrimPrefix(strings.TrimSpace(signatureHex), "0x")
sig, err := hex.DecodeString(signatureHex)
if err != nil {
return "", fmt.Errorf("decode signature: %w", err)
}
if len(sig) != 65 {
return "", fmt.Errorf("invalid signature length: %d", len(sig))
}
if sig[64] >= 27 {
sig[64] -= 27
}
hash := accounts.TextHash([]byte(message))
pubKey, err := crypto.SigToPub(hash, sig)
if err != nil {
return "", fmt.Errorf("recover pubkey: %w", err)
}
return strings.ToLower(crypto.PubkeyToAddress(*pubKey).Hex()), nil
}
func verifyDistinctPayerProof(designationCode, ownerWallet, payerWallet string, chainID int64, signatureHex string) error {
msg := payerProofMessage(designationCode, ownerWallet, payerWallet, chainID)
recovered, err := recoverPersonalSignAddress(msg, signatureHex)
if err != nil {
return err
}
owner, err := normalizeAddress(ownerWallet)
if err != nil {
return err
}
if recovered != owner {
return fmt.Errorf("ownership proof signer mismatch")
}
return nil
}
func mathHexOrDecimal256(v int64) *ethmath.HexOrDecimal256 {
out := ethmath.NewHexOrDecimal256(v)
return out
}