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

1"""KeePass keyfile creation and parsing. 

2 

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 

8 

9Example: 

10 from kdbxtool import create_keyfile, KeyFileVersion 

11 

12 # Create recommended XML v2.0 keyfile 

13 create_keyfile("my.keyx", version=KeyFileVersion.XML_V2) 

14 

15 # Create raw 32-byte keyfile 

16 create_keyfile("my.key", version=KeyFileVersion.RAW_32) 

17""" 

18 

19from __future__ import annotations 

20 

21import hashlib 

22import os 

23from enum import StrEnum 

24from pathlib import Path 

25 

26from kdbxtool.exceptions import InvalidKeyFileError 

27 

28from .crypto import constant_time_compare 

29 

30 

31class KeyFileVersion(StrEnum): 

32 """Supported KeePass keyfile formats. 

33 

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 """ 

42 

43 XML_V2 = "xml_v2" 

44 XML_V1 = "xml_v1" 

45 RAW_32 = "raw_32" 

46 HEX_64 = "hex_64" 

47 

48 

49def create_keyfile_bytes(version: KeyFileVersion = KeyFileVersion.XML_V2) -> bytes: 

50 """Create a new keyfile and return its contents as bytes. 

51 

52 Generates a cryptographically secure 32-byte random key and encodes it 

53 in the specified format. 

54 

55 Args: 

56 version: Keyfile format to use. Defaults to XML_V2 (recommended). 

57 

58 Returns: 

59 Keyfile contents as bytes, ready to write to a file. 

60 

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) 

68 

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}") 

79 

80 

81def create_keyfile( 

82 path: str | Path, 

83 version: KeyFileVersion = KeyFileVersion.XML_V2, 

84) -> None: 

85 """Create a new keyfile at the specified path. 

86 

87 Generates a cryptographically secure 32-byte random key and saves it 

88 in the specified format. 

89 

90 Args: 

91 path: Path where the keyfile will be created. 

92 version: Keyfile format to use. Defaults to XML_V2 (recommended). 

93 

94 Raises: 

95 OSError: If the file cannot be written. 

96 

97 Example: 

98 # Create XML v2.0 keyfile (recommended) 

99 create_keyfile("vault.keyx") 

100 

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) 

106 

107 

108def parse_keyfile(keyfile_data: bytes) -> bytes: 

109 """Parse keyfile data and extract the 32-byte key. 

110 

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 

116 

117 Args: 

118 keyfile_data: Raw keyfile contents. 

119 

120 Returns: 

121 32-byte key derived from keyfile. 

122 

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 

129 

130 import defusedxml.ElementTree as ET 

131 

132 tree = ET.fromstring(keyfile_data) 

133 version_elem = tree.find("Meta/Version") 

134 data_elem = tree.find("Key/Data") 

135 

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 

154 

155 # Check for raw 32-byte key 

156 if len(keyfile_data) == 32: 

157 return keyfile_data 

158 

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 

167 

168 # Hash anything else 

169 return hashlib.sha256(keyfile_data).digest() 

170 

171 

172def _create_xml_v2(key_bytes: bytes) -> bytes: 

173 """Create XML v2.0 keyfile content. 

174 

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> 

185 

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() 

190 

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") 

202 

203 

204def _create_xml_v1(key_bytes: bytes) -> bytes: 

205 """Create XML v1.0 keyfile content. 

206 

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 

219 

220 key_b64 = base64.b64encode(key_bytes).decode("ascii") 

221 

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")