binarycookies 2.2.1__tar.gz → 2.3.0__tar.gz
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.
- {binarycookies-2.2.1 → binarycookies-2.3.0}/PKG-INFO +23 -48
- {binarycookies-2.2.1 → binarycookies-2.3.0}/README.md +20 -43
- {binarycookies-2.2.1 → binarycookies-2.3.0}/pyproject.toml +2 -2
- binarycookies-2.3.0/setup.py +38 -0
- binarycookies-2.3.0/src/binarycookies/__init__.py +12 -0
- {binarycookies-2.2.1 → binarycookies-2.3.0}/src/binarycookies/__main__.py +21 -4
- {binarycookies-2.2.1 → binarycookies-2.3.0}/src/binarycookies/_deserialize.py +30 -10
- binarycookies-2.3.0/src/binarycookies/_serialize.py +222 -0
- {binarycookies-2.2.1 → binarycookies-2.3.0}/src/binarycookies/models.py +12 -0
- binarycookies-2.2.1/src/binarycookies/__init__.py +0 -4
- binarycookies-2.2.1/src/binarycookies/_serialize.py +0 -149
- {binarycookies-2.2.1 → binarycookies-2.3.0}/LICENSE +0 -0
- {binarycookies-2.2.1 → binarycookies-2.3.0}/src/binarycookies/_output_handlers.py +0 -0
- {binarycookies-2.2.1 → binarycookies-2.3.0}/src/binarycookies/parser.py +0 -0
- {binarycookies-2.2.1 → binarycookies-2.3.0}/src/binarycookies/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: binarycookies
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Python Binary Cookies (de)serializer
|
|
5
5
|
Author: Daniel Tom
|
|
6
6
|
Author-email: d.e.tom89@gmail.com
|
|
@@ -10,10 +10,8 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
13
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
16
|
-
Requires-Dist: typer (>=0.12.3,<0.
|
|
14
|
+
Requires-Dist: typer (>=0.12.3,<0.21.0)
|
|
17
15
|
Description-Content-Type: text/markdown
|
|
18
16
|
|
|
19
17
|
[](https://github.com/dan1elt0m/binary-cookies-reader/actions/workflows/test.yml)
|
|
@@ -24,6 +22,9 @@ Description-Content-Type: text/markdown
|
|
|
24
22
|
|
|
25
23
|
CLI tool and Python library for reading and writing Binary Cookies.
|
|
26
24
|
|
|
25
|
+
### Documentation
|
|
26
|
+
For detailed documentation, please visit the [Binary Cookies Documentation](https://dan1elt0m.github.io/binarycookies/)
|
|
27
|
+
|
|
27
28
|
### Requirements
|
|
28
29
|
|
|
29
30
|
- Python >= 3.9
|
|
@@ -61,49 +62,11 @@ Output:
|
|
|
61
62
|
}
|
|
62
63
|
]
|
|
63
64
|
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
Example usage:
|
|
71
|
-
```sh
|
|
72
|
-
bcparser path/to/cookies.binarycookies --output ascii
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Example output ASCII:
|
|
76
|
-
```text
|
|
77
|
-
Name: session_id
|
|
78
|
-
Value: abc123
|
|
79
|
-
URL: https://example.com
|
|
80
|
-
Path: /
|
|
81
|
-
Created: 2023-10-01T12:34:56+00:00
|
|
82
|
-
Expires: 2023-12-31T23:59:59+00:00
|
|
83
|
-
Flag: Secure
|
|
84
|
-
----------------------------------------
|
|
85
|
-
Name: user_token
|
|
86
|
-
Value: xyz789
|
|
87
|
-
URL: https://example.com
|
|
88
|
-
Path: /account
|
|
89
|
-
Created: 2023-10-01T12:34:56+00:00
|
|
90
|
-
Expires: 2023-12-31T23:59:59+00:00
|
|
91
|
-
Flag: HttpOnly
|
|
92
|
-
----------------------------------------
|
|
93
|
-
```
|
|
94
|
-
### Netscape Output
|
|
95
|
-
For `netscape` output use the --output netscape flag.
|
|
96
|
-
Example usage:
|
|
97
|
-
```sh
|
|
98
|
-
bcparser path/to/cookies.binarycookies --output netscape
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Example output Netscape:
|
|
102
|
-
```netscape
|
|
103
|
-
# Netscape HTTP Cookie File
|
|
104
|
-
example.com FALSE / TRUE 1704067199 session_id abc123
|
|
105
|
-
example.com FALSE /account FALSE 1704067199 user_token xyz789
|
|
106
|
-
```
|
|
65
|
+
#### Output formats
|
|
66
|
+
The CLI supports multiple output formats using the --output flag.
|
|
67
|
+
- `json` (default): Outputs cookies in JSON format.
|
|
68
|
+
- `ascii`: Outputs cookies in a human-readable ASCII format with each cookie property on a separate line.
|
|
69
|
+
- `netscape`: Outputs cookies in the Netscape cookie file format.
|
|
107
70
|
|
|
108
71
|
### Basic Usage Python
|
|
109
72
|
|
|
@@ -135,6 +98,18 @@ with open("path/to/cookies.binarycookies", "wb") as f:
|
|
|
135
98
|
binarycookies.dump(cookie, f)
|
|
136
99
|
```
|
|
137
100
|
|
|
101
|
+
### Ethical Use & Responsible Handling
|
|
102
|
+
This project is intended for lawful, ethical use only. Typical, appropriate uses include:
|
|
103
|
+
- Inspecting Binary Cookies from your own devices or data you are authorized to access
|
|
104
|
+
- DFIR, QA, and security testing performed with explicit, written permission
|
|
105
|
+
- Educational/research work on datasets that are owned by you, anonymized, or publicly released for that purpose
|
|
106
|
+
|
|
107
|
+
You must not use this tool to:
|
|
108
|
+
- Access, extract, modify, or distribute cookies from systems or accounts you do not own or have permission to analyze
|
|
109
|
+
- Bypass authentication, session management, DRM, or other technical controls
|
|
110
|
+
- Enable tracking, stalking, doxxing, fraud, or other privacy-invasive or harmful activities
|
|
111
|
+
|
|
112
|
+
|
|
138
113
|
### License
|
|
139
114
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
140
115
|
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
CLI tool and Python library for reading and writing Binary Cookies.
|
|
8
8
|
|
|
9
|
+
### Documentation
|
|
10
|
+
For detailed documentation, please visit the [Binary Cookies Documentation](https://dan1elt0m.github.io/binarycookies/)
|
|
11
|
+
|
|
9
12
|
### Requirements
|
|
10
13
|
|
|
11
14
|
- Python >= 3.9
|
|
@@ -43,49 +46,11 @@ Output:
|
|
|
43
46
|
}
|
|
44
47
|
]
|
|
45
48
|
```
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
Example usage:
|
|
53
|
-
```sh
|
|
54
|
-
bcparser path/to/cookies.binarycookies --output ascii
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Example output ASCII:
|
|
58
|
-
```text
|
|
59
|
-
Name: session_id
|
|
60
|
-
Value: abc123
|
|
61
|
-
URL: https://example.com
|
|
62
|
-
Path: /
|
|
63
|
-
Created: 2023-10-01T12:34:56+00:00
|
|
64
|
-
Expires: 2023-12-31T23:59:59+00:00
|
|
65
|
-
Flag: Secure
|
|
66
|
-
----------------------------------------
|
|
67
|
-
Name: user_token
|
|
68
|
-
Value: xyz789
|
|
69
|
-
URL: https://example.com
|
|
70
|
-
Path: /account
|
|
71
|
-
Created: 2023-10-01T12:34:56+00:00
|
|
72
|
-
Expires: 2023-12-31T23:59:59+00:00
|
|
73
|
-
Flag: HttpOnly
|
|
74
|
-
----------------------------------------
|
|
75
|
-
```
|
|
76
|
-
### Netscape Output
|
|
77
|
-
For `netscape` output use the --output netscape flag.
|
|
78
|
-
Example usage:
|
|
79
|
-
```sh
|
|
80
|
-
bcparser path/to/cookies.binarycookies --output netscape
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Example output Netscape:
|
|
84
|
-
```netscape
|
|
85
|
-
# Netscape HTTP Cookie File
|
|
86
|
-
example.com FALSE / TRUE 1704067199 session_id abc123
|
|
87
|
-
example.com FALSE /account FALSE 1704067199 user_token xyz789
|
|
88
|
-
```
|
|
49
|
+
#### Output formats
|
|
50
|
+
The CLI supports multiple output formats using the --output flag.
|
|
51
|
+
- `json` (default): Outputs cookies in JSON format.
|
|
52
|
+
- `ascii`: Outputs cookies in a human-readable ASCII format with each cookie property on a separate line.
|
|
53
|
+
- `netscape`: Outputs cookies in the Netscape cookie file format.
|
|
89
54
|
|
|
90
55
|
### Basic Usage Python
|
|
91
56
|
|
|
@@ -117,6 +82,18 @@ with open("path/to/cookies.binarycookies", "wb") as f:
|
|
|
117
82
|
binarycookies.dump(cookie, f)
|
|
118
83
|
```
|
|
119
84
|
|
|
85
|
+
### Ethical Use & Responsible Handling
|
|
86
|
+
This project is intended for lawful, ethical use only. Typical, appropriate uses include:
|
|
87
|
+
- Inspecting Binary Cookies from your own devices or data you are authorized to access
|
|
88
|
+
- DFIR, QA, and security testing performed with explicit, written permission
|
|
89
|
+
- Educational/research work on datasets that are owned by you, anonymized, or publicly released for that purpose
|
|
90
|
+
|
|
91
|
+
You must not use this tool to:
|
|
92
|
+
- Access, extract, modify, or distribute cookies from systems or accounts you do not own or have permission to analyze
|
|
93
|
+
- Bypass authentication, session management, DRM, or other technical controls
|
|
94
|
+
- Enable tracking, stalking, doxxing, fraud, or other privacy-invasive or harmful activities
|
|
95
|
+
|
|
96
|
+
|
|
120
97
|
### License
|
|
121
98
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
122
99
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "binarycookies"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.0"
|
|
4
4
|
description = "Python Binary Cookies (de)serializer"
|
|
5
5
|
authors = ["Daniel Tom <d.e.tom89@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -8,7 +8,7 @@ packages = [{include = "binarycookies", from="src"}]
|
|
|
8
8
|
|
|
9
9
|
[tool.poetry.dependencies]
|
|
10
10
|
python = ">=3.8,<4.0"
|
|
11
|
-
typer = ">=0.12.3,<0.
|
|
11
|
+
typer = ">=0.12.3,<0.21.0"
|
|
12
12
|
pydantic = ">=2.0.0,<3.0.0"
|
|
13
13
|
|
|
14
14
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
package_dir = \
|
|
5
|
+
{'': 'src'}
|
|
6
|
+
|
|
7
|
+
packages = \
|
|
8
|
+
['binarycookies']
|
|
9
|
+
|
|
10
|
+
package_data = \
|
|
11
|
+
{'': ['*']}
|
|
12
|
+
|
|
13
|
+
install_requires = \
|
|
14
|
+
['pydantic>=2.0.0,<3.0.0', 'typer>=0.12.3,<0.21.0']
|
|
15
|
+
|
|
16
|
+
entry_points = \
|
|
17
|
+
{'console_scripts': ['bcparser = binarycookies.__main__:main']}
|
|
18
|
+
|
|
19
|
+
setup_kwargs = {
|
|
20
|
+
'name': 'binarycookies',
|
|
21
|
+
'version': '2.3.0',
|
|
22
|
+
'description': 'Python Binary Cookies (de)serializer',
|
|
23
|
+
'long_description': '[](https://github.com/dan1elt0m/binary-cookies-reader/actions/workflows/test.yml)\n<h1>\n <img src="docs/bincook.png" width="30" style="vertical-align: middle; margin-right: 10px;">\n Binary Cookies\n</h1>\n\nCLI tool and Python library for reading and writing Binary Cookies.\n\n### Documentation\nFor detailed documentation, please visit the [Binary Cookies Documentation](https://dan1elt0m.github.io/binarycookies/)\n\n### Requirements\n\n- Python >= 3.9\n\n### Installation\n```bash \npip install binarycookies\n```\n\n### CLI example:\n```sh\nbcparser path/to/cookies.binarycookies\n```\n\nOutput:\n```json\n[\n {\n "name": "session_id",\n "value": "abc123",\n "url": "https://example.com",\n "path": "/",\n "create_datetime": "2023-10-01T12:34:56+00:00",\n "expiry_datetime": "2023-12-31T23:59:59+00:00",\n "flag": "Secure"\n },\n {\n "name": "user_token",\n "value": "xyz789",\n "url": "https://example.com",\n "path": "/account",\n "create_datetime": "2023-10-01T12:34:56+00:00",\n "expiry_datetime": "2023-12-31T23:59:59+00:00",\n "flag": "HttpOnly"\n }\n]\n```\n#### Output formats\nThe CLI supports multiple output formats using the --output flag.\n- `json` (default): Outputs cookies in JSON format.\n- `ascii`: Outputs cookies in a human-readable ASCII format with each cookie property on a separate line.\n- `netscape`: Outputs cookies in the Netscape cookie file format.\n\n### Basic Usage Python\n\n#### Deserialization\n\n```python\nimport binarycookies \n\nwith open("path/to/cookies.binarycookies", "rb") as f:\n cookies = binarycookies.load(f)\n```\n\n#### Serialization\n\n```python\nimport binarycookies \n\ncookie = {\n "name": "session_id",\n "value": "abc123",\n "url": "https://example.com",\n "path": "/",\n "create_datetime": "2023-10-01T12:34:56+00:00",\n "expiry_datetime": "2023-12-31T23:59:59+00:00",\n "flag": "Secure"\n}\n\nwith open("path/to/cookies.binarycookies", "wb") as f:\n binarycookies.dump(cookie, f)\n```\n\n### Ethical Use & Responsible Handling\nThis project is intended for lawful, ethical use only. Typical, appropriate uses include:\n- Inspecting Binary Cookies from your own devices or data you are authorized to access\n- DFIR, QA, and security testing performed with explicit, written permission\n- Educational/research work on datasets that are owned by you, anonymized, or publicly released for that purpose\n\nYou must not use this tool to:\n- Access, extract, modify, or distribute cookies from systems or accounts you do not own or have permission to analyze\n- Bypass authentication, session management, DRM, or other technical controls\n- Enable tracking, stalking, doxxing, fraud, or other privacy-invasive or harmful activities\n\n\n### License\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n\n### Contributing\nContributions are welcome! If you find a bug or have a feature request, please open an issue on GitHub. Pull requests are also welcome.\n',
|
|
24
|
+
'author': 'Daniel Tom',
|
|
25
|
+
'author_email': 'd.e.tom89@gmail.com',
|
|
26
|
+
'maintainer': 'None',
|
|
27
|
+
'maintainer_email': 'None',
|
|
28
|
+
'url': 'None',
|
|
29
|
+
'package_dir': package_dir,
|
|
30
|
+
'packages': packages,
|
|
31
|
+
'package_data': package_data,
|
|
32
|
+
'install_requires': install_requires,
|
|
33
|
+
'entry_points': entry_points,
|
|
34
|
+
'python_requires': '>=3.8,<4.0',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
setup(**setup_kwargs)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from binarycookies._deserialize import load, loads
|
|
4
|
+
from binarycookies._serialize import dump, dumps
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("binarycookies")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
# Package is not installed, fallback to a default
|
|
10
|
+
__version__ = "0.0.0+unknown"
|
|
11
|
+
|
|
12
|
+
__all__ = ["dump", "dumps", "load", "loads"]
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import Type
|
|
3
|
+
from typing import Optional, Type
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
from rich import print as rprint
|
|
7
7
|
|
|
8
|
-
from binarycookies import load
|
|
8
|
+
from binarycookies import __version__, load
|
|
9
9
|
from binarycookies._output_handlers import OUTPUT_HANDLERS, OutputType
|
|
10
10
|
|
|
11
11
|
|
|
@@ -16,7 +16,24 @@ class DateTimeEncoder(json.JSONEncoder):
|
|
|
16
16
|
return super().default(obj)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
app = typer.Typer()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def version_callback(value: bool): # noqa: FBT001
|
|
23
|
+
"""Callback to print version and exit."""
|
|
24
|
+
if value:
|
|
25
|
+
rprint(f"binarycookies version {__version__}")
|
|
26
|
+
raise typer.Exit
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def cli(
|
|
31
|
+
file_path: str = typer.Argument(..., help="Path to binary cookies file"),
|
|
32
|
+
output: OutputType = typer.Option(OutputType.json, "--output", "-o", help="Output format"),
|
|
33
|
+
version: Optional[bool] = typer.Option( # noqa: ARG001
|
|
34
|
+
None, "--version", "-v", callback=version_callback, is_eager=True, help="Show version and exit"
|
|
35
|
+
),
|
|
36
|
+
):
|
|
20
37
|
"""CLI entrypoint for reading Binary Cookies"""
|
|
21
38
|
with open(file_path, "rb") as f:
|
|
22
39
|
cookies = load(f)
|
|
@@ -31,7 +48,7 @@ def cli(file_path: str, output: OutputType = OutputType.json):
|
|
|
31
48
|
|
|
32
49
|
def main():
|
|
33
50
|
"""CLI entrypoint for reading Binary Cookies"""
|
|
34
|
-
|
|
51
|
+
app()
|
|
35
52
|
|
|
36
53
|
|
|
37
54
|
if __name__ == "__main__":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
2
3
|
from io import BytesIO
|
|
3
4
|
from struct import unpack
|
|
4
5
|
from typing import BinaryIO, List, Union
|
|
@@ -26,21 +27,35 @@ def interpret_flag(flags: int) -> Flag:
|
|
|
26
27
|
return FLAGS.get(flags, Flag.UNKNOWN)
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
MAC_UNIX_OFFSET = 978307200 # Seconds from Unix epoch (1970) to Mac epoch (2001)
|
|
31
|
+
INT32_TIME_T_MAX = 2147483647 # Max signed 32-bit time_t (Unix timestamp)
|
|
32
|
+
INT32_CUTOFF_DT = datetime(2038, 1, 19, 3, 14, 7, tzinfo=timezone.utc)
|
|
33
|
+
|
|
34
|
+
|
|
29
35
|
def mac_epoch_to_date(epoch: int) -> datetime:
|
|
30
36
|
"""
|
|
31
|
-
|
|
32
|
-
overflows on both 32-bit and 64-bit systems.
|
|
33
|
-
"""
|
|
34
|
-
# The Mac epoch starts on 2001-01-01, which is 978307200 seconds after the Unix epoch.
|
|
35
|
-
unix_epoch = epoch + 978307200
|
|
37
|
+
Convert Mac epoch seconds (since 2001-01-01 UTC) to an aware datetime.
|
|
36
38
|
|
|
39
|
+
On 32-bit systems, clamp results that would overflow `time_t` to 2038-01-19T03:14:07Z.
|
|
40
|
+
On 64-bit systems, only clamp if the value exceeds datetime's range (-> datetime.max).
|
|
41
|
+
"""
|
|
42
|
+
base = datetime(2001, 1, 1, tzinfo=timezone.utc)
|
|
43
|
+
# Fast path: pure Python arithmetic (independent of C time_t)
|
|
37
44
|
try:
|
|
38
|
-
|
|
39
|
-
except
|
|
40
|
-
#
|
|
41
|
-
|
|
45
|
+
dt = base + timedelta(seconds=epoch)
|
|
46
|
+
except OverflowError:
|
|
47
|
+
# Beyond datetime range: choose appropriate cap
|
|
48
|
+
if sys.maxsize <= 2**31 - 1:
|
|
49
|
+
return INT32_CUTOFF_DT
|
|
42
50
|
return datetime.max.replace(tzinfo=timezone.utc)
|
|
43
51
|
|
|
52
|
+
# 32-bit clamp logic
|
|
53
|
+
if sys.maxsize <= 2**31 - 1:
|
|
54
|
+
unix_ts = epoch + MAC_UNIX_OFFSET
|
|
55
|
+
if unix_ts > INT32_TIME_T_MAX:
|
|
56
|
+
return INT32_CUTOFF_DT
|
|
57
|
+
return dt
|
|
58
|
+
|
|
44
59
|
|
|
45
60
|
def read_string(data: BytesIO, size: int) -> str:
|
|
46
61
|
"""Reads a string from binary file."""
|
|
@@ -71,6 +86,10 @@ def read_cookie(cookie: BytesIO, cookie_size: int) -> Cookie:
|
|
|
71
86
|
flag_int = read_field(cookie, cookie_fields.flag)
|
|
72
87
|
flag = interpret_flag(flag_int)
|
|
73
88
|
|
|
89
|
+
# Read comment offset at offset 32
|
|
90
|
+
cookie.seek(32)
|
|
91
|
+
comment_offset = unpack(Format.integer, cookie.read(4))[0] # noqa: F841
|
|
92
|
+
|
|
74
93
|
url_offset = read_field(cookie, cookie_fields.url_offset)
|
|
75
94
|
name_offset = read_field(cookie, cookie_fields.name_offset)
|
|
76
95
|
path_offset = read_field(cookie, cookie_fields.path_offset)
|
|
@@ -79,6 +98,7 @@ def read_cookie(cookie: BytesIO, cookie_size: int) -> Cookie:
|
|
|
79
98
|
expiry_datetime = mac_epoch_to_date(read_field(cookie, cookie_fields.expiry_date))
|
|
80
99
|
create_datetime = mac_epoch_to_date(read_field(cookie, cookie_fields.create_date))
|
|
81
100
|
|
|
101
|
+
# Read strings - comment comes first, then domain (url)
|
|
82
102
|
url = read_field(cookie, BcField(offset=url_offset, size=name_offset - url_offset, format=Format.string))
|
|
83
103
|
name = read_field(cookie, BcField(offset=name_offset, size=path_offset - name_offset, format=Format.string))
|
|
84
104
|
path = read_field(cookie, BcField(offset=path_offset, size=value_offset - path_offset, format=Format.string))
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from io import BufferedWriter, BytesIO
|
|
3
|
+
from struct import pack
|
|
4
|
+
from typing import BinaryIO, Dict, List, Tuple, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import __version__ as pydantic_version
|
|
7
|
+
|
|
8
|
+
from binarycookies._deserialize import FLAGS
|
|
9
|
+
from binarycookies.models import BcField, Cookie, CookieFields, FileFields, Format
|
|
10
|
+
|
|
11
|
+
IS_PYDANTIC_V1 = pydantic_version.startswith("1.")
|
|
12
|
+
|
|
13
|
+
CookiesCollection = Union[List[Dict], List[Cookie], Tuple[Dict], Tuple[Cookie], Cookie, Dict[str, str]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def date_to_mac_epoch(date: datetime) -> int:
|
|
17
|
+
"""Converts a datetime object to mac epoch time."""
|
|
18
|
+
mac_epoch_start = datetime(2001, 1, 1, tzinfo=timezone.utc)
|
|
19
|
+
return int((date - mac_epoch_start).total_seconds())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def write_string(data: BytesIO, value: str):
|
|
23
|
+
"""Writes a string to binary file."""
|
|
24
|
+
data.write(value.encode() + b"\x00")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write_field(data: BytesIO, field: BcField, value: Union[str, int]):
|
|
28
|
+
"""Writes a field to binary data."""
|
|
29
|
+
data.seek(field.offset)
|
|
30
|
+
if field.format == Format.string:
|
|
31
|
+
write_string(data, value)
|
|
32
|
+
else:
|
|
33
|
+
data.write(pack(field.format, value))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def serialize_cookie(cookie: Cookie) -> bytes:
|
|
37
|
+
"""Serializes a cookie object to binary format."""
|
|
38
|
+
cookie_fields = CookieFields()
|
|
39
|
+
|
|
40
|
+
# Pre-calculate the size to allocate buffer
|
|
41
|
+
# Cookie header is 60 bytes according to spec:
|
|
42
|
+
# 0-3: size, 4-7: unknownOne, 8-11: flags, 12-15: unknownTwo
|
|
43
|
+
# 16-19: domainOffset, 20-23: nameOffset, 24-27: pathOffset, 28-31: valueOffset
|
|
44
|
+
# 32-35: commentOffset, 36-39: endHeader
|
|
45
|
+
# 40-47: expires, 48-55: creation
|
|
46
|
+
# 56-59: comment (empty), 60+: domain, name, path, value strings
|
|
47
|
+
url_bytes = cookie.url.encode("utf-8")
|
|
48
|
+
name_bytes = cookie.name.encode("utf-8")
|
|
49
|
+
path_bytes = cookie.path.encode("utf-8")
|
|
50
|
+
value_bytes = cookie.value.encode("utf-8")
|
|
51
|
+
comment_bytes = b"" # Empty comment
|
|
52
|
+
|
|
53
|
+
# Each string has a null terminator
|
|
54
|
+
header_size = 60
|
|
55
|
+
strings_size = (
|
|
56
|
+
len(comment_bytes) + 1 + len(url_bytes) + 1 + len(name_bytes) + 1 + len(path_bytes) + 1 + len(value_bytes) + 1
|
|
57
|
+
)
|
|
58
|
+
total_size = header_size + strings_size
|
|
59
|
+
|
|
60
|
+
# Pre-allocate buffer with zeros
|
|
61
|
+
cookie_data = BytesIO(b"\x00" * total_size)
|
|
62
|
+
|
|
63
|
+
# Write flag
|
|
64
|
+
write_field(cookie_data, cookie_fields.flag, list(FLAGS.keys())[list(FLAGS.values()).index(cookie.flag)])
|
|
65
|
+
|
|
66
|
+
# Calculate offsets - strings start at byte 60 after header
|
|
67
|
+
comment_offset = 60
|
|
68
|
+
domain_offset = comment_offset + len(comment_bytes) + 1 # +1 for null terminator
|
|
69
|
+
name_offset = domain_offset + len(url_bytes) + 1
|
|
70
|
+
path_offset = name_offset + len(name_bytes) + 1
|
|
71
|
+
value_offset = path_offset + len(path_bytes) + 1
|
|
72
|
+
|
|
73
|
+
# Write offsets (note: spec calls it domain but code uses url)
|
|
74
|
+
write_field(cookie_data, cookie_fields.url_offset, domain_offset)
|
|
75
|
+
write_field(cookie_data, cookie_fields.name_offset, name_offset)
|
|
76
|
+
write_field(cookie_data, cookie_fields.path_offset, path_offset)
|
|
77
|
+
write_field(cookie_data, cookie_fields.value_offset, value_offset)
|
|
78
|
+
|
|
79
|
+
# Write commentOffset at offset 32
|
|
80
|
+
cookie_data.seek(32)
|
|
81
|
+
cookie_data.write(pack(Format.integer, comment_offset))
|
|
82
|
+
|
|
83
|
+
# Write endHeader marker at offset 36 (4 bytes of 0x00)
|
|
84
|
+
cookie_data.seek(36)
|
|
85
|
+
cookie_data.write(b"\x00\x00\x00\x00")
|
|
86
|
+
|
|
87
|
+
write_field(cookie_data, cookie_fields.expiry_date, date_to_mac_epoch(cookie.expiry_datetime))
|
|
88
|
+
write_field(cookie_data, cookie_fields.create_date, date_to_mac_epoch(cookie.create_datetime))
|
|
89
|
+
|
|
90
|
+
# Write string data starting at offset 60
|
|
91
|
+
cookie_data.seek(60)
|
|
92
|
+
# Write comment (empty string with null terminator)
|
|
93
|
+
write_string(cookie_data, "")
|
|
94
|
+
# Write domain (url), name, path, value
|
|
95
|
+
write_string(cookie_data, cookie.url)
|
|
96
|
+
write_string(cookie_data, cookie.name)
|
|
97
|
+
write_string(cookie_data, cookie.path)
|
|
98
|
+
write_string(cookie_data, cookie.value)
|
|
99
|
+
|
|
100
|
+
# Write size at the beginning
|
|
101
|
+
size = len(cookie_data.getvalue())
|
|
102
|
+
cookie_data.seek(0)
|
|
103
|
+
cookie_data.write(pack(Format.integer, size))
|
|
104
|
+
return cookie_data.getvalue()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def dump(cookies: CookiesCollection, f: Union[BufferedWriter, BytesIO, BinaryIO]):
|
|
108
|
+
"""Dumps a Binary Cookies object to create a binary cookies file.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
cookies: A Binary Cookies object to be serialized.
|
|
112
|
+
f: The file-like object to write the binary cookies data to.
|
|
113
|
+
"""
|
|
114
|
+
binary = dumps(cookies)
|
|
115
|
+
f.write(binary)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def calculate_checksum(page_data: bytes) -> int:
|
|
119
|
+
"""Calculates the checksum by summing every 4th byte of the page data.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
page_data: The raw bytes of a page.
|
|
123
|
+
Returns:
|
|
124
|
+
int: The checksum value.
|
|
125
|
+
"""
|
|
126
|
+
checksum = 0
|
|
127
|
+
# Sum every 4th byte (bytes at positions 0, 4, 8, 12, ...)
|
|
128
|
+
for i in range(0, len(page_data), 4):
|
|
129
|
+
checksum += page_data[i]
|
|
130
|
+
return checksum
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def dumps(cookies: CookiesCollection) -> bytes:
|
|
134
|
+
"""Dumps a Binary Cookies object to a byte string.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
cookies: A Binary Cookies object to be serialized.
|
|
138
|
+
Returns:
|
|
139
|
+
bytes: The serialized binary cookies data.
|
|
140
|
+
"""
|
|
141
|
+
if isinstance(cookies, dict):
|
|
142
|
+
cookies = [Cookie.parse_obj(cookies)] if IS_PYDANTIC_V1 else [Cookie.model_validate(cookies)]
|
|
143
|
+
elif isinstance(cookies, (list, tuple)):
|
|
144
|
+
if IS_PYDANTIC_V1:
|
|
145
|
+
cookies = [Cookie.parse_obj(cookie) for cookie in cookies]
|
|
146
|
+
else:
|
|
147
|
+
cookies = [Cookie.model_validate(cookie) for cookie in cookies]
|
|
148
|
+
elif isinstance(cookies, Cookie):
|
|
149
|
+
cookies = [cookies]
|
|
150
|
+
else:
|
|
151
|
+
raise TypeError("Invalid type for cookies. Expected dict, list, tuple, or Cookie.")
|
|
152
|
+
|
|
153
|
+
file_fields = FileFields()
|
|
154
|
+
|
|
155
|
+
data = BytesIO()
|
|
156
|
+
|
|
157
|
+
# Write file header (4 bytes: "cook")
|
|
158
|
+
data.write(b"cook")
|
|
159
|
+
|
|
160
|
+
# Number of pages (1 for simplicity, big-endian)
|
|
161
|
+
write_field(data, file_fields.num_pages, 1)
|
|
162
|
+
|
|
163
|
+
# Write page size pointer
|
|
164
|
+
data.write(pack(Format.integer, 0)) # Placeholder, will be updated
|
|
165
|
+
|
|
166
|
+
# Store the position where page data starts
|
|
167
|
+
page_start_offset = data.tell()
|
|
168
|
+
page_data = BytesIO()
|
|
169
|
+
|
|
170
|
+
# Write pageStart marker (4 bytes) - Must be 0x00, 0x01, 0x00, 0x00
|
|
171
|
+
page_data.write(b"\x00\x01\x00\x00")
|
|
172
|
+
|
|
173
|
+
# Write number of cookies in the page
|
|
174
|
+
page_data.write(pack(Format.integer, len(cookies)))
|
|
175
|
+
|
|
176
|
+
cookie_data_list = []
|
|
177
|
+
# Serialize cookies
|
|
178
|
+
for cookie in cookies:
|
|
179
|
+
cookie_data_list.append(serialize_cookie(cookie))
|
|
180
|
+
|
|
181
|
+
# Calculate where cookie data will start:
|
|
182
|
+
# current position + (num_cookies * 4 bytes for offsets) + 4 bytes for pageEnd marker
|
|
183
|
+
initial_cookie_offset = page_data.tell() + (len(cookies) * 4) + 4
|
|
184
|
+
initial_cookie = True
|
|
185
|
+
previous_sizes = 0
|
|
186
|
+
|
|
187
|
+
# Write cookie offsets
|
|
188
|
+
for cookie_data in cookie_data_list:
|
|
189
|
+
if initial_cookie:
|
|
190
|
+
page_data.write(pack(Format.integer, initial_cookie_offset))
|
|
191
|
+
initial_cookie = False
|
|
192
|
+
else:
|
|
193
|
+
page_data.write(pack(Format.integer, previous_sizes + initial_cookie_offset))
|
|
194
|
+
|
|
195
|
+
previous_sizes += len(cookie_data)
|
|
196
|
+
|
|
197
|
+
# Write pageEnd marker (4 bytes) - Must be 0x00, 0x00, 0x00, 0x00
|
|
198
|
+
page_data.write(b"\x00\x00\x00\x00")
|
|
199
|
+
|
|
200
|
+
# Write cookie data
|
|
201
|
+
for cookie_data in cookie_data_list:
|
|
202
|
+
page_data.write(cookie_data)
|
|
203
|
+
|
|
204
|
+
# Get the complete page data
|
|
205
|
+
page_bytes = page_data.getvalue()
|
|
206
|
+
page_size = len(page_bytes)
|
|
207
|
+
|
|
208
|
+
# Update page size in the file header (big-endian format for page sizes)
|
|
209
|
+
data.seek(8)
|
|
210
|
+
data.write(pack(Format.integer_be, page_size))
|
|
211
|
+
|
|
212
|
+
# Write the page data
|
|
213
|
+
data.seek(page_start_offset)
|
|
214
|
+
data.write(page_bytes)
|
|
215
|
+
|
|
216
|
+
# Calculate and write checksum after all pages
|
|
217
|
+
# The checksum is the sum of every 4th byte of the page data
|
|
218
|
+
# Specification says 8 bytes, so we write it as a 64-bit integer
|
|
219
|
+
checksum = calculate_checksum(page_bytes)
|
|
220
|
+
data.write(pack("<Q", checksum)) # LE_uint64 (8 bytes)
|
|
221
|
+
|
|
222
|
+
return data.getvalue()
|
|
@@ -20,6 +20,18 @@ class Flag(str, Enum):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class Cookie(BaseModel):
|
|
23
|
+
"""Represents a cookie in the binary cookies format.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
name (str): The name of the cookie.
|
|
27
|
+
value (str): The value of the cookie.
|
|
28
|
+
url (str): The URL associated with the cookie.
|
|
29
|
+
path (str): The path for which the cookie is valid.
|
|
30
|
+
create_datetime (datetime): The creation date and time of the cookie.
|
|
31
|
+
expiry_datetime (datetime): The expiration date and time of the cookie.
|
|
32
|
+
flag (Flag): The flags associated with the cookie, such as Secure or HttpOnly.
|
|
33
|
+
"""
|
|
34
|
+
|
|
23
35
|
name: str
|
|
24
36
|
value: str
|
|
25
37
|
url: str
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
2
|
-
from io import BufferedWriter, BytesIO
|
|
3
|
-
from struct import pack
|
|
4
|
-
from typing import BinaryIO, Dict, List, Tuple, Union
|
|
5
|
-
|
|
6
|
-
from pydantic import __version__ as pydantic_version
|
|
7
|
-
|
|
8
|
-
from binarycookies._deserialize import FLAGS
|
|
9
|
-
from binarycookies.models import BcField, Cookie, CookieFields, FileFields, Format
|
|
10
|
-
|
|
11
|
-
IS_PYDANTIC_V1 = pydantic_version.startswith("1.")
|
|
12
|
-
|
|
13
|
-
CookiesCollection = Union[List[Dict], List[Cookie], Tuple[Dict], Tuple[Cookie], Cookie, Dict[str, str]]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def date_to_mac_epoch(date: datetime) -> int:
|
|
17
|
-
"""Converts a datetime object to mac epoch time."""
|
|
18
|
-
mac_epoch_start = datetime(2001, 1, 1, tzinfo=timezone.utc)
|
|
19
|
-
return int((date - mac_epoch_start).total_seconds())
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def write_string(data: BytesIO, value: str):
|
|
23
|
-
"""Writes a string to binary file."""
|
|
24
|
-
data.write(value.encode() + b"\x00")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def write_field(data: BytesIO, field: BcField, value: Union[str, int]):
|
|
28
|
-
"""Writes a field to binary data."""
|
|
29
|
-
data.seek(field.offset)
|
|
30
|
-
if field.format == Format.string:
|
|
31
|
-
write_string(data, value)
|
|
32
|
-
else:
|
|
33
|
-
data.write(pack(field.format, value))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def serialize_cookie(cookie: Cookie) -> bytes:
|
|
37
|
-
"""Serializes a cookie object to binary format."""
|
|
38
|
-
cookie_data = BytesIO()
|
|
39
|
-
cookie_fields = CookieFields()
|
|
40
|
-
# Write flag
|
|
41
|
-
write_field(cookie_data, cookie_fields.flag, list(FLAGS.keys())[list(FLAGS.values()).index(cookie.flag)])
|
|
42
|
-
|
|
43
|
-
# Calculate offsets
|
|
44
|
-
url_offset = 56 # The actual cookies content always starts at byte 56
|
|
45
|
-
name_offset = 1 + url_offset + len(cookie.url.encode("utf-8"))
|
|
46
|
-
path_offset = 1 + name_offset + len(cookie.name.encode("utf-8"))
|
|
47
|
-
value_offset = 1 + path_offset + len(cookie.path.encode("utf-8"))
|
|
48
|
-
|
|
49
|
-
write_field(cookie_data, cookie_fields.url_offset, url_offset)
|
|
50
|
-
write_field(cookie_data, cookie_fields.name_offset, name_offset)
|
|
51
|
-
write_field(cookie_data, cookie_fields.path_offset, path_offset)
|
|
52
|
-
write_field(cookie_data, cookie_fields.value_offset, value_offset)
|
|
53
|
-
|
|
54
|
-
write_field(cookie_data, cookie_fields.expiry_date, date_to_mac_epoch(cookie.expiry_datetime))
|
|
55
|
-
write_field(cookie_data, cookie_fields.create_date, date_to_mac_epoch(cookie.create_datetime))
|
|
56
|
-
|
|
57
|
-
# Write cookie data
|
|
58
|
-
write_string(cookie_data, cookie.url)
|
|
59
|
-
write_string(cookie_data, cookie.name)
|
|
60
|
-
write_string(cookie_data, cookie.path)
|
|
61
|
-
write_string(cookie_data, cookie.value)
|
|
62
|
-
|
|
63
|
-
# Write size at the beginning
|
|
64
|
-
size = len(cookie_data.getvalue())
|
|
65
|
-
cookie_data.seek(0)
|
|
66
|
-
cookie_data.write(pack(Format.integer, size))
|
|
67
|
-
return cookie_data.getvalue()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def dump(cookies: CookiesCollection, f: Union[BufferedWriter, BytesIO, BinaryIO]):
|
|
71
|
-
"""Dumps a Binary Cookies object to create a binary cookies file.k
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
cookies: A Binary Cookies object to be serialized.
|
|
75
|
-
f: The file-like object to write the binary cookies data to.
|
|
76
|
-
"""
|
|
77
|
-
binary = dumps(cookies)
|
|
78
|
-
f.write(binary)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def dumps(cookies: CookiesCollection) -> bytes:
|
|
82
|
-
"""Dumps a Binary Cookies object to a byte string.
|
|
83
|
-
Args:
|
|
84
|
-
cookies: A Binary Cookies object to be serialized.
|
|
85
|
-
Returns:
|
|
86
|
-
bytes: The serialized binary cookies data.
|
|
87
|
-
"""
|
|
88
|
-
if isinstance(cookies, dict):
|
|
89
|
-
cookies = [Cookie.parse_obj(cookies)] if IS_PYDANTIC_V1 else [Cookie.model_validate(cookies)]
|
|
90
|
-
elif isinstance(cookies, (list, tuple)):
|
|
91
|
-
if IS_PYDANTIC_V1:
|
|
92
|
-
cookies = [Cookie.parse_obj(cookie) for cookie in cookies]
|
|
93
|
-
else:
|
|
94
|
-
cookies = [Cookie.model_validate(cookie) for cookie in cookies]
|
|
95
|
-
elif isinstance(cookies, Cookie):
|
|
96
|
-
cookies = [cookies]
|
|
97
|
-
else:
|
|
98
|
-
raise TypeError("Invalid type for cookies. Expected dict, list, tuple, or Cookie.")
|
|
99
|
-
|
|
100
|
-
file_fields = FileFields()
|
|
101
|
-
|
|
102
|
-
data = BytesIO()
|
|
103
|
-
|
|
104
|
-
# Write file header
|
|
105
|
-
write_field(data, file_fields.header, "cook")
|
|
106
|
-
|
|
107
|
-
# Number of pages (1 for simplicity)
|
|
108
|
-
write_field(data, file_fields.num_pages, 1)
|
|
109
|
-
|
|
110
|
-
# Write number of cookies
|
|
111
|
-
data.write(pack(Format.integer, len(cookies)))
|
|
112
|
-
|
|
113
|
-
# Placeholder for page size
|
|
114
|
-
page_size_offset = data.tell()
|
|
115
|
-
data.write(b"\x00\x00\x00\x00")
|
|
116
|
-
|
|
117
|
-
# Write number of cookies
|
|
118
|
-
data.write(pack(Format.integer, len(cookies)))
|
|
119
|
-
cookie_data_list = []
|
|
120
|
-
# Write cookies
|
|
121
|
-
for cookie in cookies:
|
|
122
|
-
cookie_data_list.append(serialize_cookie(cookie))
|
|
123
|
-
|
|
124
|
-
initial_cookie_offset = data.tell() + (len(cookies) * 4)
|
|
125
|
-
initial_cookie = True
|
|
126
|
-
previous_sizes = 0
|
|
127
|
-
for cookie_data in cookie_data_list:
|
|
128
|
-
if initial_cookie:
|
|
129
|
-
data.write(pack(Format.integer, initial_cookie_offset))
|
|
130
|
-
initial_cookie = False
|
|
131
|
-
else:
|
|
132
|
-
data.write(pack(Format.integer, previous_sizes + initial_cookie_offset))
|
|
133
|
-
|
|
134
|
-
previous_sizes += len(cookie_data)
|
|
135
|
-
|
|
136
|
-
# Unknown data
|
|
137
|
-
data.write(b"\x00\x00\x00\x00")
|
|
138
|
-
data.write(b"\x00\x00\x00\x00")
|
|
139
|
-
data.write(b"\x00\x00\x00\x00")
|
|
140
|
-
|
|
141
|
-
for cookie_data in cookie_data_list:
|
|
142
|
-
data.write(cookie_data)
|
|
143
|
-
|
|
144
|
-
# Update page size
|
|
145
|
-
page_size = data.tell()
|
|
146
|
-
data.seek(page_size_offset)
|
|
147
|
-
data.write(pack(Format.integer, page_size))
|
|
148
|
-
|
|
149
|
-
return data.getvalue()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|