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 }