Coverage for src / kdbxtool / security / keyfile.py: 99%
75 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-20 19:19 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-20 19:19 +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 logging
23import os
24from enum import StrEnum
25from pathlib import Path
27from kdbxtool.exceptions import InvalidKeyFileError
29from .crypto import constant_time_compare
31logger = logging.getLogger(__name__)
34class KeyFileVersion(StrEnum):
35 """Supported KeePass keyfile formats.
37 Attributes:
38 XML_V2: XML format v2.0 with hex-encoded key and SHA-256 hash verification.
39 This is the recommended format for new keyfiles. Uses .keyx extension.
40 XML_V1: Legacy XML format v1.0 with base64-encoded key.
41 Supported for compatibility. Uses .key extension.
42 RAW_32: Raw 32-byte binary key. Simple but no integrity verification.
43 HEX_64: 64-character hex string (32 bytes encoded as hex).
44 """
46 XML_V2 = "xml_v2"
47 XML_V1 = "xml_v1"
48 RAW_32 = "raw_32"
49 HEX_64 = "hex_64"
52def create_keyfile_bytes(version: KeyFileVersion = KeyFileVersion.XML_V2) -> bytes:
53 """Create a new keyfile and return its contents as bytes.
55 Generates a cryptographically secure 32-byte random key and encodes it
56 in the specified format.
58 Args:
59 version: Keyfile format to use. Defaults to XML_V2 (recommended).
61 Returns:
62 Keyfile contents as bytes, ready to write to a file.
64 Example:
65 keyfile_data = create_keyfile_bytes(KeyFileVersion.XML_V2)
66 with open("my.keyx", "wb") as f:
67 f.write(keyfile_data)
68 """
69 # Generate 32 bytes of cryptographically secure random data
70 key_bytes = os.urandom(32)
72 if version == KeyFileVersion.XML_V2:
73 return _create_xml_v2(key_bytes)
74 elif version == KeyFileVersion.XML_V1:
75 return _create_xml_v1(key_bytes)
76 elif version == KeyFileVersion.RAW_32:
77 return key_bytes
78 elif version == KeyFileVersion.HEX_64:
79 return key_bytes.hex().encode("ascii")
80 else:
81 raise ValueError(f"Unknown keyfile version: {version}")
84def create_keyfile(
85 path: str | Path,
86 version: KeyFileVersion = KeyFileVersion.XML_V2,
87) -> None:
88 """Create a new keyfile at the specified path.
90 Generates a cryptographically secure 32-byte random key and saves it
91 in the specified format.
93 Args:
94 path: Path where the keyfile will be created.
95 version: Keyfile format to use. Defaults to XML_V2 (recommended).
97 Raises:
98 OSError: If the file cannot be written.
100 Example:
101 # Create XML v2.0 keyfile (recommended)
102 create_keyfile("vault.keyx")
104 # Create raw binary keyfile
105 create_keyfile("vault.key", version=KeyFileVersion.RAW_32)
106 """
107 logger.info("Creating %s keyfile", version.value)
108 keyfile_data = create_keyfile_bytes(version)
109 Path(path).write_bytes(keyfile_data)
112def parse_keyfile(keyfile_data: bytes) -> bytes:
113 """Parse keyfile data and extract the 32-byte key.
115 KeePass supports several keyfile formats:
116 1. XML keyfile (v1.0 or v2.0) - key is base64/hex encoded in XML
117 2. 32-byte raw binary - used directly
118 3. 64-byte hex string - decoded from hex
119 4. Any other size - SHA-256 hashed
121 Args:
122 keyfile_data: Raw keyfile contents.
124 Returns:
125 32-byte key derived from keyfile.
127 Raises:
128 InvalidKeyFileError: If keyfile format is invalid or hash verification fails.
129 """
130 # Try parsing as XML keyfile
131 try:
132 import base64
134 import defusedxml.ElementTree as ET
136 tree = ET.fromstring(keyfile_data)
137 version_elem = tree.find("Meta/Version")
138 data_elem = tree.find("Key/Data")
140 if version_elem is not None and data_elem is not None:
141 version = version_elem.text or ""
142 if version.startswith("1.0"):
143 # Version 1.0: base64 encoded
144 logger.debug("Parsing XML v1.0 keyfile")
145 return base64.b64decode(data_elem.text or "")
146 elif version.startswith("2.0"):
147 # Version 2.0: hex encoded with hash verification
148 logger.debug("Parsing XML v2.0 keyfile")
149 key_hex = (data_elem.text or "").strip()
150 key_bytes = bytes.fromhex(key_hex)
151 # Verify hash if present (constant-time comparison)
152 if "Hash" in data_elem.attrib:
153 expected_hash = bytes.fromhex(data_elem.attrib["Hash"])
154 computed_hash = hashlib.sha256(key_bytes).digest()[:4]
155 if not constant_time_compare(expected_hash, computed_hash):
156 raise InvalidKeyFileError("Keyfile hash verification failed")
157 return key_bytes
158 except (ET.ParseError, ValueError, AttributeError):
159 pass # Not an XML keyfile
161 # Check for raw 32-byte key
162 if len(keyfile_data) == 32:
163 logger.debug("Using raw 32-byte keyfile")
164 return keyfile_data
166 # Check for 64-byte hex-encoded key
167 if len(keyfile_data) == 64:
168 try:
169 # Verify it's valid hex
170 int(keyfile_data, 16)
171 logger.debug("Using 64-byte hex keyfile")
172 return bytes.fromhex(keyfile_data.decode("ascii"))
173 except (ValueError, UnicodeDecodeError):
174 pass # Not hex
176 # Hash anything else
177 logger.debug("Hashing %d-byte keyfile", len(keyfile_data))
178 return hashlib.sha256(keyfile_data).digest()
181def _create_xml_v2(key_bytes: bytes) -> bytes:
182 """Create XML v2.0 keyfile content.
184 Format:
185 <?xml version="1.0" encoding="utf-8"?>
186 <KeyFile>
187 <Meta>
188 <Version>2.0</Version>
189 </Meta>
190 <Key>
191 <Data Hash="XXXXXXXX">hex-encoded-key</Data>
192 </Key>
193 </KeyFile>
195 The Hash attribute contains the first 4 bytes of SHA-256(key) as hex.
196 """
197 key_hex = key_bytes.hex().upper()
198 hash_hex = hashlib.sha256(key_bytes).digest()[:4].hex().upper()
200 xml = f"""<?xml version="1.0" encoding="utf-8"?>
201<KeyFile>
202\t<Meta>
203\t\t<Version>2.0</Version>
204\t</Meta>
205\t<Key>
206\t\t<Data Hash="{hash_hex}">{key_hex}</Data>
207\t</Key>
208</KeyFile>
209"""
210 return xml.encode("utf-8")
213def _create_xml_v1(key_bytes: bytes) -> bytes:
214 """Create XML v1.0 keyfile content.
216 Format:
217 <?xml version="1.0" encoding="utf-8"?>
218 <KeyFile>
219 <Meta>
220 <Version>1.00</Version>
221 </Meta>
222 <Key>
223 <Data>base64-encoded-key</Data>
224 </Key>
225 </KeyFile>
226 """
227 import base64
229 key_b64 = base64.b64encode(key_bytes).decode("ascii")
231 xml = f"""<?xml version="1.0" encoding="utf-8"?>
232<KeyFile>
233\t<Meta>
234\t\t<Version>1.00</Version>
235\t</Meta>
236\t<Key>
237\t\t<Data>{key_b64}</Data>
238\t</Key>
239</KeyFile>
240"""
241 return xml.encode("utf-8")