github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/feedback.go (about)

     1  /*
     2  * Copyright (c) 2016, 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  	"bytes"
    24  	"context"
    25  	"crypto/aes"
    26  	"crypto/cipher"
    27  	"crypto/hmac"
    28  	"crypto/rand"
    29  	"crypto/rsa"
    30  	"crypto/sha1"
    31  	"crypto/sha256"
    32  	"crypto/x509"
    33  	"encoding/base64"
    34  	"encoding/json"
    35  	"fmt"
    36  	"net"
    37  	"net/http"
    38  	"net/url"
    39  	"path"
    40  	"time"
    41  
    42  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    43  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    44  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
    45  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
    46  )
    47  
    48  // Conforms to the format expected by the feedback decryptor.
    49  // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/EmailResponder/FeedbackDecryptor/decryptor.py
    50  type secureFeedback struct {
    51  	IV                   string `json:"iv"`
    52  	ContentCipherText    string `json:"contentCiphertext"`
    53  	WrappedEncryptionKey string `json:"wrappedEncryptionKey"`
    54  	ContentMac           string `json:"contentMac"`
    55  	WrappedMacKey        string `json:"wrappedMacKey"`
    56  }
    57  
    58  // Encrypt and marshal feedback into secure json structure utilizing the
    59  // Encrypt-then-MAC paradigm (https://tools.ietf.org/html/rfc7366#section-3).
    60  func encryptFeedback(diagnostics, b64EncodedPublicKey string) ([]byte, error) {
    61  	publicKey, err := base64.StdEncoding.DecodeString(b64EncodedPublicKey)
    62  	if err != nil {
    63  		return nil, errors.Trace(err)
    64  	}
    65  
    66  	iv, encryptionKey, diagnosticsCiphertext, err := encryptAESCBC([]byte(diagnostics))
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	digest, macKey, err := generateHMAC(iv, diagnosticsCiphertext)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	wrappedMacKey, err := encryptWithPublicKey(macKey, publicKey)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	wrappedEncryptionKey, err := encryptWithPublicKey(encryptionKey, publicKey)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	var securedFeedback = secureFeedback{
    85  		IV:                   base64.StdEncoding.EncodeToString(iv),
    86  		ContentCipherText:    base64.StdEncoding.EncodeToString(diagnosticsCiphertext),
    87  		WrappedEncryptionKey: base64.StdEncoding.EncodeToString(wrappedEncryptionKey),
    88  		ContentMac:           base64.StdEncoding.EncodeToString(digest),
    89  		WrappedMacKey:        base64.StdEncoding.EncodeToString(wrappedMacKey),
    90  	}
    91  
    92  	encryptedFeedback, err := json.Marshal(securedFeedback)
    93  	if err != nil {
    94  		return nil, errors.Trace(err)
    95  	}
    96  
    97  	return encryptedFeedback, nil
    98  }
    99  
   100  // Encrypt feedback and upload to server. If upload fails
   101  // the routine will sleep and retry multiple times.
   102  func SendFeedback(ctx context.Context, config *Config, diagnostics, uploadPath string) error {
   103  
   104  	if len(diagnostics) == 0 {
   105  		return errors.TraceNew("error diagnostics empty")
   106  	}
   107  
   108  	// Initialize a resolver to use for dials. useBindToDevice is false so
   109  	// that the feedback upload will be tunneled, indirectly, if it routes
   110  	// through the VPN.
   111  	//
   112  	// config.SetResolver makes this resolver available to MakeDialParameters
   113  	// in GetTactics.
   114  	resolver := NewResolver(config, false)
   115  	defer resolver.Stop()
   116  	config.SetResolver(resolver)
   117  
   118  	// Get tactics, may update client parameters
   119  	p := config.GetParameters().Get()
   120  	timeout := p.Duration(parameters.FeedbackTacticsWaitPeriod)
   121  	p.Close()
   122  	getTacticsCtx, cancelFunc := context.WithTimeout(ctx, timeout)
   123  	defer cancelFunc()
   124  	// Note: GetTactics will fail silently if the datastore used for retrieving
   125  	// and storing tactics is opened by another process.
   126  	GetTactics(getTacticsCtx, config)
   127  
   128  	// Get the latest client parameters
   129  	p = config.GetParameters().Get()
   130  	feedbackUploadMinRetryDelay := p.Duration(parameters.FeedbackUploadRetryMinDelaySeconds)
   131  	feedbackUploadMaxRetryDelay := p.Duration(parameters.FeedbackUploadRetryMaxDelaySeconds)
   132  	feedbackUploadTimeout := p.Duration(parameters.FeedbackUploadTimeoutSeconds)
   133  	feedbackUploadMaxAttempts := p.Int(parameters.FeedbackUploadMaxAttempts)
   134  	transferURLs := p.TransferURLs(parameters.FeedbackUploadURLs)
   135  	p.Close()
   136  
   137  	// Initialize the feedback upload dial configuration. config.DeviceBinder
   138  	// is not applied; see resolver comment above.
   139  	untunneledDialConfig := &DialConfig{
   140  		UpstreamProxyURL: config.UpstreamProxyURL,
   141  		CustomHeaders:    config.CustomHeaders,
   142  		DeviceBinder:     nil,
   143  		IPv6Synthesizer:  config.IPv6Synthesizer,
   144  		ResolveIP: func(ctx context.Context, hostname string) ([]net.IP, error) {
   145  			IPs, err := UntunneledResolveIP(
   146  				ctx, config, resolver, hostname)
   147  			if err != nil {
   148  				return nil, errors.Trace(err)
   149  			}
   150  			return IPs, nil
   151  		},
   152  		TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
   153  	}
   154  
   155  	uploadId := prng.HexString(8)
   156  
   157  	for i := 0; i < feedbackUploadMaxAttempts; i++ {
   158  
   159  		uploadURL := transferURLs.Select(i)
   160  		if uploadURL == nil {
   161  			return errors.TraceNew("error no feedback upload URL selected")
   162  		}
   163  
   164  		b64PublicKey := uploadURL.B64EncodedPublicKey
   165  		if b64PublicKey == "" {
   166  			if config.FeedbackEncryptionPublicKey == "" {
   167  				return errors.TraceNew("error no default encryption key")
   168  			}
   169  			b64PublicKey = config.FeedbackEncryptionPublicKey
   170  		}
   171  
   172  		secureFeedback, err := encryptFeedback(diagnostics, b64PublicKey)
   173  		if err != nil {
   174  			return errors.Trace(err)
   175  		}
   176  
   177  		feedbackUploadCtx, cancelFunc := context.WithTimeout(
   178  			ctx,
   179  			feedbackUploadTimeout)
   180  		defer cancelFunc()
   181  
   182  		client, err := MakeUntunneledHTTPClient(
   183  			feedbackUploadCtx,
   184  			config,
   185  			untunneledDialConfig,
   186  			uploadURL.SkipVerify)
   187  		if err != nil {
   188  			return errors.Trace(err)
   189  		}
   190  
   191  		parsedURL, err := url.Parse(uploadURL.URL)
   192  		if err != nil {
   193  			return errors.TraceMsg(err, "failed to parse feedback upload URL")
   194  		}
   195  
   196  		parsedURL.Path = path.Join(parsedURL.Path, uploadPath, uploadId)
   197  
   198  		request, err := http.NewRequestWithContext(feedbackUploadCtx, "PUT", parsedURL.String(), bytes.NewBuffer(secureFeedback))
   199  		if err != nil {
   200  			return errors.Trace(err)
   201  		}
   202  
   203  		for k, v := range uploadURL.RequestHeaders {
   204  			request.Header.Set(k, v)
   205  		}
   206  		request.Header.Set("User-Agent", MakePsiphonUserAgent(config))
   207  
   208  		err = uploadFeedback(client, request)
   209  		cancelFunc()
   210  		if err != nil {
   211  			if ctx.Err() != nil {
   212  				// Input context has completed
   213  				return errors.TraceMsg(err,
   214  					fmt.Sprintf("feedback upload attempt %d/%d cancelled", i+1, feedbackUploadMaxAttempts))
   215  			}
   216  			// Do not sleep after the last attempt
   217  			if i+1 < feedbackUploadMaxAttempts {
   218  				// Log error, sleep and then retry
   219  				timeUntilRetry := prng.Period(feedbackUploadMinRetryDelay, feedbackUploadMaxRetryDelay)
   220  				NoticeWarning(
   221  					"feedback upload attempt %d/%d failed (retry in %.0fs): %s",
   222  					i+1, feedbackUploadMaxAttempts, timeUntilRetry.Seconds(), errors.Trace(err))
   223  				select {
   224  				case <-ctx.Done():
   225  					return errors.TraceNew(
   226  						fmt.Sprintf("feedback upload attempt %d/%d cancelled before attempt",
   227  							i+2, feedbackUploadMaxAttempts))
   228  				case <-time.After(timeUntilRetry):
   229  				}
   230  				continue
   231  			}
   232  			return errors.TraceMsg(err,
   233  				fmt.Sprintf("feedback upload failed after %d attempts", i+1))
   234  		}
   235  		return nil
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  // Attempt to upload feedback data to server.
   242  func uploadFeedback(
   243  	client *http.Client, req *http.Request) error {
   244  
   245  	resp, err := client.Do(req)
   246  	if err != nil {
   247  		return errors.Trace(err)
   248  	}
   249  	defer resp.Body.Close()
   250  
   251  	if resp.StatusCode != http.StatusOK {
   252  		return errors.TraceNew("unexpected HTTP status: " + resp.Status)
   253  	}
   254  
   255  	return nil
   256  }
   257  
   258  // Pad src to the next block boundary with PKCS7 padding
   259  // (https://tools.ietf.org/html/rfc5652#section-6.3).
   260  func addPKCS7Padding(src []byte, blockSize int) []byte {
   261  	paddingLen := blockSize - (len(src) % blockSize)
   262  	padding := bytes.Repeat([]byte{byte(paddingLen)}, paddingLen)
   263  	return append(src, padding...)
   264  }
   265  
   266  // Encrypt plaintext with AES in CBC mode.
   267  func encryptAESCBC(plaintext []byte) ([]byte, []byte, []byte, error) {
   268  	// CBC mode works on blocks so plaintexts need to be padded to the
   269  	// next whole block (https://tools.ietf.org/html/rfc5246#section-6.2.3.2).
   270  	plaintext = addPKCS7Padding(plaintext, aes.BlockSize)
   271  
   272  	ciphertext := make([]byte, len(plaintext))
   273  	iv, err := common.MakeSecureRandomBytes(aes.BlockSize)
   274  	if err != nil {
   275  		return nil, nil, nil, err
   276  	}
   277  
   278  	key, err := common.MakeSecureRandomBytes(aes.BlockSize)
   279  	if err != nil {
   280  		return nil, nil, nil, errors.Trace(err)
   281  	}
   282  
   283  	block, err := aes.NewCipher(key)
   284  	if err != nil {
   285  		return nil, nil, nil, errors.Trace(err)
   286  	}
   287  
   288  	mode := cipher.NewCBCEncrypter(block, iv)
   289  	mode.CryptBlocks(ciphertext, plaintext)
   290  
   291  	return iv, key, ciphertext, nil
   292  }
   293  
   294  // Encrypt plaintext with RSA public key.
   295  func encryptWithPublicKey(plaintext, publicKey []byte) ([]byte, error) {
   296  	parsedKey, err := x509.ParsePKIXPublicKey(publicKey)
   297  	if err != nil {
   298  		return nil, errors.Trace(err)
   299  	}
   300  	if rsaPubKey, ok := parsedKey.(*rsa.PublicKey); ok {
   301  		rsaEncryptOutput, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, rsaPubKey, plaintext, nil)
   302  		if err != nil {
   303  			return nil, errors.Trace(err)
   304  		}
   305  		return rsaEncryptOutput, nil
   306  	}
   307  	return nil, errors.TraceNew("feedback key is not an RSA public key")
   308  }
   309  
   310  // Generate HMAC for Encrypt-then-MAC paradigm.
   311  func generateHMAC(iv, plaintext []byte) ([]byte, []byte, error) {
   312  	key, err := common.MakeSecureRandomBytes(16)
   313  	if err != nil {
   314  		return nil, nil, err
   315  	}
   316  
   317  	mac := hmac.New(sha256.New, key)
   318  
   319  	mac.Write(iv)
   320  	mac.Write(plaintext)
   321  
   322  	digest := mac.Sum(nil)
   323  
   324  	return digest, key, nil
   325  }