github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/exchange.go (about) 1 /* 2 * Copyright (c) 2019, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package psiphon 21 22 import ( 23 "encoding/base64" 24 "encoding/json" 25 26 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 27 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" 28 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol" 29 "golang.org/x/crypto/nacl/secretbox" 30 ) 31 32 // ExportExchangePayload creates a payload for client-to-client server 33 // connection info exchange. The payload includes the most recent successful 34 // server entry -- the server entry in the affinity position -- and any 35 // associated dial parameters, for the current network ID. 36 // 37 // ExportExchangePayload is intended to be called when the client is 38 // connected, as the affinity server will be the currently connected server 39 // and there will be dial parameters for the current network ID. 40 // 41 // Only signed server entries will be exchanged. The signature is created by 42 // the Psiphon Network and may be verified using the 43 // ServerEntrySignaturePublicKey embedded in clients. This signture defends 44 // against attacks by rogue clients and man-in-the-middle operatives which 45 // could otherwise cause the importer to receive phony server entry values. 46 // 47 // Only a subset of dial parameters are exchanged. See the comment for 48 // ExchangedDialParameters for more details. When no dial parameters is 49 // present the exchange proceeds without dial parameters. 50 // 51 // The exchange payload is obfuscated with the ExchangeObfuscationKey embedded 52 // in clients. The purpose of this obfuscation is to ensure that plaintext 53 // server entry info cannot be trivially exported and displayed or published; 54 // or at least require an effort equal to what's required without the export 55 // feature. 56 // 57 // There is no success notice for exchange ExportExchangePayload (or 58 // ImportExchangePayload) as this would potentially leak a user releationship if 59 // two users performed and exchange and subseqently submit diagnostic feedback 60 // containg import and export logs at almost the same point in time, along 61 // with logs showing connections to the same server, with source "EXCHANGED" 62 // in the importer case. 63 // 64 // Failure notices are logged as, presumably, the event will only appear on 65 // one end of the exchange and the error is potentially important diagnostics. 66 // 67 // There remains some risk of user linkability from Connecting/ConnectedServer 68 // diagnostics and metrics alone, because the appearance of "EXCHANGED" may 69 // indicate an exchange event. But there are various degrees of ambiguity in 70 // this case in terms of determining the server entry was freshly exchanged; 71 // and with likely many users often connecting to any given server in a short 72 // time period. 73 // 74 // The return value is a payload that may be exchanged with another client; 75 // when "", the export failed and a diagnostic notice has been logged. 76 func ExportExchangePayload(config *Config) string { 77 payload, err := exportExchangePayload(config) 78 if err != nil { 79 NoticeWarning("ExportExchangePayload failed: %s", errors.Trace(err)) 80 return "" 81 } 82 return payload 83 } 84 85 // ImportExchangePayload imports a payload generated by ExportExchangePayload. 86 // The server entry in the payload is promoted to the affinity position so it 87 // will be the first candidate in any establishment that begins after the 88 // import. 89 // 90 // The current network ID. This may not be the same network as the exporter, 91 // even if the client-to-client exchange occurs in real time. For example, if 92 // the exchange is performed over NFC between two devices, they may be on 93 // different mobile or WiFi networks. As mentioned in the comment for 94 // ExchangedDialParameters, the exchange dial parameters includes only the 95 // most broadly applicable fields. 96 // 97 // The return value indicates a successful import. If the import failed, a 98 // a diagnostic notice has been logged. 99 func ImportExchangePayload(config *Config, encodedPayload string) bool { 100 err := importExchangePayload(config, encodedPayload) 101 if err != nil { 102 NoticeWarning("ImportExchangePayload failed: %s", errors.Trace(err)) 103 return false 104 } 105 return true 106 } 107 108 type exchangePayload struct { 109 ServerEntryFields protocol.ServerEntryFields 110 ExchangedDialParameters *ExchangedDialParameters 111 } 112 113 func exportExchangePayload(config *Config) (string, error) { 114 115 networkID := config.GetNetworkID() 116 117 key, err := getExchangeObfuscationKey(config) 118 if err != nil { 119 return "", errors.Trace(err) 120 } 121 122 serverEntryFields, dialParams, err := 123 GetAffinityServerEntryAndDialParameters(networkID) 124 if err != nil { 125 return "", errors.Trace(err) 126 } 127 128 // Fail if the server entry has no signature, as the exchange would be 129 // insecure. Given the mechanism where handshake will return a signed server 130 // entry to clients without one, this case is not expected to occur. 131 if !serverEntryFields.HasSignature() { 132 return "", errors.TraceNew("export server entry not signed") 133 } 134 135 // RemoveUnsignedFields also removes potentially sensitive local fields, so 136 // explicitly strip these before exchanging. 137 serverEntryFields.RemoveUnsignedFields() 138 139 var exchangedDialParameters *ExchangedDialParameters 140 if dialParams != nil { 141 exchangedDialParameters = NewExchangedDialParameters(dialParams) 142 } 143 144 payload := &exchangePayload{ 145 ServerEntryFields: serverEntryFields, 146 ExchangedDialParameters: exchangedDialParameters, 147 } 148 149 payloadJSON, err := json.Marshal(payload) 150 if err != nil { 151 return "", errors.Trace(err) 152 } 153 154 // A unique nonce is generated and included with the payload as the 155 // obfuscation keys is not single-use. 156 nonce, err := common.MakeSecureRandomBytes(24) 157 if err != nil { 158 return "", errors.Trace(err) 159 } 160 161 var secretboxNonce [24]byte 162 copy(secretboxNonce[:], nonce) 163 var secretboxKey [32]byte 164 copy(secretboxKey[:], key) 165 boxedPayload := secretbox.Seal( 166 nil, payloadJSON, &secretboxNonce, &secretboxKey) 167 boxedPayload = append(secretboxNonce[:], boxedPayload...) 168 169 return base64.StdEncoding.EncodeToString(boxedPayload), nil 170 } 171 172 func importExchangePayload(config *Config, encodedPayload string) error { 173 174 networkID := config.GetNetworkID() 175 176 key, err := getExchangeObfuscationKey(config) 177 if err != nil { 178 return errors.Trace(err) 179 } 180 181 boxedPayload, err := base64.StdEncoding.DecodeString(encodedPayload) 182 if err != nil { 183 return errors.Trace(err) 184 } 185 186 if len(boxedPayload) <= 24 { 187 return errors.TraceNew("unexpected box length") 188 } 189 190 var secretboxNonce [24]byte 191 copy(secretboxNonce[:], boxedPayload[:24]) 192 var secretboxKey [32]byte 193 copy(secretboxKey[:], key) 194 payloadJSON, ok := secretbox.Open( 195 nil, boxedPayload[24:], &secretboxNonce, &secretboxKey) 196 if !ok { 197 return errors.TraceNew("unbox failed") 198 } 199 200 var payload *exchangePayload 201 err = json.Unmarshal(payloadJSON, &payload) 202 if err != nil { 203 return errors.Trace(err) 204 } 205 206 // Explicitly strip any unsigned fields that should not be exchanged or 207 // imported. 208 payload.ServerEntryFields.RemoveUnsignedFields() 209 210 err = payload.ServerEntryFields.VerifySignature( 211 config.ServerEntrySignaturePublicKey) 212 if err != nil { 213 return errors.Trace(err) 214 } 215 216 payload.ServerEntryFields.SetLocalSource( 217 protocol.SERVER_ENTRY_SOURCE_EXCHANGED) 218 payload.ServerEntryFields.SetLocalTimestamp( 219 common.TruncateTimestampToHour(common.GetCurrentTimestamp())) 220 221 // The following sequence of datastore calls -- StoreServerEntry, 222 // PromoteServerEntry, SetDialParameters -- is not an atomic transaction but 223 // the datastore will end up in a consistent state in case of failure to 224 // complete the sequence. The existing calls are reused to avoid redundant 225 // code. 226 // 227 // TODO: refactor existing code to allow reuse in a single transaction? 228 229 err = StoreServerEntry(payload.ServerEntryFields, true) 230 if err != nil { 231 return errors.Trace(err) 232 } 233 234 err = PromoteServerEntry(config, payload.ServerEntryFields.GetIPAddress()) 235 if err != nil { 236 return errors.Trace(err) 237 } 238 239 if payload.ExchangedDialParameters != nil { 240 241 serverEntry, err := payload.ServerEntryFields.GetServerEntry() 242 if err != nil { 243 return errors.Trace(err) 244 } 245 246 // Don't abort if Validate fails, as the current client may simply not 247 // support the exchanged dial parameter values (for example, a new tunnel 248 // protocol). 249 // 250 // No notice is issued in the error case for the give linkage reason, as the 251 // notice would be a proxy for an import success log. 252 253 err = payload.ExchangedDialParameters.Validate(serverEntry) 254 if err == nil { 255 dialParams := payload.ExchangedDialParameters.MakeDialParameters( 256 config, 257 config.GetParameters().Get(), 258 serverEntry) 259 260 err = SetDialParameters( 261 payload.ServerEntryFields.GetIPAddress(), 262 networkID, 263 dialParams) 264 if err != nil { 265 return errors.Trace(err) 266 } 267 } 268 } 269 270 return nil 271 } 272 273 func getExchangeObfuscationKey(config *Config) ([]byte, error) { 274 key, err := base64.StdEncoding.DecodeString(config.ExchangeObfuscationKey) 275 if err != nil { 276 return nil, errors.Trace(err) 277 } 278 if len(key) != 32 { 279 return nil, errors.TraceNew("invalid key size") 280 } 281 return key, nil 282 }