| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- /*
- * Copyright (c) 2019, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- package psiphon
- import (
- "encoding/base64"
- "encoding/json"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
- "golang.org/x/crypto/nacl/secretbox"
- )
- // ExportExchangePayload creates a payload for client-to-client server
- // connection info exchange. The payload includes the most recent successful
- // server entry -- the server entry in the affinity position -- and any
- // associated dial parameters, for the current network ID.
- //
- // ExportExchangePayload is intended to be called when the client is
- // connected, as the affinity server will be the currently connected server
- // and there will be dial parameters for the current network ID.
- //
- // Only signed server entries will be exchanged. The signature is created by
- // the Psiphon Network and may be verified using the
- // ServerEntrySignaturePublicKey embedded in clients. This signture defends
- // against attacks by rogue clients and man-in-the-middle operatives which
- // could otherwise cause the importer to receive phony server entry values.
- //
- // Only a subset of dial parameters are exchanged. See the comment for
- // ExchangedDialParameters for more details. When no dial parameters is
- // present the exchange proceeds without dial parameters.
- //
- // The exchange payload is obfuscated with the ExchangeObfuscationKey embedded
- // in clients. The purpose of this obfuscation is to ensure that plaintext
- // server entry info cannot be trivially exported and displayed or published;
- // or at least require an effort equal to what's required without the export
- // feature.
- //
- // There is no success notice for exchange ExportExchangePayload (or
- // ImportExchangePayload) as this would potentially leak a user releationship if
- // two users performed and exchange and subseqently submit diagnostic feedback
- // containg import and export logs at almost the same point in time, along
- // with logs showing connections to the same server, with source "EXCHANGED"
- // in the importer case.
- //
- // Failure notices are logged as, presumably, the event will only appear on
- // one end of the exchange and the error is potentially important diagnostics.
- //
- // There remains some risk of user linkability from Connecting/ConnectedServer
- // diagnostics and metrics alone, because the appearance of "EXCHANGED" may
- // indicate an exchange event. But there are various degrees of ambiguity in
- // this case in terms of determining the server entry was freshly exchanged;
- // and with likely many users often connecting to any given server in a short
- // time period.
- //
- // The return value is a payload that may be exchanged with another client;
- // when "", the export failed and a diagnostic notice has been logged.
- func ExportExchangePayload(config *Config) string {
- payload, err := exportExchangePayload(config)
- if err != nil {
- NoticeWarning("ExportExchangePayload failed: %s", errors.Trace(err))
- return ""
- }
- return payload
- }
- // ImportExchangePayload imports a payload generated by ExportExchangePayload.
- // The server entry in the payload is promoted to the affinity position so it
- // will be the first candidate in any establishment that begins after the
- // import.
- //
- // The current network ID. This may not be the same network as the exporter,
- // even if the client-to-client exchange occurs in real time. For example, if
- // the exchange is performed over NFC between two devices, they may be on
- // different mobile or WiFi networks. As mentioned in the comment for
- // ExchangedDialParameters, the exchange dial parameters includes only the
- // most broadly applicable fields.
- //
- // The return value indicates a successful import. If the import failed, a
- // a diagnostic notice has been logged.
- func ImportExchangePayload(config *Config, encodedPayload string) bool {
- err := importExchangePayload(config, encodedPayload)
- if err != nil {
- NoticeWarning("ImportExchangePayload failed: %s", errors.Trace(err))
- return false
- }
- return true
- }
- type exchangePayload struct {
- ServerEntryFields protocol.ServerEntryFields
- ExchangedDialParameters *ExchangedDialParameters
- }
- func exportExchangePayload(config *Config) (string, error) {
- networkID := config.GetNetworkID()
- key, err := getExchangeObfuscationKey(config)
- if err != nil {
- return "", errors.Trace(err)
- }
- serverEntryFields, dialParams, err :=
- GetAffinityServerEntryAndDialParameters(networkID)
- if err != nil {
- return "", errors.Trace(err)
- }
- // Fail if the server entry has no signature, as the exchange would be
- // insecure. Given the mechanism where handshake will return a signed server
- // entry to clients without one, this case is not expected to occur.
- if !serverEntryFields.HasSignature() {
- return "", errors.TraceNew("export server entry not signed")
- }
- // RemoveUnsignedFields also removes potentially sensitive local fields, so
- // explicitly strip these before exchanging.
- serverEntryFields.RemoveUnsignedFields()
- var exchangedDialParameters *ExchangedDialParameters
- if dialParams != nil {
- exchangedDialParameters = NewExchangedDialParameters(dialParams)
- }
- payload := &exchangePayload{
- ServerEntryFields: serverEntryFields,
- ExchangedDialParameters: exchangedDialParameters,
- }
- payloadJSON, err := json.Marshal(payload)
- if err != nil {
- return "", errors.Trace(err)
- }
- // A unique nonce is generated and included with the payload as the
- // obfuscation keys is not single-use.
- nonce, err := common.MakeSecureRandomBytes(24)
- if err != nil {
- return "", errors.Trace(err)
- }
- var secretboxNonce [24]byte
- copy(secretboxNonce[:], nonce)
- var secretboxKey [32]byte
- copy(secretboxKey[:], key)
- boxedPayload := secretbox.Seal(
- nil, payloadJSON, &secretboxNonce, &secretboxKey)
- boxedPayload = append(secretboxNonce[:], boxedPayload...)
- return base64.StdEncoding.EncodeToString(boxedPayload), nil
- }
- func importExchangePayload(config *Config, encodedPayload string) error {
- networkID := config.GetNetworkID()
- key, err := getExchangeObfuscationKey(config)
- if err != nil {
- return errors.Trace(err)
- }
- boxedPayload, err := base64.StdEncoding.DecodeString(encodedPayload)
- if err != nil {
- return errors.Trace(err)
- }
- if len(boxedPayload) <= 24 {
- return errors.TraceNew("unexpected box length")
- }
- var secretboxNonce [24]byte
- copy(secretboxNonce[:], boxedPayload[:24])
- var secretboxKey [32]byte
- copy(secretboxKey[:], key)
- payloadJSON, ok := secretbox.Open(
- nil, boxedPayload[24:], &secretboxNonce, &secretboxKey)
- if !ok {
- return errors.TraceNew("unbox failed")
- }
- var payload *exchangePayload
- err = json.Unmarshal(payloadJSON, &payload)
- if err != nil {
- return errors.Trace(err)
- }
- // Explicitly strip any unsigned fields that should not be exchanged or
- // imported.
- payload.ServerEntryFields.RemoveUnsignedFields()
- err = payload.ServerEntryFields.VerifySignature(
- config.ServerEntrySignaturePublicKey)
- if err != nil {
- return errors.Trace(err)
- }
- payload.ServerEntryFields.SetLocalSource(
- protocol.SERVER_ENTRY_SOURCE_EXCHANGED)
- payload.ServerEntryFields.SetLocalTimestamp(
- common.TruncateTimestampToHour(common.GetCurrentTimestamp()))
- // The following sequence of datastore calls -- StoreServerEntry,
- // PromoteServerEntry, SetDialParameters -- is not an atomic transaction but
- // the datastore will end up in a consistent state in case of failure to
- // complete the sequence. The existing calls are reused to avoid redundant
- // code.
- //
- // TODO: refactor existing code to allow reuse in a single transaction?
- err = StoreServerEntry(payload.ServerEntryFields, true)
- if err != nil {
- return errors.Trace(err)
- }
- err = PromoteServerEntry(config, payload.ServerEntryFields.GetIPAddress())
- if err != nil {
- return errors.Trace(err)
- }
- if payload.ExchangedDialParameters != nil {
- serverEntry, err := payload.ServerEntryFields.GetServerEntry()
- if err != nil {
- return errors.Trace(err)
- }
- // Don't abort if Validate fails, as the current client may simply not
- // support the exchanged dial parameter values (for example, a new tunnel
- // protocol).
- //
- // No notice is issued in the error case for the give linkage reason, as the
- // notice would be a proxy for an import success log.
- err = payload.ExchangedDialParameters.Validate(serverEntry)
- if err == nil {
- dialParams := payload.ExchangedDialParameters.MakeDialParameters(
- config,
- config.GetParameters().Get(),
- serverEntry)
- err = SetDialParameters(
- payload.ServerEntryFields.GetIPAddress(),
- networkID,
- dialParams)
- if err != nil {
- return errors.Trace(err)
- }
- }
- }
- return nil
- }
- func getExchangeObfuscationKey(config *Config) ([]byte, error) {
- key, err := base64.StdEncoding.DecodeString(config.ExchangeObfuscationKey)
- if err != nil {
- return nil, errors.Trace(err)
- }
- if len(key) != 32 {
- return nil, errors.TraceNew("invalid key size")
- }
- return key, nil
- }
|