tisura 0.0.1-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +22 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +20 -0
- package/dist/connect/index.d.ts +1 -0
- package/dist/connect/index.js +6 -0
- package/dist/parse/db.d.ts +6 -0
- package/dist/parse/db.js +48 -0
- package/dist/parse/index.d.ts +7 -0
- package/dist/parse/index.js +23 -0
- package/dist/parse/parser.d.ts +55 -0
- package/dist/parse/parser.js +348 -0
- package/dist/parse/types.d.ts +40 -0
- package/dist/parse/types.js +2 -0
- package/dist/prove/index.d.ts +1 -0
- package/dist/prove/index.js +7 -0
- package/dist/tls/index.d.ts +7 -0
- package/dist/tls/index.js +51 -0
- package/jest.config.js +5 -0
- package/package.json +36 -0
- package/scripts/build-wasm.sh +17 -0
- package/src/client.ts +20 -0
- package/src/config.ts +28 -0
- package/src/connect/index.ts +3 -0
- package/src/parse/README.md +7 -0
- package/src/parse/db.ts +50 -0
- package/src/parse/index.ts +23 -0
- package/src/parse/parser.ts +416 -0
- package/src/parse/types.ts +45 -0
- package/src/prove/index.ts +4 -0
- package/src/tls/index.ts +17 -0
- package/src/tls/rust/Cargo.lock +371 -0
- package/src/tls/rust/Cargo.toml +21 -0
- package/src/tls/rust/src/lib.rs +514 -0
- package/tests/parse/db.test.ts +78 -0
- package/tests/parse/testData.ts +875 -0
- package/tsconfig.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Sentinel SDK
|
|
2
|
+
|
|
3
|
+
This project contains the code for [tisura](https://www.npmjs.com/package/tisura) npm package.
|
|
4
|
+
It provides modules for parsing network packets, proving statements, managing connections, and handling TLS operations.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install tisura
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Basic Setup
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Tisura } from 'tisura';
|
|
18
|
+
|
|
19
|
+
const client = new Tisura({
|
|
20
|
+
dbHost: "localhost",
|
|
21
|
+
dbName: "your_database",
|
|
22
|
+
dbUser: "your_username",
|
|
23
|
+
dbPassword: "your_password",
|
|
24
|
+
dbPort: 5432,
|
|
25
|
+
tlsVersion: "1.3"
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Parsing Network Packets
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Get and parse packets from database
|
|
33
|
+
const parseClient = client.parse;
|
|
34
|
+
const packets = await parseClient.getPackets();
|
|
35
|
+
const appData = await parseClient.getAppData();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Creating TLS connection
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// Get and parse packets from database
|
|
42
|
+
const tlsClient = client.tls;
|
|
43
|
+
await tlsClient.init();
|
|
44
|
+
const tlsConn = await tlsClient.TlsConnection();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Available Modules
|
|
48
|
+
|
|
49
|
+
The SDK consists of several modules:
|
|
50
|
+
|
|
51
|
+
- **Parse**: Network packet parsing and analysis
|
|
52
|
+
- **Prove**: Statement proving and verification (coming soon)
|
|
53
|
+
- **Connect**: Connection management (coming soon)
|
|
54
|
+
- **TLS**: TLS operations with WASM implementation
|
|
55
|
+
|
|
56
|
+
### Module-specific Imports
|
|
57
|
+
|
|
58
|
+
You can import specific modules directly:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { ParseClient } from 'tisura/parse';
|
|
62
|
+
// import { ProveClient } from 'tisura/prove'; // Coming soon
|
|
63
|
+
// import { ConnectClient } from 'tisura/connect'; // Coming soon
|
|
64
|
+
import { TLSClient } from 'tisura/tls';
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
The SDK requires the following configuration parameters:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
interface TisuraConfig {
|
|
73
|
+
dbHost?: string; // Database host
|
|
74
|
+
dbName?: string; // Database name
|
|
75
|
+
dbUser?: string; // Database username
|
|
76
|
+
dbPassword?: string; // Database password
|
|
77
|
+
dbPort?: number; // Database port
|
|
78
|
+
tlsVersion?: string; // TLS version ("1.2" or "1.3")
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
### Install, build and run tests
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Install dependencies
|
|
88
|
+
npm install
|
|
89
|
+
|
|
90
|
+
# Build the package
|
|
91
|
+
npm run build
|
|
92
|
+
|
|
93
|
+
# Run tests
|
|
94
|
+
npm test
|
|
95
|
+
```
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Tisura = void 0;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const parse_1 = require("./parse");
|
|
6
|
+
// import { ProveClient } from "./prove";
|
|
7
|
+
// import { ConnectClient } from "./connect";
|
|
8
|
+
// import { TLSClient } from "./tls";
|
|
9
|
+
class Tisura extends config_1.TisuraBase {
|
|
10
|
+
parse;
|
|
11
|
+
// public prove: ProveClient;
|
|
12
|
+
// public connect: ConnectClient;
|
|
13
|
+
// public tls: TLSClient;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
super(config);
|
|
16
|
+
this.parse = new parse_1.ParseClient(this.config);
|
|
17
|
+
// this.prove = new ProveClient(this.config);
|
|
18
|
+
// this.connect = new ConnectClient(this.config);
|
|
19
|
+
// this.tls = new TLSClient(this.config);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.Tisura = Tisura;
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
export interface TisuraConfig {
|
|
3
|
+
dbHost?: string;
|
|
4
|
+
dbName?: string;
|
|
5
|
+
dbUser?: string;
|
|
6
|
+
dbPassword?: string;
|
|
7
|
+
dbPort?: number;
|
|
8
|
+
tlsVersion?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class TisuraBase {
|
|
11
|
+
protected config: TisuraConfig;
|
|
12
|
+
constructor(config: TisuraConfig);
|
|
13
|
+
protected getPool(): Pool;
|
|
14
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TisuraBase = void 0;
|
|
4
|
+
const pg_1 = require("pg");
|
|
5
|
+
class TisuraBase {
|
|
6
|
+
config;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
getPool() {
|
|
11
|
+
return new pg_1.Pool({
|
|
12
|
+
host: this.config.dbHost,
|
|
13
|
+
database: this.config.dbName,
|
|
14
|
+
user: this.config.dbUser,
|
|
15
|
+
password: this.config.dbPassword,
|
|
16
|
+
port: this.config.dbPort,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.TisuraBase = TisuraBase;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function connectToServer(url: string): void;
|
package/dist/parse/db.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readPacketsFromDb = readPacketsFromDb;
|
|
4
|
+
/**
|
|
5
|
+
* Reads rows from the "captures" table and returns an array of PacketInfo.
|
|
6
|
+
*/
|
|
7
|
+
async function readPacketsFromDb(pool) {
|
|
8
|
+
try {
|
|
9
|
+
// Query the database
|
|
10
|
+
const result = await pool.query(`
|
|
11
|
+
SELECT
|
|
12
|
+
id,
|
|
13
|
+
timestamp,
|
|
14
|
+
source_ip,
|
|
15
|
+
source_port,
|
|
16
|
+
destination_ip,
|
|
17
|
+
destination_port,
|
|
18
|
+
protocol,
|
|
19
|
+
length,
|
|
20
|
+
payload
|
|
21
|
+
FROM captures
|
|
22
|
+
ORDER BY id;
|
|
23
|
+
`);
|
|
24
|
+
// Map each row to a PacketInfo object
|
|
25
|
+
const packets = result.rows.map((row) => {
|
|
26
|
+
// In Node.js, 'timestamp' column often comes as a Date object.
|
|
27
|
+
// Python's row[1].timestamp() returned float seconds from epoch.
|
|
28
|
+
// We'll replicate that by dividing Date.getTime() by 1000.
|
|
29
|
+
const epochSeconds = row.timestamp.getTime() / 1000;
|
|
30
|
+
return {
|
|
31
|
+
timestamp: epochSeconds,
|
|
32
|
+
sourceIp: row.source_ip,
|
|
33
|
+
sourcePort: row.source_port,
|
|
34
|
+
destinationIp: row.destination_ip,
|
|
35
|
+
destinationPort: row.destination_port,
|
|
36
|
+
protocol: row.protocol,
|
|
37
|
+
length: row.length,
|
|
38
|
+
// 'payload' from Postgres may be a Buffer in Node. Convert to Uint8Array.
|
|
39
|
+
payload: new Uint8Array(row.payload),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
return packets;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
// Close the pool to free up resources
|
|
46
|
+
await pool.end();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TisuraBase, TisuraConfig } from "../config";
|
|
2
|
+
import { PacketInfo, ApplicationData } from "./types";
|
|
3
|
+
export declare class ParseClient extends TisuraBase {
|
|
4
|
+
constructor(config: TisuraConfig);
|
|
5
|
+
getPackets(): Promise<PacketInfo[]>;
|
|
6
|
+
getAppData(): Promise<ApplicationData[]>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ParseClient = void 0;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
const parser_1 = require("./parser");
|
|
6
|
+
const db_1 = require("./db");
|
|
7
|
+
class ParseClient extends config_1.TisuraBase {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super(config);
|
|
10
|
+
}
|
|
11
|
+
async getPackets() {
|
|
12
|
+
const pool = this.getPool();
|
|
13
|
+
const packets = await (0, db_1.readPacketsFromDb)(pool);
|
|
14
|
+
return packets;
|
|
15
|
+
}
|
|
16
|
+
async getAppData() {
|
|
17
|
+
const packets = await this.getPackets();
|
|
18
|
+
const parser = new parser_1.Parser(packets, this.config.tlsVersion);
|
|
19
|
+
const appData = parser.run();
|
|
20
|
+
return appData;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.ParseClient = ParseClient;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PacketInfo, Stream, TLSRecord, ApplicationData } from "./types";
|
|
2
|
+
export declare class Parser {
|
|
3
|
+
private packets;
|
|
4
|
+
private tlsVersion?;
|
|
5
|
+
streams: Stream[];
|
|
6
|
+
records: TLSRecord[];
|
|
7
|
+
appData: ApplicationData[];
|
|
8
|
+
/**
|
|
9
|
+
* In TLS 1.2, we track a client sequence number for Application Data records
|
|
10
|
+
*/
|
|
11
|
+
clientSeqNum: number;
|
|
12
|
+
constructor(packets: PacketInfo[], tlsVersion?: string);
|
|
13
|
+
/**
|
|
14
|
+
* Run the parser, orchestrating the steps:
|
|
15
|
+
* 1) Reassemble TCP streams and parse out TLS records
|
|
16
|
+
* 2) Combine all records into a single list (ordered by time)
|
|
17
|
+
* 3) Filter to find TLS Application Data
|
|
18
|
+
*/
|
|
19
|
+
run(): ApplicationData[];
|
|
20
|
+
/**
|
|
21
|
+
* Reassemble TCP packets into streams and parse TLS records simultaneously
|
|
22
|
+
*/
|
|
23
|
+
private reassembleAndParse;
|
|
24
|
+
/**
|
|
25
|
+
* Combine all parsed records from the streams into one flat list (sorted by time).
|
|
26
|
+
*/
|
|
27
|
+
private combineStreams;
|
|
28
|
+
/**
|
|
29
|
+
* Extract TLS parameters (e.g., random, cipher_suite) from a Handshake record
|
|
30
|
+
* if it's ClientHello or ServerHello.
|
|
31
|
+
*
|
|
32
|
+
* - If ClientHello, returns [clientRandom, null]
|
|
33
|
+
* - If ServerHello, returns [serverRandom, cipherSuiteNumber]
|
|
34
|
+
*/
|
|
35
|
+
private extractTlsParams;
|
|
36
|
+
/**
|
|
37
|
+
* Filter out only TLS Application Data records (0x17).
|
|
38
|
+
* Includes logic for TLS 1.2 vs. TLS 1.3 as per the Python version.
|
|
39
|
+
*/
|
|
40
|
+
private filterAppData;
|
|
41
|
+
/**
|
|
42
|
+
* Get a directed stream ID from a given packet.
|
|
43
|
+
* Returns a tuple: ((srcIp, srcPort), (dstIp, dstPort))
|
|
44
|
+
* or null if not TCP.
|
|
45
|
+
*/
|
|
46
|
+
private getDirectedStreamId;
|
|
47
|
+
/**
|
|
48
|
+
* Convert a StreamID to a string for use as a Map key.
|
|
49
|
+
*/
|
|
50
|
+
private streamIdToString;
|
|
51
|
+
/**
|
|
52
|
+
* Concatenate two Uint8Arrays
|
|
53
|
+
*/
|
|
54
|
+
private concatUint8Arrays;
|
|
55
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Parser = void 0;
|
|
4
|
+
const HEADER_LENGTH = 5;
|
|
5
|
+
class Parser {
|
|
6
|
+
packets;
|
|
7
|
+
tlsVersion;
|
|
8
|
+
// Internal state
|
|
9
|
+
streams = [];
|
|
10
|
+
records = [];
|
|
11
|
+
appData = [];
|
|
12
|
+
/**
|
|
13
|
+
* In TLS 1.2, we track a client sequence number for Application Data records
|
|
14
|
+
*/
|
|
15
|
+
clientSeqNum = 0;
|
|
16
|
+
constructor(packets, tlsVersion) {
|
|
17
|
+
this.packets = packets;
|
|
18
|
+
this.tlsVersion = tlsVersion;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run the parser, orchestrating the steps:
|
|
22
|
+
* 1) Reassemble TCP streams and parse out TLS records
|
|
23
|
+
* 2) Combine all records into a single list (ordered by time)
|
|
24
|
+
* 3) Filter to find TLS Application Data
|
|
25
|
+
*/
|
|
26
|
+
run() {
|
|
27
|
+
this.streams = this.reassembleAndParse();
|
|
28
|
+
this.records = this.combineStreams();
|
|
29
|
+
this.appData = this.filterAppData();
|
|
30
|
+
return this.appData;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reassemble TCP packets into streams and parse TLS records simultaneously
|
|
34
|
+
*/
|
|
35
|
+
reassembleAndParse() {
|
|
36
|
+
// Use a Map keyed by the stream ID stringified.
|
|
37
|
+
// (Alternatively, you could store by the actual tuple, but strings are simpler.)
|
|
38
|
+
const partialRecords = new Map();
|
|
39
|
+
// Map of streamIDString -> Stream
|
|
40
|
+
const streamsMap = new Map();
|
|
41
|
+
for (const pkt of this.packets) {
|
|
42
|
+
if (pkt.protocol !== "TCP") {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const streamId = this.getDirectedStreamId(pkt);
|
|
46
|
+
if (!streamId) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const streamIdKey = this.streamIdToString(streamId);
|
|
50
|
+
if (!partialRecords.has(streamIdKey)) {
|
|
51
|
+
partialRecords.set(streamIdKey, {
|
|
52
|
+
data: new Uint8Array(0),
|
|
53
|
+
firstTime: null,
|
|
54
|
+
lastTime: null,
|
|
55
|
+
neededLength: null,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!streamsMap.has(streamIdKey)) {
|
|
59
|
+
streamsMap.set(streamIdKey, { records: [], streamId });
|
|
60
|
+
}
|
|
61
|
+
const partial = partialRecords.get(streamIdKey);
|
|
62
|
+
const pktTime = pkt.timestamp;
|
|
63
|
+
// Append new data
|
|
64
|
+
partial.data = this.concatUint8Arrays(partial.data, pkt.payload);
|
|
65
|
+
partial.lastTime = pktTime;
|
|
66
|
+
if (partial.firstTime === null) {
|
|
67
|
+
partial.firstTime = pktTime;
|
|
68
|
+
}
|
|
69
|
+
// Process complete records
|
|
70
|
+
// We need at least 5 bytes to parse the record header (TLS).
|
|
71
|
+
while (partial.data.length >= HEADER_LENGTH) {
|
|
72
|
+
// If we haven't identified the record length yet
|
|
73
|
+
if (partial.neededLength === null) {
|
|
74
|
+
const contentType = partial.data[0];
|
|
75
|
+
// The total TLS record length is in bytes 3-4 (big-endian).
|
|
76
|
+
const length = (partial.data[3] << 8) | partial.data[4];
|
|
77
|
+
partial.neededLength = HEADER_LENGTH + length; // header (5) + payload
|
|
78
|
+
}
|
|
79
|
+
// If we have a complete record
|
|
80
|
+
if (partial.data.length >= partial.neededLength) {
|
|
81
|
+
const recordData = partial.data.slice(0, partial.neededLength);
|
|
82
|
+
const contentType = recordData[0];
|
|
83
|
+
const record = {
|
|
84
|
+
fullRecord: recordData,
|
|
85
|
+
contentType,
|
|
86
|
+
streamId,
|
|
87
|
+
firstPacketTime: partial.firstTime,
|
|
88
|
+
lastPacketTime: partial.lastTime,
|
|
89
|
+
};
|
|
90
|
+
if (contentType === 0x16) {
|
|
91
|
+
// Handshake
|
|
92
|
+
const handshakeType = recordData.length > 5 ? recordData[5] : null;
|
|
93
|
+
console.log(`[PARSER] Found handshake type: ${handshakeType}`);
|
|
94
|
+
if (handshakeType === 2) {
|
|
95
|
+
// ServerHello
|
|
96
|
+
console.log(`[PARSER] ServerHello in stream ${this.streamIdToString(streamId)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
console.log(`[PARSER] New record: type=0x${contentType.toString(16).padStart(2, "0")}, stream=${this.streamIdToString(streamId)}`);
|
|
100
|
+
console.log(`[PARSER] Time span: ${partial.firstTime} -> ${partial.lastTime}`);
|
|
101
|
+
// Add record to the corresponding Stream
|
|
102
|
+
const streamObj = streamsMap.get(streamIdKey);
|
|
103
|
+
streamObj.records.push(record);
|
|
104
|
+
// Remove this record's data
|
|
105
|
+
partial.data = partial.data.slice(partial.neededLength);
|
|
106
|
+
// Reset neededLength
|
|
107
|
+
partial.neededLength = null;
|
|
108
|
+
// Reset first_time to the last_time for the next record
|
|
109
|
+
partial.firstTime = partial.lastTime;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Need more data for this record
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Return an array of Streams
|
|
118
|
+
return Array.from(streamsMap.values());
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Combine all parsed records from the streams into one flat list (sorted by time).
|
|
122
|
+
*/
|
|
123
|
+
combineStreams() {
|
|
124
|
+
const combinedRecords = this.streams.flatMap((stream) => stream.records);
|
|
125
|
+
// Sort by last_packet_time
|
|
126
|
+
combinedRecords.sort((a, b) => a.lastPacketTime - b.lastPacketTime);
|
|
127
|
+
return combinedRecords;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Extract TLS parameters (e.g., random, cipher_suite) from a Handshake record
|
|
131
|
+
* if it's ClientHello or ServerHello.
|
|
132
|
+
*
|
|
133
|
+
* - If ClientHello, returns [clientRandom, null]
|
|
134
|
+
* - If ServerHello, returns [serverRandom, cipherSuiteNumber]
|
|
135
|
+
*/
|
|
136
|
+
extractTlsParams(handshakeRecord) {
|
|
137
|
+
// Handshake layer starts after the first 5 bytes (TLS header)
|
|
138
|
+
const hsData = handshakeRecord.slice(5);
|
|
139
|
+
const hsType = hsData[0];
|
|
140
|
+
const hsLength = (hsData[1] << 16) | (hsData[2] << 8) | hsData[3];
|
|
141
|
+
const body = hsData.slice(4, 4 + hsLength);
|
|
142
|
+
// Typically 32 bytes of random data
|
|
143
|
+
const randomSize = 32;
|
|
144
|
+
const random = body.slice(2, 2 + randomSize);
|
|
145
|
+
if (hsType === 0x01) {
|
|
146
|
+
// ClientHello
|
|
147
|
+
return [random, null];
|
|
148
|
+
}
|
|
149
|
+
else if (hsType === 0x02) {
|
|
150
|
+
// ServerHello
|
|
151
|
+
const sessionIdLength = body[34];
|
|
152
|
+
const start = 35 + sessionIdLength;
|
|
153
|
+
const end = start + 2;
|
|
154
|
+
const cipherSuite = (body[start] << 8) | body[start + 1];
|
|
155
|
+
return [random, cipherSuite];
|
|
156
|
+
}
|
|
157
|
+
// If not recognized, return defaults
|
|
158
|
+
return [new Uint8Array(0), null];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Filter out only TLS Application Data records (0x17).
|
|
162
|
+
* Includes logic for TLS 1.2 vs. TLS 1.3 as per the Python version.
|
|
163
|
+
*/
|
|
164
|
+
filterAppData() {
|
|
165
|
+
console.log(`[PARSER] Filtering ${this.records.length} TLS records for application data`);
|
|
166
|
+
const appData = [];
|
|
167
|
+
// ------------------------------------
|
|
168
|
+
// COMMON STATE
|
|
169
|
+
// ------------------------------------
|
|
170
|
+
let clientPort = null;
|
|
171
|
+
let clientToServerStreamId = null;
|
|
172
|
+
let tlsParams = {
|
|
173
|
+
clientRandom: new Uint8Array(0),
|
|
174
|
+
serverRandom: new Uint8Array(0),
|
|
175
|
+
cipherSuite: 0,
|
|
176
|
+
};
|
|
177
|
+
// TLS 1.2 tracking
|
|
178
|
+
let clientCcsSent = false;
|
|
179
|
+
let saveClientAppData = false;
|
|
180
|
+
// TLS 1.3 tracking
|
|
181
|
+
let clientAppDataAfterKeyUpdate = false;
|
|
182
|
+
this.clientSeqNum = 0;
|
|
183
|
+
for (let i = 0; i < this.records.length; i++) {
|
|
184
|
+
const record = this.records[i];
|
|
185
|
+
let isClient = false;
|
|
186
|
+
if (clientPort !== null) {
|
|
187
|
+
// If the source port matches the known client port
|
|
188
|
+
isClient = record.streamId[0][1] === clientPort;
|
|
189
|
+
}
|
|
190
|
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
191
|
+
// TLS 1.2 LOGIC
|
|
192
|
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
193
|
+
if (this.tlsVersion === "1.2") {
|
|
194
|
+
if (record.contentType === 0x16) {
|
|
195
|
+
// Handshake
|
|
196
|
+
const payload = record.fullRecord.slice(5);
|
|
197
|
+
if (payload.length === 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const hsType = payload[0];
|
|
201
|
+
if (hsType === 0x01) {
|
|
202
|
+
// ClientHello
|
|
203
|
+
clientPort = record.streamId[0][1];
|
|
204
|
+
clientToServerStreamId = record.streamId;
|
|
205
|
+
saveClientAppData = false;
|
|
206
|
+
console.log(`[TLS 1.2] Found ClientHello from client port ${clientPort} at record #${i}`);
|
|
207
|
+
const [clientRandom, _] = this.extractTlsParams(record.fullRecord);
|
|
208
|
+
tlsParams = {
|
|
209
|
+
clientRandom,
|
|
210
|
+
serverRandom: new Uint8Array(0),
|
|
211
|
+
cipherSuite: 0,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
else if (hsType === 0x02) {
|
|
215
|
+
// ServerHello
|
|
216
|
+
const [serverRandom, cipherSuite] = this.extractTlsParams(record.fullRecord);
|
|
217
|
+
tlsParams = {
|
|
218
|
+
clientRandom: tlsParams.clientRandom,
|
|
219
|
+
serverRandom,
|
|
220
|
+
cipherSuite: cipherSuite || 0,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// Possibly an encrypted handshake message (e.g. Finished)
|
|
225
|
+
if (isClient && clientCcsSent && !saveClientAppData) {
|
|
226
|
+
// The next handshake record from client after CCS is the client Finished
|
|
227
|
+
saveClientAppData = true;
|
|
228
|
+
clientCcsSent = false;
|
|
229
|
+
this.clientSeqNum = 0;
|
|
230
|
+
console.log(`[TLS 1.2] Detected Client Finished (encrypted) at record #${i}. Now capturing client app data.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else if (record.contentType === 0x14) {
|
|
235
|
+
// ChangeCipherSpec (TLS 1.2)
|
|
236
|
+
if (isClient) {
|
|
237
|
+
clientCcsSent = true;
|
|
238
|
+
console.log(`[TLS 1.2] Client CCS at record #${i} => waiting for client Finished.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else if (record.contentType === 0x17) {
|
|
242
|
+
// Application Data
|
|
243
|
+
if (isClient && saveClientAppData) {
|
|
244
|
+
console.log(`[TLS 1.2] Capturing client app data at record #${i}`);
|
|
245
|
+
// client→server app data
|
|
246
|
+
this.clientSeqNum += 1;
|
|
247
|
+
appData.push({
|
|
248
|
+
tlsRecord: record,
|
|
249
|
+
seqNum: this.clientSeqNum,
|
|
250
|
+
tlsParams,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
255
|
+
// TLS 1.3 LOGIC
|
|
256
|
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
257
|
+
}
|
|
258
|
+
else if (this.tlsVersion == "1.3") {
|
|
259
|
+
if (record.contentType === 0x16) {
|
|
260
|
+
// Handshake
|
|
261
|
+
const payload = record.fullRecord.slice(5);
|
|
262
|
+
if (payload.length === 0)
|
|
263
|
+
continue;
|
|
264
|
+
const hsType = payload[0];
|
|
265
|
+
if (hsType === 0x01) {
|
|
266
|
+
// ClientHello
|
|
267
|
+
clientPort = record.streamId[0][1];
|
|
268
|
+
clientToServerStreamId = record.streamId;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (record.contentType === 0x14) {
|
|
272
|
+
// In TLS 1.3, 0x14 can be KeyUpdate or a dummy CCS
|
|
273
|
+
const payload = record.fullRecord.slice(5);
|
|
274
|
+
if (isClient && payload.length > 0 && payload[0] === 0x01) {
|
|
275
|
+
// Suppose this indicates a KeyUpdate from client
|
|
276
|
+
clientAppDataAfterKeyUpdate = true;
|
|
277
|
+
saveClientAppData = false;
|
|
278
|
+
this.clientSeqNum = 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (record.contentType === 0x17) {
|
|
282
|
+
// Application Data in TLS 1.3
|
|
283
|
+
const isClientToServer = clientToServerStreamId &&
|
|
284
|
+
record.streamId[0][0] === clientToServerStreamId[0][0] &&
|
|
285
|
+
record.streamId[0][1] === clientToServerStreamId[0][1] &&
|
|
286
|
+
record.streamId[1][0] === clientToServerStreamId[1][0] &&
|
|
287
|
+
record.streamId[1][1] === clientToServerStreamId[1][1];
|
|
288
|
+
if (isClientToServer && clientAppDataAfterKeyUpdate) {
|
|
289
|
+
if (saveClientAppData) {
|
|
290
|
+
appData.push({
|
|
291
|
+
tlsRecord: record,
|
|
292
|
+
seqNum: this.clientSeqNum,
|
|
293
|
+
tlsParams: {
|
|
294
|
+
clientRandom: new Uint8Array(0),
|
|
295
|
+
serverRandom: new Uint8Array(0),
|
|
296
|
+
cipherSuite: 0,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
this.clientSeqNum++;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
saveClientAppData = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.log("[PARSER] Error: `tlsVersion` should be either 1.2 or 1.3.");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Warn if we never identified a client port
|
|
313
|
+
if (clientPort === null) {
|
|
314
|
+
console.log("[PARSER] Warning: Could not identify client port");
|
|
315
|
+
}
|
|
316
|
+
return appData;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get a directed stream ID from a given packet.
|
|
320
|
+
* Returns a tuple: ((srcIp, srcPort), (dstIp, dstPort))
|
|
321
|
+
* or null if not TCP.
|
|
322
|
+
*/
|
|
323
|
+
getDirectedStreamId(pkt) {
|
|
324
|
+
if (pkt.protocol === "TCP") {
|
|
325
|
+
const src = [pkt.sourceIp, pkt.sourcePort];
|
|
326
|
+
const dst = [pkt.destinationIp, pkt.destinationPort];
|
|
327
|
+
return [src, dst];
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Convert a StreamID to a string for use as a Map key.
|
|
333
|
+
*/
|
|
334
|
+
streamIdToString(streamId) {
|
|
335
|
+
const [[srcIp, srcPort], [dstIp, dstPort]] = streamId;
|
|
336
|
+
return `${srcIp}:${srcPort}->${dstIp}:${dstPort}`;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Concatenate two Uint8Arrays
|
|
340
|
+
*/
|
|
341
|
+
concatUint8Arrays(a, b) {
|
|
342
|
+
const c = new Uint8Array(a.length + b.length);
|
|
343
|
+
c.set(a, 0);
|
|
344
|
+
c.set(b, a.length);
|
|
345
|
+
return c;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
exports.Parser = Parser;
|