binarycookies 2.2.3__tar.gz → 2.4.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.
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: binarycookies
3
- Version: 2.2.3
3
+ Version: 2.4.0
4
4
  Summary: Python Binary Cookies (de)serializer
5
5
  Author: Daniel Tom
6
6
  Author-email: d.e.tom89@gmail.com
7
- Requires-Python: >=3.8,<4.0
7
+ Requires-Python: >=3.9,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.8
10
9
  Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Dist: pydantic (>=2.0.0,<3.0.0)
14
- Requires-Dist: typer (>=0.12.3,<0.20.0)
14
+ Requires-Dist: typer (>=0.12.3,<0.21.0)
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  [![Github Actions Status](https://github.com/dan1elt0m/binary-cookies-reader/workflows/test/badge.svg)](https://github.com/dan1elt0m/binary-cookies-reader/actions/workflows/test.yml)
@@ -49,7 +49,9 @@ Output:
49
49
  "path": "/",
50
50
  "create_datetime": "2023-10-01T12:34:56+00:00",
51
51
  "expiry_datetime": "2023-12-31T23:59:59+00:00",
52
- "flag": "Secure"
52
+ "flag": "Secure",
53
+ "raw_flags": 1,
54
+ "comment": null
53
55
  },
54
56
  {
55
57
  "name": "user_token",
@@ -58,7 +60,9 @@ Output:
58
60
  "path": "/account",
59
61
  "create_datetime": "2023-10-01T12:34:56+00:00",
60
62
  "expiry_datetime": "2023-12-31T23:59:59+00:00",
61
- "flag": "HttpOnly"
63
+ "flag": "HttpOnly",
64
+ "raw_flags": 4,
65
+ "comment": null
62
66
  }
63
67
  ]
64
68
  ```
@@ -87,7 +91,7 @@ import binarycookies
87
91
  cookie = {
88
92
  "name": "session_id",
89
93
  "value": "abc123",
90
- "url": "https://example.com",
94
+ "url": "https://example.com", # "domain" is accepted as an alias for "url"
91
95
  "path": "/",
92
96
  "create_datetime": "2023-10-01T12:34:56+00:00",
93
97
  "expiry_datetime": "2023-12-31T23:59:59+00:00",
@@ -98,6 +102,13 @@ with open("path/to/cookies.binarycookies", "wb") as f:
98
102
  binarycookies.dump(cookie, f)
99
103
  ```
100
104
 
105
+ Optional fields:
106
+ - `comment`: cookie comment string stored in the file (default: no comment)
107
+ - `raw_flags`: the raw flags bitfield written to disk. When omitted, it is derived
108
+ from `flag` (`Secure` = 1, `HttpOnly` = 4, `Secure; HttpOnly` = 5). Cookies read
109
+ with `load`/`loads` always carry `raw_flags`, so unknown flag bits survive a
110
+ read-modify-write round trip.
111
+
101
112
  ### Ethical Use & Responsible Handling
102
113
  This project is intended for lawful, ethical use only. Typical, appropriate uses include:
103
114
  - Inspecting Binary Cookies from your own devices or data you are authorized to access
@@ -33,7 +33,9 @@ Output:
33
33
  "path": "/",
34
34
  "create_datetime": "2023-10-01T12:34:56+00:00",
35
35
  "expiry_datetime": "2023-12-31T23:59:59+00:00",
36
- "flag": "Secure"
36
+ "flag": "Secure",
37
+ "raw_flags": 1,
38
+ "comment": null
37
39
  },
38
40
  {
39
41
  "name": "user_token",
@@ -42,7 +44,9 @@ Output:
42
44
  "path": "/account",
43
45
  "create_datetime": "2023-10-01T12:34:56+00:00",
44
46
  "expiry_datetime": "2023-12-31T23:59:59+00:00",
45
- "flag": "HttpOnly"
47
+ "flag": "HttpOnly",
48
+ "raw_flags": 4,
49
+ "comment": null
46
50
  }
47
51
  ]
48
52
  ```
@@ -71,7 +75,7 @@ import binarycookies
71
75
  cookie = {
72
76
  "name": "session_id",
73
77
  "value": "abc123",
74
- "url": "https://example.com",
78
+ "url": "https://example.com", # "domain" is accepted as an alias for "url"
75
79
  "path": "/",
76
80
  "create_datetime": "2023-10-01T12:34:56+00:00",
77
81
  "expiry_datetime": "2023-12-31T23:59:59+00:00",
@@ -82,6 +86,13 @@ with open("path/to/cookies.binarycookies", "wb") as f:
82
86
  binarycookies.dump(cookie, f)
83
87
  ```
84
88
 
89
+ Optional fields:
90
+ - `comment`: cookie comment string stored in the file (default: no comment)
91
+ - `raw_flags`: the raw flags bitfield written to disk. When omitted, it is derived
92
+ from `flag` (`Secure` = 1, `HttpOnly` = 4, `Secure; HttpOnly` = 5). Cookies read
93
+ with `load`/`loads` always carry `raw_flags`, so unknown flag bits survive a
94
+ read-modify-write round trip.
95
+
85
96
  ### Ethical Use & Responsible Handling
86
97
  This project is intended for lawful, ethical use only. Typical, appropriate uses include:
87
98
  - Inspecting Binary Cookies from your own devices or data you are authorized to access
@@ -1,14 +1,14 @@
1
1
  [tool.poetry]
2
2
  name = "binarycookies"
3
- version = "2.2.3"
3
+ version = "2.4.0"
4
4
  description = "Python Binary Cookies (de)serializer"
5
5
  authors = ["Daniel Tom <d.e.tom89@gmail.com>"]
6
6
  readme = "README.md"
7
7
  packages = [{include = "binarycookies", from="src"}]
8
8
 
9
9
  [tool.poetry.dependencies]
10
- python = ">=3.8,<4.0"
11
- typer = ">=0.12.3,<0.20.0"
10
+ python = ">=3.9,<4.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,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
- def cli(file_path: str, output: OutputType = OutputType.json):
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
- typer.run(cli)
51
+ app()
35
52
 
36
53
 
37
54
  if __name__ == "__main__":
@@ -14,6 +14,10 @@ from binarycookies.models import (
14
14
  Format,
15
15
  )
16
16
 
17
+ SECURE_BIT = 0x1
18
+ HTTP_ONLY_BIT = 0x4
19
+
20
+ # Kept for backwards compatibility; interpret_flag treats flags as a bitfield instead
17
21
  FLAGS = {
18
22
  0: Flag.UNKNOWN,
19
23
  1: Flag.SECURE,
@@ -23,8 +27,16 @@ FLAGS = {
23
27
 
24
28
 
25
29
  def interpret_flag(flags: int) -> Flag:
26
- """Interprets the flags of a cookie and returns a human-readable string."""
27
- return FLAGS.get(flags, Flag.UNKNOWN)
30
+ """Interprets the Secure/HttpOnly bits of the raw flags bitfield."""
31
+ secure = bool(flags & SECURE_BIT)
32
+ http_only = bool(flags & HTTP_ONLY_BIT)
33
+ if secure and http_only:
34
+ return Flag.SECURE_HTTPONLY
35
+ if secure:
36
+ return Flag.SECURE
37
+ if http_only:
38
+ return Flag.HTTPONLY
39
+ return Flag.UNKNOWN
28
40
 
29
41
 
30
42
  MAC_UNIX_OFFSET = 978307200 # Seconds from Unix epoch (1970) to Mac epoch (2001)
@@ -58,17 +70,12 @@ def mac_epoch_to_date(epoch: int) -> datetime:
58
70
 
59
71
 
60
72
  def read_string(data: BytesIO, size: int) -> str:
61
- """Reads a string from binary file."""
62
- result = ""
63
- count = 0
64
- c = data.read(1)
65
- while unpack("<b", c)[0] != 0:
66
- count += 1
67
- if count > size:
68
- break
69
- result += str(c.decode())
70
- c = data.read(1)
71
- return result
73
+ """Reads a null-terminated UTF-8 string of at most `size` bytes from binary data."""
74
+ raw = data.read(size)
75
+ end = raw.find(b"\x00")
76
+ if end != -1:
77
+ raw = raw[:end]
78
+ return raw.decode("utf-8")
72
79
 
73
80
 
74
81
  def read_field(data: BytesIO, field: BcField) -> Union[str, int]:
@@ -83,8 +90,12 @@ def read_cookie(cookie: BytesIO, cookie_size: int) -> Cookie:
83
90
  """Reads a cookie from the given offset in the page."""
84
91
 
85
92
  cookie_fields = CookieFields()
86
- flag_int = read_field(cookie, cookie_fields.flag)
87
- flag = interpret_flag(flag_int)
93
+ raw_flags = read_field(cookie, cookie_fields.flag)
94
+ flag = interpret_flag(raw_flags)
95
+
96
+ # Read comment offset at offset 32 (0 means the cookie has no comment)
97
+ cookie.seek(32)
98
+ comment_offset = unpack(Format.integer, cookie.read(4))[0]
88
99
 
89
100
  url_offset = read_field(cookie, cookie_fields.url_offset)
90
101
  name_offset = read_field(cookie, cookie_fields.name_offset)
@@ -94,6 +105,13 @@ def read_cookie(cookie: BytesIO, cookie_size: int) -> Cookie:
94
105
  expiry_datetime = mac_epoch_to_date(read_field(cookie, cookie_fields.expiry_date))
95
106
  create_datetime = mac_epoch_to_date(read_field(cookie, cookie_fields.create_date))
96
107
 
108
+ # Read strings - the comment (if any) comes first, then domain (url)
109
+ comment = None
110
+ if comment_offset > 0:
111
+ comment_end = url_offset if comment_offset <= url_offset else cookie_size
112
+ comment = read_field(
113
+ cookie, BcField(offset=comment_offset, size=comment_end - comment_offset, format=Format.string)
114
+ )
97
115
  url = read_field(cookie, BcField(offset=url_offset, size=name_offset - url_offset, format=Format.string))
98
116
  name = read_field(cookie, BcField(offset=name_offset, size=path_offset - name_offset, format=Format.string))
99
117
  path = read_field(cookie, BcField(offset=path_offset, size=value_offset - path_offset, format=Format.string))
@@ -107,6 +125,8 @@ def read_cookie(cookie: BytesIO, cookie_size: int) -> Cookie:
107
125
  create_datetime=create_datetime,
108
126
  expiry_datetime=expiry_datetime,
109
127
  flag=flag,
128
+ raw_flags=raw_flags,
129
+ comment=comment,
110
130
  )
111
131
 
112
132
 
@@ -0,0 +1,215 @@
1
+ import plistlib
2
+ from datetime import datetime, timezone
3
+ from io import BufferedWriter, BytesIO
4
+ from struct import pack
5
+ from typing import BinaryIO, Dict, List, Tuple, Union
6
+
7
+ from pydantic import __version__ as pydantic_version
8
+
9
+ from binarycookies.models import BcField, Cookie, CookieFields, FileFields, Flag, 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
+ # Every page starts with these 4 bytes (0x00000100 stored big-endian)
16
+ PAGE_HEADER = b"\x00\x00\x01\x00"
17
+ # 4 zero bytes terminate the page's cookie offset table
18
+ PAGE_FOOTER = b"\x00\x00\x00\x00"
19
+ # 8-byte magic that follows the checksum at the end of the file
20
+ FILE_FOOTER = b"\x07\x17\x20\x05\x00\x00\x00\x4b"
21
+ # Cookie records without comments have a 56-byte fixed header; strings follow
22
+ COOKIE_HEADER_SIZE = 56
23
+ # Value Safari stores in the trailing NSHTTPCookieAcceptPolicy plist
24
+ DEFAULT_COOKIE_ACCEPT_POLICY = 2
25
+ # Raw flag bitfield written when a cookie has no raw_flags set
26
+ FLAG_VALUES = {Flag.UNKNOWN: 0, Flag.SECURE: 1, Flag.HTTPONLY: 4, Flag.SECURE_HTTPONLY: 5}
27
+
28
+
29
+ def date_to_mac_epoch(date: datetime) -> int:
30
+ """Converts a datetime object to mac epoch time. Naive datetimes are assumed to be UTC."""
31
+ if date.tzinfo is None:
32
+ date = date.replace(tzinfo=timezone.utc)
33
+ mac_epoch_start = datetime(2001, 1, 1, tzinfo=timezone.utc)
34
+ return int((date - mac_epoch_start).total_seconds())
35
+
36
+
37
+ def write_string(data: BytesIO, value: str):
38
+ """Writes a string to binary file."""
39
+ data.write(value.encode() + b"\x00")
40
+
41
+
42
+ def write_field(data: BytesIO, field: BcField, value: Union[str, int]):
43
+ """Writes a field to binary data."""
44
+ data.seek(field.offset)
45
+ if field.format == Format.string:
46
+ write_string(data, value)
47
+ else:
48
+ data.write(pack(field.format, value))
49
+
50
+
51
+ def serialize_cookie(cookie: Cookie) -> bytes:
52
+ """Serializes a cookie object to Apple's binary cookie record format."""
53
+ cookie_fields = CookieFields()
54
+
55
+ # Cookie record layout (matches Safari/CFNetwork):
56
+ # 0-3: size, 4-7: unknownOne, 8-11: flags, 12-15: unknownTwo
57
+ # 16-19: domainOffset, 20-23: nameOffset, 24-27: pathOffset, 28-31: valueOffset
58
+ # 32-35: commentOffset (0 = no comment), 36-39: commentURLOffset (0 = none)
59
+ # 40-47: expires (float64 LE), 48-55: creation (float64 LE)
60
+ # 56+: null-terminated comment (if any), domain, name, path, value strings
61
+ url_bytes = cookie.url.encode("utf-8")
62
+ name_bytes = cookie.name.encode("utf-8")
63
+ path_bytes = cookie.path.encode("utf-8")
64
+ value_bytes = cookie.value.encode("utf-8")
65
+ comment_bytes = cookie.comment.encode("utf-8") if cookie.comment else None
66
+
67
+ # Each string has a null terminator
68
+ strings_size = len(url_bytes) + 1 + len(name_bytes) + 1 + len(path_bytes) + 1 + len(value_bytes) + 1
69
+ if comment_bytes is not None:
70
+ strings_size += len(comment_bytes) + 1
71
+ total_size = COOKIE_HEADER_SIZE + strings_size
72
+
73
+ # Pre-allocate buffer with zeros; the comment URL offset at 36 stays 0
74
+ cookie_data = BytesIO(b"\x00" * total_size)
75
+
76
+ # Write size and raw flags (falling back to the bits implied by the flag enum)
77
+ cookie_data.write(pack(Format.integer, total_size))
78
+ raw_flags = cookie.raw_flags if cookie.raw_flags is not None else FLAG_VALUES[cookie.flag]
79
+ write_field(cookie_data, cookie_fields.flag, raw_flags)
80
+
81
+ # Calculate offsets - strings start right after the fixed header, comment first
82
+ comment_offset = COOKIE_HEADER_SIZE if comment_bytes is not None else 0
83
+ domain_offset = COOKIE_HEADER_SIZE if comment_bytes is None else comment_offset + len(comment_bytes) + 1
84
+ name_offset = domain_offset + len(url_bytes) + 1 # +1 for null terminator
85
+ path_offset = name_offset + len(name_bytes) + 1
86
+ value_offset = path_offset + len(path_bytes) + 1
87
+
88
+ # Write offsets (note: spec calls it domain but code uses url)
89
+ write_field(cookie_data, cookie_fields.url_offset, domain_offset)
90
+ write_field(cookie_data, cookie_fields.name_offset, name_offset)
91
+ write_field(cookie_data, cookie_fields.path_offset, path_offset)
92
+ write_field(cookie_data, cookie_fields.value_offset, value_offset)
93
+
94
+ if comment_offset:
95
+ cookie_data.seek(32)
96
+ cookie_data.write(pack(Format.integer, comment_offset))
97
+
98
+ write_field(cookie_data, cookie_fields.expiry_date, date_to_mac_epoch(cookie.expiry_datetime))
99
+ write_field(cookie_data, cookie_fields.create_date, date_to_mac_epoch(cookie.create_datetime))
100
+
101
+ # Write comment (if any), domain (url), name, path, value strings
102
+ cookie_data.seek(COOKIE_HEADER_SIZE)
103
+ if cookie.comment:
104
+ write_string(cookie_data, cookie.comment)
105
+ write_string(cookie_data, cookie.url)
106
+ write_string(cookie_data, cookie.name)
107
+ write_string(cookie_data, cookie.path)
108
+ write_string(cookie_data, cookie.value)
109
+
110
+ return cookie_data.getvalue()
111
+
112
+
113
+ def serialize_page(cookies: List[Cookie]) -> bytes:
114
+ """Serializes a list of cookies into a single page in Apple's binary cookies format."""
115
+ cookie_data_list = [serialize_cookie(cookie) for cookie in cookies]
116
+
117
+ page = BytesIO()
118
+ page.write(PAGE_HEADER)
119
+ page.write(pack(Format.integer, len(cookie_data_list)))
120
+
121
+ # Cookie offsets are relative to the page start:
122
+ # 8-byte page header + 4 bytes per offset + 4-byte page footer
123
+ cookie_offset = 8 + (len(cookie_data_list) * 4) + 4
124
+ for cookie_data in cookie_data_list:
125
+ page.write(pack(Format.integer, cookie_offset))
126
+ cookie_offset += len(cookie_data)
127
+
128
+ page.write(PAGE_FOOTER)
129
+
130
+ for cookie_data in cookie_data_list:
131
+ page.write(cookie_data)
132
+
133
+ return page.getvalue()
134
+
135
+
136
+ def dump(cookies: CookiesCollection, f: Union[BufferedWriter, BytesIO, BinaryIO]):
137
+ """Dumps a Binary Cookies object to create a binary cookies file.
138
+
139
+ Args:
140
+ cookies: A Binary Cookies object to be serialized.
141
+ f: The file-like object to write the binary cookies data to.
142
+ """
143
+ binary = dumps(cookies)
144
+ f.write(binary)
145
+
146
+
147
+ def calculate_checksum(page_data: bytes) -> int:
148
+ """Calculates the checksum by summing every 4th byte of the page data.
149
+
150
+ Args:
151
+ page_data: The raw bytes of a page.
152
+ Returns:
153
+ int: The checksum value.
154
+ """
155
+ checksum = 0
156
+ # Sum every 4th byte (bytes at positions 0, 4, 8, 12, ...)
157
+ for i in range(0, len(page_data), 4):
158
+ checksum += page_data[i]
159
+ return checksum
160
+
161
+
162
+ def _as_cookie(cookie: Union[Dict, Cookie]) -> Cookie:
163
+ """Coerces a dict (accepting `domain` as an alias for `url`) into a Cookie."""
164
+ if isinstance(cookie, Cookie):
165
+ return cookie
166
+ if isinstance(cookie, dict) and "url" not in cookie and "domain" in cookie:
167
+ cookie = {**cookie}
168
+ cookie["url"] = cookie.pop("domain")
169
+ return Cookie.parse_obj(cookie) if IS_PYDANTIC_V1 else Cookie.model_validate(cookie)
170
+
171
+
172
+ def dumps(cookies: CookiesCollection) -> bytes:
173
+ """Dumps a Binary Cookies object to a byte string.
174
+
175
+ Args:
176
+ cookies: A Binary Cookies object to be serialized.
177
+ Returns:
178
+ bytes: The serialized binary cookies data.
179
+ """
180
+ if isinstance(cookies, (dict, Cookie)):
181
+ cookies = [cookies]
182
+ elif not isinstance(cookies, (list, tuple)):
183
+ raise TypeError("Invalid type for cookies. Expected dict, list, tuple, or Cookie.")
184
+ cookies = [_as_cookie(cookie) for cookie in cookies]
185
+
186
+ file_fields = FileFields()
187
+
188
+ # Safari stores one page per domain, in first-seen order
189
+ cookies_by_domain: Dict[str, List[Cookie]] = {}
190
+ for cookie in cookies:
191
+ cookies_by_domain.setdefault(cookie.url, []).append(cookie)
192
+ pages = [serialize_page(domain_cookies) for domain_cookies in cookies_by_domain.values()]
193
+
194
+ data = BytesIO()
195
+
196
+ # Write file header (4 bytes: "cook")
197
+ data.write(b"cook")
198
+
199
+ # Number of pages (big-endian)
200
+ write_field(data, file_fields.num_pages, len(pages))
201
+
202
+ # Page sizes (big-endian), followed by the page data itself
203
+ for page in pages:
204
+ data.write(pack(Format.integer_be, len(page)))
205
+ for page in pages:
206
+ data.write(page)
207
+
208
+ # File tail: 4-byte big-endian checksum over every 4th byte of each page,
209
+ # the 8-byte footer magic, and a binary plist with the cookie accept policy
210
+ checksum = sum(calculate_checksum(page) for page in pages)
211
+ data.write(pack(">I", checksum & 0xFFFFFFFF))
212
+ data.write(FILE_FOOTER)
213
+ data.write(plistlib.dumps({"NSHTTPCookieAcceptPolicy": DEFAULT_COOKIE_ACCEPT_POLICY}, fmt=plistlib.FMT_BINARY))
214
+
215
+ return data.getvalue()
@@ -1,5 +1,6 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum
3
+ from typing import Optional
3
4
 
4
5
  from pydantic import BaseModel
5
6
 
@@ -25,11 +26,14 @@ class Cookie(BaseModel):
25
26
  Attributes:
26
27
  name (str): The name of the cookie.
27
28
  value (str): The value of the cookie.
28
- url (str): The URL associated with the cookie.
29
+ url (str): The domain the cookie belongs to (also exposed as `domain`).
29
30
  path (str): The path for which the cookie is valid.
30
31
  create_datetime (datetime): The creation date and time of the cookie.
31
32
  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
+ flag (Flag): Human-readable interpretation of the Secure/HttpOnly flag bits.
34
+ raw_flags (Optional[int]): The raw flags bitfield as stored on disk. Populated when
35
+ reading so unknown bits survive a round trip; derived from `flag` when absent.
36
+ comment (Optional[str]): The optional comment stored with the cookie.
33
37
  """
34
38
 
35
39
  name: str
@@ -39,6 +43,13 @@ class Cookie(BaseModel):
39
43
  create_datetime: datetime
40
44
  expiry_datetime: datetime
41
45
  flag: Flag
46
+ raw_flags: Optional[int] = None
47
+ comment: Optional[str] = None
48
+
49
+ @property
50
+ def domain(self) -> str:
51
+ """The domain the cookie belongs to; alias for `url`."""
52
+ return self.url
42
53
 
43
54
 
44
55
  class Format(str, Enum):
@@ -1,38 +0,0 @@
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.20.0']
15
-
16
- entry_points = \
17
- {'console_scripts': ['bcparser = binarycookies.__main__:main']}
18
-
19
- setup_kwargs = {
20
- 'name': 'binarycookies',
21
- 'version': '2.2.3',
22
- 'description': 'Python Binary Cookies (de)serializer',
23
- 'long_description': '[![Github Actions Status](https://github.com/dan1elt0m/binary-cookies-reader/workflows/test/badge.svg)](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)
@@ -1,4 +0,0 @@
1
- from binarycookies._deserialize import load, loads
2
- from binarycookies._serialize import dump, dumps
3
-
4
- __all__ = ["dump", "dumps", "load", "loads"]
@@ -1,150 +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.
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
-
84
- Args:
85
- cookies: A Binary Cookies object to be serialized.
86
- Returns:
87
- bytes: The serialized binary cookies data.
88
- """
89
- if isinstance(cookies, dict):
90
- cookies = [Cookie.parse_obj(cookies)] if IS_PYDANTIC_V1 else [Cookie.model_validate(cookies)]
91
- elif isinstance(cookies, (list, tuple)):
92
- if IS_PYDANTIC_V1:
93
- cookies = [Cookie.parse_obj(cookie) for cookie in cookies]
94
- else:
95
- cookies = [Cookie.model_validate(cookie) for cookie in cookies]
96
- elif isinstance(cookies, Cookie):
97
- cookies = [cookies]
98
- else:
99
- raise TypeError("Invalid type for cookies. Expected dict, list, tuple, or Cookie.")
100
-
101
- file_fields = FileFields()
102
-
103
- data = BytesIO()
104
-
105
- # Write file header
106
- write_field(data, file_fields.header, "cook")
107
-
108
- # Number of pages (1 for simplicity)
109
- write_field(data, file_fields.num_pages, 1)
110
-
111
- # Write number of cookies
112
- data.write(pack(Format.integer, len(cookies)))
113
-
114
- # Placeholder for page size
115
- page_size_offset = data.tell()
116
- data.write(b"\x00\x00\x00\x00")
117
-
118
- # Write number of cookies
119
- data.write(pack(Format.integer, len(cookies)))
120
- cookie_data_list = []
121
- # Write cookies
122
- for cookie in cookies:
123
- cookie_data_list.append(serialize_cookie(cookie))
124
-
125
- initial_cookie_offset = data.tell() + (len(cookies) * 4)
126
- initial_cookie = True
127
- previous_sizes = 0
128
- for cookie_data in cookie_data_list:
129
- if initial_cookie:
130
- data.write(pack(Format.integer, initial_cookie_offset))
131
- initial_cookie = False
132
- else:
133
- data.write(pack(Format.integer, previous_sizes + initial_cookie_offset))
134
-
135
- previous_sizes += len(cookie_data)
136
-
137
- # Unknown data
138
- data.write(b"\x00\x00\x00\x00")
139
- data.write(b"\x00\x00\x00\x00")
140
- data.write(b"\x00\x00\x00\x00")
141
-
142
- for cookie_data in cookie_data_list:
143
- data.write(cookie_data)
144
-
145
- # Update page size
146
- page_size = data.tell()
147
- data.seek(page_size_offset)
148
- data.write(pack(Format.integer, page_size))
149
-
150
- return data.getvalue()
File without changes