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 := ®en.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 := ®en.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 }