status-go/server/pairing/connection.go

175 lines
4.4 KiB
Go

package pairing
import (
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
"math/big"
"net"
"net/url"
"strings"
"github.com/btcsuite/btcutil/base58"
"github.com/status-im/status-go/server/pairing/versioning"
)
const (
connectionStringID = "cs"
)
type ConnectionParams struct {
version versioning.ConnectionParamVersion
netIP net.IP
port int
publicKey *ecdsa.PublicKey
aesKey []byte
}
func NewConnectionParams(netIP net.IP, port int, publicKey *ecdsa.PublicKey, aesKey []byte) *ConnectionParams {
cp := new(ConnectionParams)
cp.version = versioning.LatestConnectionParamVer
cp.netIP = netIP
cp.port = port
cp.publicKey = publicKey
cp.aesKey = aesKey
return cp
}
// ToString generates a string required for generating a secure connection to another Status device.
//
// The returned string will look like below:
// - "cs2:4FHRnp:H6G:uqnnMwVUfJc2Fkcaojet8F1ufKC3hZdGEt47joyBx9yd:BbnZ7Gc66t54a9kEFCf7FW8SGQuYypwHVeNkRYeNoqV6"
//
// Format bytes encoded into a base58 string, delimited by ":"
// - string type identifier
// - version
// - net.IP
// - port
// - ecdsa CompressedPublicKey
// - AES encryption key
func (cp *ConnectionParams) ToString() string {
v := base58.Encode(new(big.Int).SetInt64(int64(cp.version)).Bytes())
ip := base58.Encode(cp.netIP)
p := base58.Encode(new(big.Int).SetInt64(int64(cp.port)).Bytes())
k := base58.Encode(elliptic.MarshalCompressed(cp.publicKey.Curve, cp.publicKey.X, cp.publicKey.Y))
ek := base58.Encode(cp.aesKey)
return fmt.Sprintf("%s%s:%s:%s:%s:%s", connectionStringID, v, ip, p, k, ek)
}
// FromString parses a connection params string required for to securely connect to another Status device.
// This function parses a connection string generated by ToString
func (cp *ConnectionParams) FromString(s string) error {
if len(s) < 2 {
return fmt.Errorf("connection string is invalid: '%s'", s)
}
if s[:2] != connectionStringID {
return fmt.Errorf("connection string doesn't begin with identifier '%s'", connectionStringID)
}
requiredParams := 5
sData := strings.Split(s[2:], ":")
if len(sData) != requiredParams {
return fmt.Errorf("expected data '%s' to have length of '%d', received '%d'", s, requiredParams, len(sData))
}
cp.version = versioning.ConnectionParamVersion(new(big.Int).SetBytes(base58.Decode(sData[0])).Int64())
cp.netIP = base58.Decode(sData[1])
cp.port = int(new(big.Int).SetBytes(base58.Decode(sData[2])).Int64())
cp.publicKey = new(ecdsa.PublicKey)
cp.publicKey.X, cp.publicKey.Y = elliptic.UnmarshalCompressed(elliptic.P256(), base58.Decode(sData[3]))
cp.publicKey.Curve = elliptic.P256()
cp.aesKey = base58.Decode(sData[4])
return cp.validate()
}
func (cp *ConnectionParams) validate() error {
err := cp.validateVersion()
if err != nil {
return err
}
err = cp.validateNetIP()
if err != nil {
return err
}
err = cp.validatePort()
if err != nil {
return err
}
err = cp.validatePublicKey()
if err != nil {
return err
}
return cp.validateAESKey()
}
func (cp *ConnectionParams) validateVersion() error {
if cp.version <= versioning.LatestConnectionParamVer {
return nil
}
return fmt.Errorf("unsupported version '%d'", cp.version)
}
func (cp *ConnectionParams) validateNetIP() error {
if ok := net.ParseIP(cp.netIP.String()); ok == nil {
return fmt.Errorf("invalid net ip '%s'", cp.netIP)
}
return nil
}
func (cp *ConnectionParams) validatePort() error {
if cp.port > 0 && cp.port < 0x10000 {
return nil
}
return fmt.Errorf("port '%d' outside of bounds of 1 - 65535", cp.port)
}
func (cp *ConnectionParams) validatePublicKey() error {
switch {
case cp.publicKey.Curve == nil, cp.publicKey.Curve != elliptic.P256():
return fmt.Errorf("public key Curve not `elliptic.P256`")
case cp.publicKey.X == nil, cp.publicKey.X.Cmp(big.NewInt(0)) == 0:
return fmt.Errorf("public key X not set")
case cp.publicKey.Y == nil, cp.publicKey.Y.Cmp(big.NewInt(0)) == 0:
return fmt.Errorf("public key Y not set")
default:
return nil
}
}
func (cp *ConnectionParams) validateAESKey() error {
if len(cp.aesKey) != 32 {
return fmt.Errorf("AES key invalid length, expect length 32, received length '%d'", len(cp.aesKey))
}
return nil
}
func (cp *ConnectionParams) URL() (*url.URL, error) {
err := cp.validate()
if err != nil {
return nil, err
}
u := &url.URL{
Scheme: "https",
Host: fmt.Sprintf("%s:%d", cp.netIP, cp.port),
}
return u, nil
}
func ValidateConnectionString(cs string) error {
ccp := ConnectionParams{}
err := ccp.FromString(cs)
return err
}