Skip to content

Commit 7243d60

Browse files
authored
allow decrypting net-messages via ParserConfig.NetMessageDecryptionKey (#323)
1 parent 07c1a10 commit 7243d60

File tree

10 files changed

+180
-34
lines changed

10 files changed

+180
-34
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ require (
88
github.com/markus-wa/go-unassert v0.1.2
99
github.com/markus-wa/gobitread v0.2.3
1010
github.com/markus-wa/godispatch v1.4.1
11+
github.com/markus-wa/ice-cipher-go v0.0.0-20220126215401-a6adadccc817
1112
github.com/markus-wa/quickhull-go/v2 v2.1.0
1213
github.com/pkg/errors v0.9.1
13-
github.com/stretchr/testify v1.6.1
14+
github.com/stretchr/testify v1.7.0
1415
)
1516

1617
replace github.com/dustin/go-heatmap => github.com/markus-wa/go-heatmap v1.0.0
1718

19+
replace github.com/stretchr/testify => github.com/stretchr/testify v1.6.1
20+
1821
go 1.11

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ github.com/markus-wa/gobitread v0.2.3 h1:COx7dtYQ7Q+77hgUmD+O4MvOcqG7y17RP3Z7Bbj
2626
github.com/markus-wa/gobitread v0.2.3/go.mod h1:PcWXMH4gx7o2CKslbkFkLyJB/aHW7JVRG3MRZe3PINg=
2727
github.com/markus-wa/godispatch v1.4.1 h1:Cdff5x33ShuX3sDmUbYWejk7tOuoHErFYMhUc2h7sLc=
2828
github.com/markus-wa/godispatch v1.4.1/go.mod h1:tk8L0yzLO4oAcFwM2sABMge0HRDJMdE8E7xm4gK/+xM=
29+
github.com/markus-wa/ice-cipher-go v0.0.0-20220126215401-a6adadccc817 h1:VB4ANo078mbGQNBgauLOzJkoHpIqrMygbPHVW0jjql0=
30+
github.com/markus-wa/ice-cipher-go v0.0.0-20220126215401-a6adadccc817/go.mod h1:JIsht5Oa9P50VnGJTvH2a6nkOqDFJbUeU1YRZYvdplw=
2931
github.com/markus-wa/quickhull-go/v2 v2.1.0 h1:DA2pzEzH0k5CEnlUsouRqNdD+jzNFb4DBhrX4Hpa5So=
3032
github.com/markus-wa/quickhull-go/v2 v2.1.0/go.mod h1:bOlBUpIzGSMMhHX0f9N8CQs0VZD4nnPeta0OocH7m4o=
3133
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -34,7 +36,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
3436
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3537
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
3638
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
37-
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
3839
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
3940
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
4041
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -68,6 +69,5 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
6869
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
6970
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
7071
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
71-
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
7272
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
7373
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

pkg/demoinfocs/demoinfocs_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,43 @@ func TestDemoInfoCs(t *testing.T) {
194194
assertGolden(t, assertions, "default", actual.Bytes())
195195
}
196196

197+
func TestEncryptedNetMessages(t *testing.T) {
198+
t.Parallel()
199+
200+
if testing.Short() {
201+
t.Skip("skipping test due to -short flag")
202+
}
203+
204+
infoF, err := os.Open(csDemosPath + "/match730_003528806449641685104_1453182610_271.dem.info")
205+
206+
b, err := ioutil.ReadAll(infoF)
207+
assert.NoError(t, err)
208+
209+
k, err := demoinfocs.MatchInfoDecryptionKey(b)
210+
assert.NoError(t, err)
211+
212+
f, err := os.Open(csDemosPath + "/match730_003528806449641685104_1453182610_271.dem")
213+
assert.NoError(t, err)
214+
defer mustClose(t, f)
215+
216+
cfg := demoinfocs.DefaultParserConfig
217+
cfg.NetMessageDecryptionKey = k
218+
219+
p := demoinfocs.NewParserWithConfig(f, cfg)
220+
221+
p.RegisterEventHandler(func(message events.ChatMessage) {
222+
t.Log(message)
223+
})
224+
225+
err = p.ParseToEnd()
226+
assert.NoError(t, err)
227+
}
228+
229+
func TestMatchInfoDecryptionKey_Error(t *testing.T) {
230+
_, err := demoinfocs.MatchInfoDecryptionKey([]byte{0})
231+
assert.Error(t, err)
232+
}
233+
197234
func TestRetake_BadBombsiteIndex(t *testing.T) {
198235
t.Parallel()
199236

pkg/demoinfocs/events/events.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@ const (
522522
WarnTypeBombsiteUnknown // may occur on de_grind for bombsite B as the bounding box of the bombsite is wrong
523523
WarnTypeTeamSwapPlayerNil // TODO: figure out why this happens
524524
WarnTypeGameEventBeforeDescriptors // may occur in POV demos
525+
526+
// WarnTypeMissingNetMessageDecryptionKey occurs when encrypted net-messages are encountered and the decryption key is missing
527+
// See ParserConfig.NetMessageDecryptionKey
528+
WarnTypeMissingNetMessageDecryptionKey
525529
)
526530

527531
// ParserWarn signals that a non-fatal problem occurred during parsing.

pkg/demoinfocs/matchinfo.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package demoinfocs
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
7+
"github.com/gogo/protobuf/proto"
8+
"github.com/pkg/errors"
9+
10+
"github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/msg"
11+
)
12+
13+
// MatchInfoDecryptionKey extracts the net-message decryption key stored in `match730_*.dem.info`.
14+
// Pass the whole contents of `match730_*.dem.info` to this function to get the key.
15+
func MatchInfoDecryptionKey(b []byte) ([]byte, error) {
16+
m := new(msg.CDataGCCStrike15V2_MatchInfo)
17+
18+
err := proto.Unmarshal(b, m)
19+
if err != nil {
20+
return nil, errors.Wrap(err, "failed to unmarshal MatchInfo message")
21+
}
22+
23+
k := []byte(strings.ToUpper(strconv.FormatUint(m.Watchablematchinfo.ClDecryptdataKeyPub, 16)))
24+
25+
return k, nil
26+
}

pkg/demoinfocs/net_messages.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ package demoinfocs
22

33
import (
44
"bytes"
5+
"encoding/binary"
6+
7+
"github.com/gogo/protobuf/proto"
8+
"github.com/markus-wa/go-unassert"
9+
"github.com/markus-wa/ice-cipher-go/pkg/ice"
510

611
bit "github.com/markus-wa/demoinfocs-golang/v2/internal/bitread"
712
events "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/events"
@@ -71,3 +76,56 @@ func (p *parser) handleServerInfo(srvInfo *msg.CSVCMsg_ServerInfo) {
7176
TickTime: p.TickTime(),
7277
})
7378
}
79+
80+
func (p *parser) handleEncryptedData(msg *msg.CSVCMsg_EncryptedData) {
81+
if msg.KeyType != 2 {
82+
return
83+
}
84+
85+
if p.decryptionKey == nil {
86+
p.msgDispatcher.Dispatch(events.ParserWarn{
87+
Type: events.WarnTypeMissingNetMessageDecryptionKey,
88+
Message: "received encrypted net-message but no decryption key is set",
89+
})
90+
91+
return
92+
}
93+
94+
k := ice.NewKey(2, p.decryptionKey)
95+
b := k.DecryptAll(msg.Encrypted)
96+
97+
r := bytes.NewReader(b)
98+
br := bit.NewSmallBitReader(r)
99+
100+
paddingBytes := br.ReadSingleByte()
101+
br.Skip(int(paddingBytes) << 3)
102+
103+
bBytesWritten := br.ReadBytes(4)
104+
nBytesWritten := int(binary.BigEndian.Uint32(bBytesWritten))
105+
106+
unassert.Same(len(b), 5+int(paddingBytes)+nBytesWritten)
107+
108+
cmd := br.ReadVarInt32()
109+
size := br.ReadVarInt32()
110+
111+
m := p.netMessageForCmd(int(cmd))
112+
113+
if m == nil {
114+
err := br.Pool()
115+
if err != nil {
116+
p.setError(err)
117+
}
118+
119+
return
120+
}
121+
122+
msgB := br.ReadBytes(int(size))
123+
err := proto.Unmarshal(msgB, m)
124+
if err != nil {
125+
p.setError(err)
126+
127+
return
128+
}
129+
130+
p.msgDispatcher.Dispatch(m)
131+
}

pkg/demoinfocs/parser.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type parser struct {
6363
demoInfoProvider demoInfoProvider // Provides demo infos to other packages that the core package depends on
6464
err error // Contains a error that occurred during parsing if any
6565
errLock sync.Mutex // Used to sync up error mutations between parsing & handling go-routines
66+
decryptionKey []byte // Stored in `match730_*.dem.info` see MatchInfoDecryptionKey().
6667

6768
// Additional fields, mainly caching & tracking things
6869

@@ -316,6 +317,10 @@ type ParserConfig struct {
316317
// IgnoreErrBombsiteIndexNotFound tells the parser to not return an error when a bombsite-index from a game-event is not found in the demo.
317318
// See https://github.com/markus-wa/demoinfocs-golang/issues/314
318319
IgnoreErrBombsiteIndexNotFound bool
320+
321+
// NetMessageDecryptionKey tells the parser how to decrypt certain encrypted net-messages.
322+
// See MatchInfoDecryptionKey() on how to retrieve the key from
323+
NetMessageDecryptionKey []byte
319324
}
320325

321326
// DefaultParserConfig is the default Parser configuration used by NewParser().
@@ -342,6 +347,7 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
342347
p.userMessageHandler = newUserMessageHandler(&p)
343348
p.bombsiteA.index = -1
344349
p.bombsiteB.index = -1
350+
p.decryptionKey = config.NetMessageDecryptionKey
345351

346352
dispatcherCfg := dp.Config{
347353
PanicHandler: func(v interface{}) {
@@ -361,6 +367,7 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
361367
p.msgDispatcher.RegisterHandler(p.handleSetConVar)
362368
p.msgDispatcher.RegisterHandler(p.handleFrameParsed)
363369
p.msgDispatcher.RegisterHandler(p.handleServerInfo)
370+
p.msgDispatcher.RegisterHandler(p.handleEncryptedData)
364371
p.msgDispatcher.RegisterHandler(p.gameState.handleIngameTickNumber)
365372

366373
if config.MsgQueueBufferSize >= 0 {

pkg/demoinfocs/parsing.go

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,39 @@ var defaultNetMessageCreators = map[int]NetMessageCreator{
292292
int(msg.SVC_Messages_svc_UserMessage): func() proto.Message { return new(msg.CSVCMsg_UserMessage) },
293293
int(msg.SVC_Messages_svc_ServerInfo): func() proto.Message { return new(msg.CSVCMsg_ServerInfo) },
294294
int(msg.NET_Messages_net_SetConVar): func() proto.Message { return new(msg.CNETMsg_SetConVar) },
295+
int(msg.SVC_Messages_svc_EncryptedData): func() proto.Message { return new(msg.CSVCMsg_EncryptedData) },
296+
}
297+
298+
func (p *parser) netMessageForCmd(cmd int) proto.Message {
299+
msgCreator := defaultNetMessageCreators[cmd]
300+
301+
if msgCreator != nil {
302+
return msgCreator()
303+
}
304+
305+
var msgName string
306+
if cmd < 8 || cmd >= 100 {
307+
msgName = msg.NET_Messages_name[int32(cmd)]
308+
} else {
309+
msgName = msg.SVC_Messages_name[int32(cmd)]
310+
}
311+
312+
if msgName == "" {
313+
// Send a warning if the command is unknown
314+
// This might mean our proto files are out of date
315+
p.eventDispatcher.Dispatch(events.ParserWarn{Message: fmt.Sprintf("unknown message command %q", cmd)})
316+
unassert.Error("unknown message command %q", cmd)
317+
}
318+
319+
// Handle additional net-messages as defined by the user
320+
msgCreator = p.additionalNetMessageCreators[cmd]
321+
if msgCreator != nil {
322+
return msgCreator()
323+
}
324+
325+
debugUnhandledMessage(cmd, msgName)
326+
327+
return nil
295328
}
296329

297330
//nolint:funlen
@@ -311,41 +344,19 @@ func (p *parser) parsePacket() {
311344

312345
p.bitReader.BeginChunk(size << 3)
313346

314-
msgCreator := defaultNetMessageCreators[cmd]
315-
316-
if msgCreator == nil {
317-
var msgName string
318-
if cmd < 8 || cmd >= 100 {
319-
msgName = msg.NET_Messages_name[int32(cmd)]
320-
} else {
321-
msgName = msg.SVC_Messages_name[int32(cmd)]
322-
}
323-
324-
if msgName == "" {
325-
// Send a warning if the command is unknown
326-
// This might mean our proto files are out of date
327-
p.eventDispatcher.Dispatch(events.ParserWarn{Message: fmt.Sprintf("unknown message command %q", cmd)})
328-
unassert.Error("unknown message command %q", cmd)
329-
}
330-
331-
// Handle additional net-messages as defined by the user
332-
msgCreator = p.additionalNetMessageCreators[cmd]
333-
if msgCreator == nil {
334-
debugUnhandledMessage(cmd, msgName)
335-
336-
// On to the next one
337-
p.bitReader.EndChunk()
338-
339-
continue
340-
}
347+
m := p.netMessageForCmd(cmd)
348+
349+
if m == nil {
350+
// On to the next one
351+
p.bitReader.EndChunk()
352+
353+
continue
341354
}
342355

343356
b := byteSlicePool.Get().(*[]byte)
344357

345358
p.bitReader.ReadBytesInto(b, size)
346359

347-
m := msgCreator()
348-
349360
err := proto.Unmarshal(*b, m)
350361
if err != nil {
351362
// TODO: Don't crash here, happens with demos that work in gotv

scripts/coverage.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
set -e
44

55
scripts_dir=$(dirname "$0")
6-
$scripts_dir/download-test-data.sh default.7z unexpected_end_of_demo.7z regression-set.7z retake_unknwon_bombsite_index.7z
6+
$scripts_dir/download-test-data.sh default.7z unexpected_end_of_demo.7z regression-set.7z retake_unknwon_bombsite_index.7z valve_matchmaking.7z
77

88
# don't cover mocks and generated protobuf code
99
coverpkg_ignore='/(fake|msg)'

test/cs-demos

Submodule cs-demos updated from 2be8467 to a335c3d

0 commit comments

Comments
 (0)