github.com/status-im/status-go@v1.1.0/server/pairing/connection.go (about)

     1  package pairing
     2  
     3  import (
     4  	"crypto/ecdsa"
     5  	"crypto/elliptic"
     6  	"fmt"
     7  	"log"
     8  	"math/big"
     9  	"net"
    10  	"net/url"
    11  	"strings"
    12  
    13  	"github.com/btcsuite/btcutil/base58"
    14  	"github.com/google/uuid"
    15  
    16  	"github.com/status-im/status-go/server/pairing/versioning"
    17  )
    18  
    19  const (
    20  	connectionStringID = "cs"
    21  )
    22  
    23  type ConnectionParams struct {
    24  	version        versioning.ConnectionParamVersion
    25  	netIPs         []net.IP
    26  	port           int
    27  	publicKey      *ecdsa.PublicKey
    28  	aesKey         []byte
    29  	installationID string
    30  	keyUID         string
    31  }
    32  
    33  func NewConnectionParams(netIPs []net.IP, port int, publicKey *ecdsa.PublicKey, aesKey []byte, installationID, keyUID string) *ConnectionParams {
    34  	cp := new(ConnectionParams)
    35  	cp.version = versioning.LatestConnectionParamVer
    36  	cp.netIPs = netIPs
    37  	cp.port = port
    38  	cp.publicKey = publicKey
    39  	cp.aesKey = aesKey
    40  	cp.installationID = installationID
    41  	cp.keyUID = keyUID
    42  	return cp
    43  }
    44  
    45  // ToString generates a string required for generating a secure connection to another Status device.
    46  //
    47  // The returned string will look like below:
    48  //   - "cs2:4FHRnp:H6G:uqnnMwVUfJc2Fkcaojet8F1ufKC3hZdGEt47joyBx9yd:BbnZ7Gc66t54a9kEFCf7FW8SGQuYypwHVeNkRYeNoqV6"
    49  //
    50  // Format bytes encoded into a base58 string, delimited by ":"
    51  //   - string type identifier
    52  //   - version
    53  //   - net.IP
    54  //   - array of IPs in next form:
    55  //     | 1 byte | 4*N bytes | 1 byte | 16*N bytes |
    56  //     |   N    | N * IPv4  |    M   |  M * IPv6  |
    57  //   - port
    58  //   - ecdsa CompressedPublicKey
    59  //   - AES encryption key
    60  //   - string InstallationID of the sending device
    61  //   - string KeyUID of the sending device
    62  // NOTE:
    63  // - append(accrete) parameters instead of changing(breaking) existing parameters. Appending should **never** break, modifying existing parameters will break. Watch this before making changes: https://www.youtube.com/watch?v=oyLBGkS5ICk
    64  // - never strictly check version, unless you really want to break
    65  
    66  // This flag is used to keep compatibility with 2.29. It will output a 5 parameters connection string with version 3.
    67  var keep229Compatibility bool = true
    68  
    69  func (cp *ConnectionParams) ToString() string {
    70  	v := base58.Encode(new(big.Int).SetInt64(int64(cp.version)).Bytes())
    71  	ips := base58.Encode(SerializeNetIps(cp.netIPs))
    72  	p := base58.Encode(new(big.Int).SetInt64(int64(cp.port)).Bytes())
    73  	k := base58.Encode(elliptic.MarshalCompressed(cp.publicKey.Curve, cp.publicKey.X, cp.publicKey.Y))
    74  	ek := base58.Encode(cp.aesKey)
    75  
    76  	if keep229Compatibility {
    77  		return fmt.Sprintf("%s%s:%s:%s:%s:%s", connectionStringID, v, ips, p, k, ek)
    78  	}
    79  
    80  	var i string
    81  	if cp.installationID != "" {
    82  
    83  		u, err := uuid.Parse(cp.installationID)
    84  		if err != nil {
    85  			log.Fatalf("Failed to parse UUID: %v", err)
    86  		} else {
    87  
    88  			// Convert UUID to byte slice
    89  			byteSlice := u[:]
    90  			i = base58.Encode(byteSlice)
    91  		}
    92  	}
    93  
    94  	var kuid string
    95  	if cp.keyUID != "" {
    96  		kuid = base58.Encode([]byte(cp.keyUID))
    97  
    98  	}
    99  
   100  	return fmt.Sprintf("%s%s:%s:%s:%s:%s:%s:%s", connectionStringID, v, ips, p, k, ek, i, kuid)
   101  }
   102  
   103  func (cp *ConnectionParams) InstallationID() string {
   104  	return cp.installationID
   105  }
   106  
   107  func (cp *ConnectionParams) KeyUID() string {
   108  	return cp.keyUID
   109  }
   110  
   111  func SerializeNetIps(ips []net.IP) []byte {
   112  	var out []byte
   113  	var ipv4 []net.IP
   114  	var ipv6 []net.IP
   115  
   116  	for _, ip := range ips {
   117  		if v := ip.To4(); v != nil {
   118  			ipv4 = append(ipv4, v)
   119  		} else {
   120  			ipv6 = append(ipv6, ip)
   121  		}
   122  	}
   123  
   124  	for _, arr := range [][]net.IP{ipv4, ipv6} {
   125  		out = append(out, uint8(len(arr)))
   126  		for _, ip := range arr {
   127  			out = append(out, ip...)
   128  		}
   129  	}
   130  
   131  	return out
   132  }
   133  
   134  func ParseNetIps(in []byte) ([]net.IP, error) {
   135  	var out []net.IP
   136  
   137  	if len(in) < 1 {
   138  		return nil, fmt.Errorf("net.ip field is too short: '%d', at least 1 byte required", len(in))
   139  	}
   140  
   141  	for _, ipLen := range []int{net.IPv4len, net.IPv6len} {
   142  
   143  		count := int(in[0])
   144  		in = in[1:]
   145  
   146  		if expectedLen := ipLen * count; len(in) < expectedLen {
   147  			return nil, fmt.Errorf("net.ip.ip%d field is too short, expected at least '%d' bytes, '%d' bytes found", ipLen, expectedLen, len(in))
   148  		}
   149  
   150  		for i := 0; i < count; i++ {
   151  			offset := i * ipLen
   152  			ip := in[offset : ipLen+offset]
   153  			out = append(out, ip)
   154  		}
   155  
   156  		in = in[ipLen*count:]
   157  	}
   158  
   159  	return out, nil
   160  }
   161  
   162  // FromString parses a connection params string required for to securely connect to another Status device.
   163  // This function parses a connection string generated by ToString
   164  func (cp *ConnectionParams) FromString(s string) error {
   165  
   166  	if len(s) < 2 {
   167  		return fmt.Errorf("connection string is too short: '%s'", s)
   168  	}
   169  
   170  	if s[:2] != connectionStringID {
   171  		return fmt.Errorf("connection string doesn't begin with identifier '%s'", connectionStringID)
   172  	}
   173  
   174  	requiredParams := 5
   175  
   176  	sData := strings.Split(s[2:], ":")
   177  	// NOTE: always allow extra parameters for forward compatibility, error on not enough required parameters or failing to parse
   178  	if len(sData) < requiredParams {
   179  		return fmt.Errorf("expected data '%s' to have length of '%d', received '%d'", s, requiredParams, len(sData))
   180  	}
   181  
   182  	netIpsBytes := base58.Decode(sData[1])
   183  	netIps, err := ParseNetIps(netIpsBytes)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	cp.netIPs = netIps
   188  
   189  	cp.port = int(new(big.Int).SetBytes(base58.Decode(sData[2])).Int64())
   190  	cp.publicKey = new(ecdsa.PublicKey)
   191  	cp.publicKey.X, cp.publicKey.Y = elliptic.UnmarshalCompressed(elliptic.P256(), base58.Decode(sData[3]))
   192  	cp.publicKey.Curve = elliptic.P256()
   193  	cp.aesKey = base58.Decode(sData[4])
   194  
   195  	if len(sData) > 5 && len(sData[5]) != 0 {
   196  		installationIDBytes := base58.Decode(sData[5])
   197  		installationID, err := uuid.FromBytes(installationIDBytes)
   198  		if err != nil {
   199  			return err
   200  		}
   201  		cp.installationID = installationID.String()
   202  	}
   203  
   204  	if len(sData) > 6 && len(sData[6]) != 0 {
   205  		decodedBytes := base58.Decode(sData[6])
   206  		cp.keyUID = string(decodedBytes)
   207  	}
   208  
   209  	return cp.validate()
   210  }
   211  
   212  func (cp *ConnectionParams) validate() error {
   213  	err := cp.validateNetIP()
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	err = cp.validatePort()
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	err = cp.validatePublicKey()
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	return cp.validateAESKey()
   229  }
   230  
   231  func (cp *ConnectionParams) validateNetIP() error {
   232  	for _, ip := range cp.netIPs {
   233  		if ok := net.ParseIP(ip.String()); ok == nil {
   234  			return fmt.Errorf("invalid net ip '%s'", cp.netIPs)
   235  		}
   236  	}
   237  	return nil
   238  }
   239  
   240  func (cp *ConnectionParams) validatePort() error {
   241  	if cp.port > 0 && cp.port < 0x10000 {
   242  		return nil
   243  	}
   244  
   245  	return fmt.Errorf("port '%d' outside of bounds of 1 - 65535", cp.port)
   246  }
   247  
   248  func (cp *ConnectionParams) validatePublicKey() error {
   249  	switch {
   250  	case cp.publicKey.Curve == nil, cp.publicKey.Curve != elliptic.P256():
   251  		return fmt.Errorf("public key Curve not `elliptic.P256`")
   252  	case cp.publicKey.X == nil, cp.publicKey.X.Cmp(big.NewInt(0)) == 0:
   253  		return fmt.Errorf("public key X not set")
   254  	case cp.publicKey.Y == nil, cp.publicKey.Y.Cmp(big.NewInt(0)) == 0:
   255  		return fmt.Errorf("public key Y not set")
   256  	default:
   257  		return nil
   258  	}
   259  }
   260  
   261  func (cp *ConnectionParams) validateAESKey() error {
   262  	if len(cp.aesKey) != 32 {
   263  		return fmt.Errorf("AES key invalid length, expect length 32, received length '%d'", len(cp.aesKey))
   264  	}
   265  	return nil
   266  }
   267  
   268  func (cp *ConnectionParams) URL(IPIndex int) (*url.URL, error) {
   269  	if IPIndex < 0 || IPIndex >= len(cp.netIPs) {
   270  		return nil, fmt.Errorf("invalid IP index '%d'", IPIndex)
   271  	}
   272  
   273  	err := cp.validate()
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  
   278  	return cp.BuildURL(cp.netIPs[IPIndex]), nil
   279  }
   280  
   281  func (cp *ConnectionParams) BuildURL(ip net.IP) *url.URL {
   282  	return &url.URL{
   283  		Scheme: "https",
   284  		Host:   fmt.Sprintf("%s:%d", ip, cp.port),
   285  	}
   286  }
   287  
   288  func ValidateConnectionString(cs string) error {
   289  	ccp := ConnectionParams{}
   290  	err := ccp.FromString(cs)
   291  	return err
   292  }