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  }