github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/values/values.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  /*
    21  
    22  Package values provides a mechanism for specifying and selecting dynamic
    23  values employed by the Psiphon client and server.
    24  
    25  */
    26  package values
    27  
    28  import (
    29  	"bytes"
    30  	"encoding/gob"
    31  	"fmt"
    32  	"regexp/syntax"
    33  	"strings"
    34  	"sync"
    35  	"sync/atomic"
    36  
    37  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    38  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
    39  	regen "github.com/zach-klippenstein/goregen"
    40  	"golang.org/x/crypto/nacl/secretbox"
    41  )
    42  
    43  // ValueSpec specifies a value selection space.
    44  type ValueSpec struct {
    45  	Probability float64
    46  	Parts       []PartSpec
    47  	Padding     []byte
    48  }
    49  
    50  type PartSpec struct {
    51  	Items              []string
    52  	MinCount, MaxCount int
    53  }
    54  
    55  // NewPickOneSpec creates a simple spec to select one item from a list as a
    56  // value.
    57  func NewPickOneSpec(items []string) *ValueSpec {
    58  	return &ValueSpec{
    59  		Probability: 1.0,
    60  		Parts:       []PartSpec{{Items: items, MinCount: 1, MaxCount: 1}},
    61  	}
    62  }
    63  
    64  // GetValue selects a value according to the spec. An optional seed may
    65  // be specified to support replay.
    66  func (spec *ValueSpec) GetValue(PRNG *prng.PRNG) string {
    67  	rangeFunc := prng.Range
    68  	intnFunc := prng.Intn
    69  	if PRNG != nil {
    70  		rangeFunc = PRNG.Range
    71  		intnFunc = PRNG.Intn
    72  	}
    73  	var value strings.Builder
    74  	for _, part := range spec.Parts {
    75  		count := rangeFunc(part.MinCount, part.MaxCount)
    76  		for i := 0; i < count; i++ {
    77  			value.WriteString(part.Items[intnFunc(len(part.Items))])
    78  		}
    79  	}
    80  	return value.String()
    81  }
    82  
    83  // Obfuscate creates an obfuscated blob from a spec.
    84  func (spec *ValueSpec) Obfuscate(
    85  	obfuscationKey []byte,
    86  	minPadding, maxPadding int) ([]byte, error) {
    87  
    88  	if len(obfuscationKey) != 32 {
    89  		return nil, errors.TraceNew("invalid key length")
    90  	}
    91  	var key [32]byte
    92  	copy(key[:], []byte(obfuscationKey))
    93  
    94  	spec.Padding = prng.Padding(minPadding, maxPadding)
    95  
    96  	var obfuscatedValueSpec bytes.Buffer
    97  	err := gob.NewEncoder(&obfuscatedValueSpec).Encode(spec)
    98  	if err != nil {
    99  		return nil, errors.Trace(err)
   100  	}
   101  
   102  	return secretbox.Seal(
   103  		nil, []byte(obfuscatedValueSpec.Bytes()), &[24]byte{}, &key), nil
   104  }
   105  
   106  // DeobfuscateValueSpec reconstitutes an obfuscated spec.
   107  func DeobfuscateValueSpec(obfuscatedValueSpec, obfuscationKey []byte) *ValueSpec {
   108  
   109  	if len(obfuscationKey) != 32 {
   110  		return nil
   111  	}
   112  	var key [32]byte
   113  	copy(key[:], obfuscationKey)
   114  
   115  	deobfuscatedValueSpec, ok := secretbox.Open(nil, obfuscatedValueSpec, &[24]byte{}, &key)
   116  	if !ok {
   117  		return nil
   118  	}
   119  
   120  	spec := new(ValueSpec)
   121  	err := gob.NewDecoder(bytes.NewBuffer(deobfuscatedValueSpec)).Decode(spec)
   122  	if err != nil {
   123  		return nil
   124  	}
   125  	spec.Padding = nil
   126  
   127  	return spec
   128  }
   129  
   130  var (
   131  	revision              atomic.Value
   132  	sshClientVersionsSpec atomic.Value
   133  	sshServerVersionsSpec atomic.Value
   134  	userAgentsSpec        atomic.Value
   135  	hostNamesSpec         atomic.Value
   136  	cookieNamesSpec       atomic.Value
   137  	contentTypeSpec       atomic.Value
   138  )
   139  
   140  // SetRevision set the revision value, which may be used to track which value
   141  // specs are active. The revision is not managed by this package and must be
   142  // set by the package user.
   143  func SetRevision(rev string) {
   144  	revision.Store(rev)
   145  }
   146  
   147  // GetRevision gets the previously set revision.
   148  func GetRevision() string {
   149  	rev, ok := revision.Load().(string)
   150  	if !ok {
   151  		return "none"
   152  	}
   153  	return rev
   154  }
   155  
   156  // SetSSHClientVersionsSpec sets the corresponding value spec.
   157  func SetSSHClientVersionsSpec(spec *ValueSpec) {
   158  	if spec == nil {
   159  		return
   160  	}
   161  	sshClientVersionsSpec.Store(spec)
   162  }
   163  
   164  // GetSSHClientVersion selects a value based on the previously set spec, or
   165  // returns a default when no spec is set.
   166  func GetSSHClientVersion() string {
   167  	spec, ok := sshClientVersionsSpec.Load().(*ValueSpec)
   168  	if !ok || !prng.FlipWeightedCoin(spec.Probability) {
   169  		return generate(prng.DefaultPRNG(), "SSH-2\\.0-OpenSSH_[7-8]\\.[0-9]")
   170  	}
   171  	return spec.GetValue(nil)
   172  }
   173  
   174  // SetSSHServerVersionsSpec sets the corresponding value spec.
   175  func SetSSHServerVersionsSpec(spec *ValueSpec) {
   176  	if spec == nil {
   177  		return
   178  	}
   179  	sshServerVersionsSpec.Store(spec)
   180  }
   181  
   182  // GetSSHServerVersion selects a value based on the previously set spec, or
   183  // returns a default when no spec is set.
   184  func GetSSHServerVersion(seed *prng.Seed) string {
   185  	var PRNG *prng.PRNG
   186  	if seed != nil {
   187  		PRNG = prng.NewPRNGWithSeed(seed)
   188  	}
   189  	spec, ok := sshServerVersionsSpec.Load().(*ValueSpec)
   190  	if !ok || !PRNG.FlipWeightedCoin(spec.Probability) {
   191  		return generate(PRNG, "SSH-2\\.0-OpenSSH_[7-8]\\.[0-9]")
   192  	}
   193  	return spec.GetValue(PRNG)
   194  }
   195  
   196  // SetUserAgentsSpec sets the corresponding value spec.
   197  func SetUserAgentsSpec(spec *ValueSpec) {
   198  	if spec == nil {
   199  		return
   200  	}
   201  	userAgentsSpec.Store(spec)
   202  }
   203  
   204  // GetUserAgent selects a value based on the previously set spec, or
   205  // returns a default when no spec is set.
   206  func GetUserAgent() string {
   207  	spec, ok := userAgentsSpec.Load().(*ValueSpec)
   208  	if !ok || !prng.FlipWeightedCoin(spec.Probability) {
   209  		return generateUserAgent()
   210  	}
   211  	return spec.GetValue(nil)
   212  }
   213  
   214  // SetHostNamesSpec sets the corresponding value spec.
   215  func SetHostNamesSpec(spec *ValueSpec) {
   216  	if spec == nil {
   217  		return
   218  	}
   219  	hostNamesSpec.Store(spec)
   220  }
   221  
   222  // GetHostName selects a value based on the previously set spec, or
   223  // returns a default when no spec is set.
   224  func GetHostName() string {
   225  	spec, ok := hostNamesSpec.Load().(*ValueSpec)
   226  	if !ok || !prng.FlipWeightedCoin(spec.Probability) {
   227  		return generate(prng.DefaultPRNG(), "[a-z]{4,15}(\\.com|\\.net|\\.org)")
   228  	}
   229  	return spec.GetValue(nil)
   230  }
   231  
   232  // SetCookieNamesSpec sets the corresponding value spec.
   233  func SetCookieNamesSpec(spec *ValueSpec) {
   234  	if spec == nil {
   235  		return
   236  	}
   237  	cookieNamesSpec.Store(spec)
   238  }
   239  
   240  // GetCookieName selects a value based on the previously set spec, or
   241  // returns a default when no spec is set.
   242  func GetCookieName(PRNG *prng.PRNG) string {
   243  	spec, ok := cookieNamesSpec.Load().(*ValueSpec)
   244  	if !ok || !PRNG.FlipWeightedCoin(spec.Probability) {
   245  		return generate(PRNG, "[a-z_]{2,10}")
   246  	}
   247  	return spec.GetValue(PRNG)
   248  }
   249  
   250  // SetContentTypesSpec sets the corresponding value spec.
   251  func SetContentTypesSpec(spec *ValueSpec) {
   252  	if spec == nil {
   253  		return
   254  	}
   255  	contentTypeSpec.Store(spec)
   256  }
   257  
   258  // GetContentType selects a value based on the previously set spec, or
   259  // returns a default when no spec is set.
   260  func GetContentType(PRNG *prng.PRNG) string {
   261  	spec, ok := contentTypeSpec.Load().(*ValueSpec)
   262  	if !ok || !PRNG.FlipWeightedCoin(spec.Probability) {
   263  		return generate(PRNG, "application/octet-stream|audio/mpeg|image/jpeg|video/mpeg")
   264  	}
   265  	return spec.GetValue(PRNG)
   266  }
   267  
   268  func generate(PRNG *prng.PRNG, pattern string) string {
   269  
   270  	args := &regen.GeneratorArgs{
   271  		RngSource: PRNG,
   272  		Flags:     syntax.OneLine | syntax.NonGreedy,
   273  	}
   274  	rg, err := regen.NewGenerator(pattern, args)
   275  	if err != nil {
   276  		panic(err.Error())
   277  	}
   278  	return rg.Generate()
   279  }
   280  
   281  var (
   282  	userAgentGeneratorMutex sync.Mutex
   283  	userAgentGenerators     []*userAgentGenerator
   284  )
   285  
   286  type userAgentGenerator struct {
   287  	version   func() string
   288  	generator regen.Generator
   289  }
   290  
   291  func generateUserAgent() string {
   292  
   293  	userAgentGeneratorMutex.Lock()
   294  	defer userAgentGeneratorMutex.Unlock()
   295  
   296  	if userAgentGenerators == nil {
   297  
   298  		// Initialize user agent generators once and reuse. This saves the
   299  		// overhead of parsing the relatively complex regular expressions on
   300  		// each GetUserAgent call.
   301  
   302  		// These regular expressions and version ranges are adapted from:
   303  		//
   304  		// https://github.com/tarampampam/random-user-agent/blob/d0dd4059ac518e8b0f79510d050877c685539fbc/src/useragent/generator.ts
   305  		// https://github.com/tarampampam/random-user-agent/blob/d0dd4059ac518e8b0f79510d050877c685539fbc/src/useragent/versions.ts
   306  
   307  		chromeVersion := func() string {
   308  			return fmt.Sprintf("%d.0.%d.%d",
   309  				prng.Range(101, 104), prng.Range(4951, 5162), prng.Range(80, 212))
   310  		}
   311  
   312  		safariVersion := func() string {
   313  			return fmt.Sprintf("%d.%d.%d",
   314  				prng.Range(537, 611), prng.Range(1, 36), prng.Range(1, 15))
   315  		}
   316  
   317  		makeGenerator := func(pattern string) regen.Generator {
   318  			args := &regen.GeneratorArgs{
   319  				RngSource: prng.DefaultPRNG(),
   320  				Flags:     syntax.OneLine | syntax.NonGreedy,
   321  			}
   322  			rg, err := regen.NewGenerator(pattern, args)
   323  			if err != nil {
   324  				panic(err.Error())
   325  			}
   326  			return rg
   327  		}
   328  
   329  		userAgentGenerators = []*userAgentGenerator{
   330  			&userAgentGenerator{chromeVersion, makeGenerator("Mozilla/5\\.0 \\(Macintosh; Intel Mac OS X 1[01]_(1|)[0-5]\\) AppleWebKit/537\\.36 \\(KHTML, like Gecko\\) Chrome/__VER__ Safari/537\\.36")},
   331  			&userAgentGenerator{chromeVersion, makeGenerator("Mozilla/5\\.0 \\(Windows NT 1(0|0|1)\\.0; (WOW64|Win64)(; x64|; x64|)\\) AppleWebKit/537\\.36 \\(KHTML, like Gecko\\) Chrome/__VER__ Safari/537\\.36")},
   332  			&userAgentGenerator{chromeVersion, makeGenerator("Mozilla/5\\.0 \\(Linux; Android (9|10|10|11|12); [a-zA-Z0-9_]{5,10}\\) AppleWebKit/537\\.36 \\(KHTML, like Gecko\\) Chrome/__VER__ Mobile Safari/537\\.36")},
   333  			&userAgentGenerator{safariVersion, makeGenerator("Mozilla/5\\.0 \\(iPhone; CPU iPhone OS 1[3-5]_[1-5] like Mac OS X\\) AppleWebKit/(__VER__|__VER__|600\\.[1-8]\\.[12][0-7]|537\\.36) \\(KHTML, like Gecko\\) Version/1[0-4]\\.[0-7](\\.[1-9][0-7]|) Mobile/[A-Z0-9]{6} Safari/__VER__")},
   334  			&userAgentGenerator{safariVersion, makeGenerator("Mozilla/5\\.0 \\(Macintosh; Intel Mac OS X 1[01]_(1|)[0-7](_[1-7]|)\\) AppleWebKit/(__VER__|__VER__|600\\.[1-8]\\.[12][0-7]|537\\.36) \\(KHTML, like Gecko\\) Version/1[0-4]\\.[0-7](\\.[1-9][0-7]|) Safari/__VER__")},
   335  		}
   336  	}
   337  
   338  	g := userAgentGenerators[prng.Range(0, len(userAgentGenerators)-1)]
   339  
   340  	value := g.generator.Generate()
   341  	value = strings.ReplaceAll(value, "__VER__", g.version())
   342  	return value
   343  }