Coverage for src / kdbxtool / security / keyfile.py: 99%
67 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-19 21:22 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-19 21:22 +0000
1"""KeePass keyfile creation and parsing.
3This module provides support for all KeePass keyfile formats:
4- XML v2.0: Recommended format with hex-encoded key and SHA-256 hash verification
5- XML v1.0: Legacy format with base64-encoded key
6- RAW_32: Raw 32-byte binary key
7- HEX_64: 64-character hex string
9Example:
10 from kdbxtool import create_keyfile, KeyFileVersion
12 # Create recommended XML v2.0 keyfile
13 create_keyfile("my.keyx", version=KeyFileVersion.XML_V2)
15 # Create raw 32-byte keyfile
16 create_keyfile("my.key", version=KeyFileVersion.RAW_32)
17"""
19from __future__ import annotations
21import hashlib
22import os
23from enum import StrEnum
24from pathlib import Path
26from kdbxtool.exceptions import InvalidKeyFileError
28from .crypto import constant_time_compare
31class KeyFileVersion(StrEnum):
32 """Supported KeePass keyfile formats.
34 Attributes:
35 XML_V2: XML format v2.0 with hex-encoded key and SHA-256 hash verification.
36 This is the recommended format for new keyfiles. Uses .keyx extension.
37 XML_V1: Legacy XML format v1.0 with base64-encoded key.
38 Supported for compatibility. Uses .key extension.
39 RAW_32: Raw 32-byte binary key. Simple but no integrity verification.
40 HEX_64: 64-character hex string (32 bytes encoded as hex).
41 """
43 XML_V2 = "xml_v2"
44 XML_V1 = "xml_v1"
45 RAW_32 = "raw_32"
46 HEX_64 = "hex_64"
49def create_keyfile_bytes(version: KeyFileVersion = KeyFileVersion.XML_V2) -> bytes:
50 """Create a new keyfile and return its contents as bytes.
52 Generates a cryptographically secure 32-byte random key and encodes it
53 in the specified format.
55 Args:
56 version: Keyfile format to use. Defaults to XML_V2 (recommended).
58 Returns:
59 Keyfile contents as bytes, ready to write to a file.
61 Example:
62 keyfile_data = create_keyfile_bytes(KeyFileVersion.XML_V2)
63 with open("my.keyx", "wb") as f:
64 f.write(keyfile_data)
65 """
66 # Generate 32 bytes of cryptographically secure random data
67 key_bytes = os.urandom(32)
69 if version == KeyFileVersion.XML_V2:
70 return _create_xml_v2(key_bytes)
71 elif version == KeyFileVersion.XML_V1:
72 return _create_xml_v1(key_bytes)
73 elif version == KeyFileVersion.RAW_32:
74 return key_bytes
75 elif version == KeyFileVersion.HEX_64:
76 return key_bytes.hex().encode("ascii")
77 else:
78 raise ValueError(f"Unknown keyfile version: {version}")
81def create_keyfile(
82 path: str | Path,
83 version: KeyFileVersion = KeyFileVersion.XML_V2,
84) -> None:
85 """Create a new keyfile at the specified path.
87 Generates a cryptographically secure 32-byte random key and saves it
88 in the specified format.
90 Args:
91 path: Path where the keyfile will be created.
92 version: Keyfile format to use. Defaults to XML_V2 (recommended).
94 Raises:
95 OSError: If the file cannot be written.
97 Example:
98 # Create XML v2.0 keyfile (recommended)
99 create_keyfile("vault.keyx")
101 # Create raw binary keyfile
102 create_keyfile("vault.key", version=KeyFileVersion.RAW_32)
103 """
104 keyfile_data = create_keyfile_bytes(version)
105 Path(path).write_bytes(keyfile_data)
108def parse_keyfile(keyfile_data: bytes) -> bytes:
109 """Parse keyfile data and extract the 32-byte key.
111 KeePass supports several keyfile formats:
112 1. XML keyfile (v1.0 or v2.0) - key is base64/hex encoded in XML
113 2. 32-byte raw binary - used directly
114 3. 64-byte hex string - decoded from hex
115 4. Any other size - SHA-256 hashed
117 Args:
118 keyfile_data: Raw keyfile contents.
120 Returns:
121 32-byte key derived from keyfile.
123 Raises:
124 InvalidKeyFileError: If keyfile format is invalid or hash verification fails.
125 """
126 # Try parsing as XML keyfile
127 try:
128 import base64
130 import defusedxml.ElementTree as ET
132 tree = ET.fromstring(keyfile_data)
133 version_elem = tree.find("Meta/Version")
134 data_elem = tree.find("Key/Data")
136 if version_elem is not None and data_elem is not None:
137 version = version_elem.text or ""
138 if version.startswith("1.0"):
139 # Version 1.0: base64 encoded
140 return base64.b64decode(data_elem.text or "")
141 elif version.startswith("2.0"):
142 # Version 2.0: hex encoded with hash verification
143 key_hex = (data_elem.text or "").strip()
144 key_bytes = bytes.fromhex(key_hex)
145 # Verify hash if present (constant-time comparison)
146 if "Hash" in data_elem.attrib:
147 expected_hash = bytes.fromhex(data_elem.attrib["Hash"])
148 computed_hash = hashlib.sha256(key_bytes).digest()[:4]
149 if not constant_time_compare(expected_hash, computed_hash):
150 raise InvalidKeyFileError("Keyfile hash verification failed")
151 return key_bytes
152 except (ET.ParseError, ValueError, AttributeError):
153 pass # Not an XML keyfile
155 # Check for raw 32-byte key
156 if len(keyfile_data) == 32:
157 return keyfile_data
159 # Check for 64-byte hex-encoded key
160 if len(keyfile_data) == 64:
161 try:
162 # Verify it's valid hex
163 int(keyfile_data, 16)
164 return bytes.fromhex(keyfile_data.decode("ascii"))
165 except (ValueError, UnicodeDecodeError):
166 pass # Not hex
168 # Hash anything else
169 return hashlib.sha256(keyfile_data).digest()
172def _create_xml_v2(key_bytes: bytes) -> bytes:
173 """Create XML v2.0 keyfile content.
175 Format:
176 <?xml version="1.0" encoding="utf-8"?>
177 <KeyFile>
178 <Meta>
179 <Version>2.0</Version>
180 </Meta>
181 <Key>
182 <Data Hash="XXXXXXXX">hex-encoded-key</Data>
183 </Key>
184 </KeyFile>
186 The Hash attribute contains the first 4 bytes of SHA-256(key) as hex.
187 """
188 key_hex = key_bytes.hex().upper()
189 hash_hex = hashlib.sha256(key_bytes).digest()[:4].hex().upper()
191 xml = f"""<?xml version="1.0" encoding="utf-8"?>
192<KeyFile>
193\t<Meta>
194\t\t<Version>2.0</Version>
195\t</Meta>
196\t<Key>
197\t\t<Data Hash="{hash_hex}">{key_hex}</Data>
198\t</Key>
199</KeyFile>
200"""
201 return xml.encode("utf-8")
204def _create_xml_v1(key_bytes: bytes) -> bytes:
205 """Create XML v1.0 keyfile content.
207 Format:
208 <?xml version="1.0" encoding="utf-8"?>
209 <KeyFile>
210 <Meta>
211 <Version>1.00</Version>
212 </Meta>
213 <Key>
214 <Data>base64-encoded-key</Data>
215 </Key>
216 </KeyFile>
217 """
218 import base64
220 key_b64 = base64.b64encode(key_bytes).decode("ascii")
222 xml = f"""<?xml version="1.0" encoding="utf-8"?>
223<KeyFile>
224\t<Meta>
225\t\t<Version>1.00</Version>
226\t</Meta>
227\t<Key>
228\t\t<Data>{key_b64}</Data>
229\t</Key>
230</KeyFile>
231"""
232 return xml.encode("utf-8")