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

     1  /*
     2   * Copyright (c) 2015, 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 obfuscator
    21  
    22  import (
    23  	"bytes"
    24  	"crypto/rc4"
    25  	"crypto/sha1"
    26  	"encoding/binary"
    27  	"io"
    28  
    29  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    30  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    31  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
    32  )
    33  
    34  const (
    35  	OBFUSCATE_SEED_LENGTH         = 16
    36  	OBFUSCATE_KEY_LENGTH          = 16
    37  	OBFUSCATE_HASH_ITERATIONS     = 6000
    38  	OBFUSCATE_MAX_PADDING         = 8192
    39  	OBFUSCATE_MAGIC_VALUE         = 0x0BF5CA7E
    40  	OBFUSCATE_CLIENT_TO_SERVER_IV = "client_to_server"
    41  	OBFUSCATE_SERVER_TO_CLIENT_IV = "server_to_client"
    42  )
    43  
    44  // Obfuscator implements the seed message, key derivation, and
    45  // stream ciphers for:
    46  // https://github.com/brl/obfuscated-openssh/blob/master/README.obfuscation
    47  //
    48  // Limitation: the RC4 cipher is vulnerable to ciphertext malleability and
    49  // the "magic" value provides only weak authentication due to its small
    50  // size. Increasing the size of the magic field will break compatibility
    51  // with legacy clients. New protocols and schemes should not use this
    52  // obfuscator.
    53  type Obfuscator struct {
    54  	seedMessage          []byte
    55  	paddingLength        int
    56  	clientToServerCipher *rc4.Cipher
    57  	serverToClientCipher *rc4.Cipher
    58  	paddingPRNGSeed      *prng.Seed
    59  	paddingPRNG          *prng.PRNG
    60  }
    61  
    62  // ObfuscatorConfig specifies an Obfuscator configuration.
    63  type ObfuscatorConfig struct {
    64  	Keyword         string
    65  	PaddingPRNGSeed *prng.Seed
    66  	MinPadding      *int
    67  	MaxPadding      *int
    68  
    69  	// SeedHistory and IrregularLogger are optional parameters used only by
    70  	// server obfuscators.
    71  
    72  	SeedHistory       *SeedHistory
    73  	StrictHistoryMode bool
    74  	IrregularLogger   func(clientIP string, err error, logFields common.LogFields)
    75  }
    76  
    77  // NewClientObfuscator creates a new Obfuscator, staging a seed message to be
    78  // sent to the server (by the caller) and initializing stream ciphers to
    79  // obfuscate data.
    80  //
    81  // ObfuscatorConfig.PaddingPRNGSeed allows for optional replay of the
    82  // obfuscator padding and must not be nil.
    83  func NewClientObfuscator(
    84  	config *ObfuscatorConfig) (obfuscator *Obfuscator, err error) {
    85  
    86  	if config.Keyword == "" {
    87  		return nil, errors.TraceNew("missing keyword")
    88  	}
    89  
    90  	if config.PaddingPRNGSeed == nil {
    91  		return nil, errors.TraceNew("missing padding seed")
    92  	}
    93  
    94  	paddingPRNG := prng.NewPRNGWithSeed(config.PaddingPRNGSeed)
    95  
    96  	obfuscatorSeed, err := common.MakeSecureRandomBytes(OBFUSCATE_SEED_LENGTH)
    97  	if err != nil {
    98  		return nil, errors.Trace(err)
    99  	}
   100  
   101  	clientToServerCipher, serverToClientCipher, err := initObfuscatorCiphers(config, obfuscatorSeed)
   102  	if err != nil {
   103  		return nil, errors.Trace(err)
   104  	}
   105  
   106  	// The first prng.SEED_LENGTH bytes of the initial obfuscator message
   107  	// padding field is used by the server as a seed for its obfuscator
   108  	// padding and other protocol attributes (directly and via
   109  	// GetDerivedPRNG). This allows for optional downstream replay of these
   110  	// protocol attributes. Accordingly, the minimum padding is set to at
   111  	// least prng.SEED_LENGTH.
   112  
   113  	minPadding := prng.SEED_LENGTH
   114  	if config.MinPadding != nil &&
   115  		*config.MinPadding >= prng.SEED_LENGTH &&
   116  		*config.MinPadding <= OBFUSCATE_MAX_PADDING {
   117  		minPadding = *config.MinPadding
   118  	}
   119  
   120  	maxPadding := OBFUSCATE_MAX_PADDING
   121  	if config.MaxPadding != nil &&
   122  		*config.MaxPadding >= prng.SEED_LENGTH &&
   123  		*config.MaxPadding <= OBFUSCATE_MAX_PADDING &&
   124  		*config.MaxPadding >= minPadding {
   125  		maxPadding = *config.MaxPadding
   126  	}
   127  
   128  	seedMessage, paddingLength, err := makeSeedMessage(
   129  		paddingPRNG, minPadding, maxPadding, obfuscatorSeed, clientToServerCipher)
   130  	if err != nil {
   131  		return nil, errors.Trace(err)
   132  	}
   133  
   134  	return &Obfuscator{
   135  		seedMessage:          seedMessage,
   136  		paddingLength:        paddingLength,
   137  		clientToServerCipher: clientToServerCipher,
   138  		serverToClientCipher: serverToClientCipher,
   139  		paddingPRNGSeed:      config.PaddingPRNGSeed,
   140  		paddingPRNG:          paddingPRNG}, nil
   141  }
   142  
   143  // NewServerObfuscator creates a new Obfuscator, reading a seed message directly
   144  // from the clientReader and initializing stream ciphers to obfuscate data.
   145  //
   146  // ObfuscatorConfig.PaddingPRNGSeed is not used, as the server obtains a PRNG
   147  // seed from the client's initial obfuscator message; this scheme allows for
   148  // optional replay of the downstream obfuscator padding.
   149  //
   150  // The clientIP value is used by the SeedHistory, which retains client IP values
   151  // for a short time. See SeedHistory documentation.
   152  func NewServerObfuscator(
   153  	config *ObfuscatorConfig, clientIP string, clientReader io.Reader) (obfuscator *Obfuscator, err error) {
   154  
   155  	if config.Keyword == "" {
   156  		return nil, errors.TraceNew("missing keyword")
   157  	}
   158  
   159  	clientToServerCipher, serverToClientCipher, paddingPRNGSeed, err := readSeedMessage(
   160  		config, clientIP, clientReader)
   161  	if err != nil {
   162  		return nil, errors.Trace(err)
   163  	}
   164  
   165  	return &Obfuscator{
   166  		paddingLength:        -1,
   167  		clientToServerCipher: clientToServerCipher,
   168  		serverToClientCipher: serverToClientCipher,
   169  		paddingPRNGSeed:      paddingPRNGSeed,
   170  		paddingPRNG:          prng.NewPRNGWithSeed(paddingPRNGSeed),
   171  	}, nil
   172  }
   173  
   174  // GetDerivedPRNG creates a new PRNG with a seed derived from the obfuscator
   175  // padding seed and distinguished by the salt, which should be a unique
   176  // identifier for each usage context.
   177  //
   178  // For NewServerObfuscator, the obfuscator padding seed is obtained from the
   179  // client, so derived PRNGs may be used to replay sequences post-initial
   180  // obfuscator message.
   181  func (obfuscator *Obfuscator) GetDerivedPRNG(salt string) (*prng.PRNG, error) {
   182  	return prng.NewPRNGWithSaltedSeed(obfuscator.paddingPRNGSeed, salt)
   183  }
   184  
   185  // GetPaddingLength returns the client seed message padding length. Only valid
   186  // for NewClientObfuscator.
   187  func (obfuscator *Obfuscator) GetPaddingLength() int {
   188  	return obfuscator.paddingLength
   189  }
   190  
   191  // SendSeedMessage returns the seed message created in NewObfuscatorClient,
   192  // removing the reference so that it may be garbage collected.
   193  func (obfuscator *Obfuscator) SendSeedMessage() []byte {
   194  	seedMessage := obfuscator.seedMessage
   195  	obfuscator.seedMessage = nil
   196  	return seedMessage
   197  }
   198  
   199  // ObfuscateClientToServer applies the client RC4 stream to the bytes in buffer.
   200  func (obfuscator *Obfuscator) ObfuscateClientToServer(buffer []byte) {
   201  	obfuscator.clientToServerCipher.XORKeyStream(buffer, buffer)
   202  }
   203  
   204  // ObfuscateServerToClient applies the server RC4 stream to the bytes in buffer.
   205  func (obfuscator *Obfuscator) ObfuscateServerToClient(buffer []byte) {
   206  	obfuscator.serverToClientCipher.XORKeyStream(buffer, buffer)
   207  }
   208  
   209  func initObfuscatorCiphers(
   210  	config *ObfuscatorConfig, obfuscatorSeed []byte) (*rc4.Cipher, *rc4.Cipher, error) {
   211  
   212  	clientToServerKey, err := deriveKey(obfuscatorSeed, []byte(config.Keyword), []byte(OBFUSCATE_CLIENT_TO_SERVER_IV))
   213  	if err != nil {
   214  		return nil, nil, errors.Trace(err)
   215  	}
   216  
   217  	serverToClientKey, err := deriveKey(obfuscatorSeed, []byte(config.Keyword), []byte(OBFUSCATE_SERVER_TO_CLIENT_IV))
   218  	if err != nil {
   219  		return nil, nil, errors.Trace(err)
   220  	}
   221  
   222  	clientToServerCipher, err := rc4.NewCipher(clientToServerKey)
   223  	if err != nil {
   224  		return nil, nil, errors.Trace(err)
   225  	}
   226  
   227  	serverToClientCipher, err := rc4.NewCipher(serverToClientKey)
   228  	if err != nil {
   229  		return nil, nil, errors.Trace(err)
   230  	}
   231  
   232  	return clientToServerCipher, serverToClientCipher, nil
   233  }
   234  
   235  func deriveKey(obfuscatorSeed, keyword, iv []byte) ([]byte, error) {
   236  	h := sha1.New()
   237  	h.Write(obfuscatorSeed)
   238  	h.Write(keyword)
   239  	h.Write(iv)
   240  	digest := h.Sum(nil)
   241  	for i := 0; i < OBFUSCATE_HASH_ITERATIONS; i++ {
   242  		h.Reset()
   243  		h.Write(digest)
   244  		digest = h.Sum(nil)
   245  	}
   246  	if len(digest) < OBFUSCATE_KEY_LENGTH {
   247  		return nil, errors.TraceNew("insufficient bytes for obfuscation key")
   248  	}
   249  	return digest[0:OBFUSCATE_KEY_LENGTH], nil
   250  }
   251  
   252  func makeSeedMessage(
   253  	paddingPRNG *prng.PRNG,
   254  	minPadding, maxPadding int,
   255  	obfuscatorSeed []byte,
   256  	clientToServerCipher *rc4.Cipher) ([]byte, int, error) {
   257  
   258  	padding := paddingPRNG.Padding(minPadding, maxPadding)
   259  	buffer := new(bytes.Buffer)
   260  	err := binary.Write(buffer, binary.BigEndian, obfuscatorSeed)
   261  	if err != nil {
   262  		return nil, 0, errors.Trace(err)
   263  	}
   264  	err = binary.Write(buffer, binary.BigEndian, uint32(OBFUSCATE_MAGIC_VALUE))
   265  	if err != nil {
   266  		return nil, 0, errors.Trace(err)
   267  	}
   268  	err = binary.Write(buffer, binary.BigEndian, uint32(len(padding)))
   269  	if err != nil {
   270  		return nil, 0, errors.Trace(err)
   271  	}
   272  	err = binary.Write(buffer, binary.BigEndian, padding)
   273  	if err != nil {
   274  		return nil, 0, errors.Trace(err)
   275  	}
   276  	seedMessage := buffer.Bytes()
   277  	clientToServerCipher.XORKeyStream(seedMessage[len(obfuscatorSeed):], seedMessage[len(obfuscatorSeed):])
   278  	return seedMessage, len(padding), nil
   279  }
   280  
   281  func readSeedMessage(
   282  	config *ObfuscatorConfig,
   283  	clientIP string,
   284  	clientReader io.Reader) (*rc4.Cipher, *rc4.Cipher, *prng.Seed, error) {
   285  
   286  	seed := make([]byte, OBFUSCATE_SEED_LENGTH)
   287  	_, err := io.ReadFull(clientReader, seed)
   288  	if err != nil {
   289  		return nil, nil, nil, errors.Trace(err)
   290  	}
   291  
   292  	// Irregular events that indicate an invalid client are logged via
   293  	// IrregularLogger. Note that event detection isn't infallible. For example,
   294  	// a man-in-the-middle may have manipulated the seed message sent by a valid
   295  	// client; or with a very small probability a valid client may generate a
   296  	// duplicate seed message.
   297  	//
   298  	// Another false positive case: a retired server IP may be recycled and
   299  	// deployed with a new obfuscation key; legitimate clients may still attempt
   300  	// to connect using the old obfuscation key; this case is partically
   301  	// mitigated by the server entry pruning mechanism.
   302  	//
   303  	// Network I/O failures (e.g., failure to read the expected number of seed
   304  	// message bytes) are not considered a reliable indicator of irregular
   305  	// events.
   306  
   307  	// To distinguish different cases, irregular tunnel logs should indicate
   308  	// which function called NewServerObfuscator.
   309  	errBackTrace := "obfuscator.NewServerObfuscator"
   310  
   311  	if config.SeedHistory != nil {
   312  		ok, duplicateLogFields := config.SeedHistory.AddNew(
   313  			config.StrictHistoryMode, clientIP, "obfuscator-seed", seed)
   314  		errStr := "duplicate obfuscation seed"
   315  		if duplicateLogFields != nil {
   316  			if config.IrregularLogger != nil {
   317  				config.IrregularLogger(
   318  					clientIP,
   319  					errors.BackTraceNew(errBackTrace, errStr),
   320  					*duplicateLogFields)
   321  			}
   322  		}
   323  		if !ok {
   324  			return nil, nil, nil, errors.TraceNew(errStr)
   325  		}
   326  	}
   327  
   328  	clientToServerCipher, serverToClientCipher, err := initObfuscatorCiphers(config, seed)
   329  	if err != nil {
   330  		return nil, nil, nil, errors.Trace(err)
   331  	}
   332  
   333  	fixedLengthFields := make([]byte, 8) // 4 bytes each for magic value and padding length
   334  	_, err = io.ReadFull(clientReader, fixedLengthFields)
   335  	if err != nil {
   336  		return nil, nil, nil, errors.Trace(err)
   337  	}
   338  
   339  	clientToServerCipher.XORKeyStream(fixedLengthFields, fixedLengthFields)
   340  
   341  	buffer := bytes.NewReader(fixedLengthFields)
   342  
   343  	// The magic value must be validated before acting on paddingLength as
   344  	// paddingLength validation is vulnerable to a chosen ciphertext probing
   345  	// attack: only a fixed number of any possible byte value for each
   346  	// paddingLength is valid.
   347  
   348  	var magicValue, paddingLength int32
   349  	err = binary.Read(buffer, binary.BigEndian, &magicValue)
   350  	if err != nil {
   351  		return nil, nil, nil, errors.Trace(err)
   352  	}
   353  	err = binary.Read(buffer, binary.BigEndian, &paddingLength)
   354  	if err != nil {
   355  		return nil, nil, nil, errors.Trace(err)
   356  	}
   357  
   358  	errStr := ""
   359  
   360  	if magicValue != OBFUSCATE_MAGIC_VALUE {
   361  		errStr = "invalid magic value"
   362  	}
   363  
   364  	if errStr == "" && (paddingLength < 0 || paddingLength > OBFUSCATE_MAX_PADDING) {
   365  		errStr = "invalid padding length"
   366  	}
   367  
   368  	if errStr != "" {
   369  		if config.IrregularLogger != nil {
   370  			config.IrregularLogger(
   371  				clientIP,
   372  				errors.BackTraceNew(errBackTrace, errStr),
   373  				nil)
   374  		}
   375  		return nil, nil, nil, errors.TraceNew(errStr)
   376  	}
   377  
   378  	padding := make([]byte, paddingLength)
   379  	_, err = io.ReadFull(clientReader, padding)
   380  	if err != nil {
   381  		return nil, nil, nil, errors.Trace(err)
   382  	}
   383  
   384  	clientToServerCipher.XORKeyStream(padding, padding)
   385  
   386  	// Use the first prng.SEED_LENGTH bytes of padding as a PRNG seed for
   387  	// subsequent operations. This allows the client to direct server-side
   388  	// replay of certain protocol attributes.
   389  	//
   390  	// Since legacy clients may send < prng.SEED_LENGTH bytes of padding,
   391  	// generate a new seed in that case.
   392  
   393  	var paddingPRNGSeed *prng.Seed
   394  
   395  	if len(padding) >= prng.SEED_LENGTH {
   396  		paddingPRNGSeed = new(prng.Seed)
   397  		copy(paddingPRNGSeed[:], padding[0:prng.SEED_LENGTH])
   398  	} else {
   399  		paddingPRNGSeed, err = prng.NewSeed()
   400  		if err != nil {
   401  			return nil, nil, nil, errors.Trace(err)
   402  		}
   403  	}
   404  
   405  	return clientToServerCipher, serverToClientCipher, paddingPRNGSeed, nil
   406  }