Coverage for src / kdbxtool / exceptions.py: 91%

66 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-19 21:22 +0000

1"""Custom exception hierarchy for kdbxtool. 

2 

3This module provides a rich exception hierarchy for better error handling 

4and user feedback. All exceptions inherit from KdbxError. 

5 

6Exception Hierarchy: 

7 KdbxError (base) 

8 ├── FormatError 

9 │ ├── InvalidSignatureError 

10 │ ├── UnsupportedVersionError 

11 │ └── CorruptedDataError 

12 ├── CryptoError 

13 │ ├── DecryptionError 

14 │ ├── AuthenticationError 

15 │ ├── KdfError 

16 │ ├── UnknownCipherError 

17 │ └── TwofishNotAvailableError 

18 ├── CredentialError 

19 │ ├── InvalidPasswordError 

20 │ ├── InvalidKeyFileError 

21 │ ├── MissingCredentialsError 

22 │ └── YubiKeyError 

23 │ ├── YubiKeyNotFoundError 

24 │ ├── YubiKeySlotError 

25 │ ├── YubiKeyTimeoutError 

26 │ └── YubiKeyNotAvailableError 

27 └── DatabaseError 

28 ├── EntryNotFoundError 

29 └── GroupNotFoundError 

30 

31Security Note: 

32 Exception messages are designed to avoid leaking sensitive information. 

33 They provide enough context for debugging without exposing secrets. 

34""" 

35 

36from __future__ import annotations 

37 

38 

39class KdbxError(Exception): 

40 """Base exception for all kdbxtool errors. 

41 

42 All exceptions raised by kdbxtool inherit from this class, 

43 making it easy to catch all library-specific errors. 

44 """ 

45 

46 

47# --- Format Errors --- 

48 

49 

50class FormatError(KdbxError): 

51 """Error in KDBX file format or structure. 

52 

53 Raised when the file doesn't conform to the KDBX specification. 

54 """ 

55 

56 

57class InvalidSignatureError(FormatError): 

58 """Invalid KDBX file signature (magic bytes). 

59 

60 The file doesn't start with the expected KDBX magic bytes, 

61 indicating it's not a valid KeePass database file. 

62 """ 

63 

64 

65class UnsupportedVersionError(FormatError): 

66 """Unsupported KDBX version. 

67 

68 The file uses a KDBX version that this library doesn't support. 

69 """ 

70 

71 def __init__(self, version_major: int, version_minor: int) -> None: 

72 self.version_major = version_major 

73 self.version_minor = version_minor 

74 super().__init__(f"Unsupported KDBX version: {version_major}.{version_minor}") 

75 

76 

77class CorruptedDataError(FormatError): 

78 """Database file is corrupted or truncated. 

79 

80 The file structure is invalid, possibly due to incomplete download, 

81 disk corruption, or other data integrity issues. 

82 """ 

83 

84 

85# --- Crypto Errors --- 

86 

87 

88class CryptoError(KdbxError): 

89 """Error in cryptographic operations. 

90 

91 Base class for all cryptographic errors including encryption, 

92 decryption, and key derivation. 

93 """ 

94 

95 

96class DecryptionError(CryptoError): 

97 """Failed to decrypt database content. 

98 

99 This typically indicates wrong credentials (password/keyfile), 

100 but the message is kept generic to avoid confirming which 

101 credential component is incorrect. 

102 """ 

103 

104 def __init__(self, message: str = "Decryption failed") -> None: 

105 super().__init__(message) 

106 

107 

108class AuthenticationError(CryptoError): 

109 """HMAC or integrity verification failed. 

110 

111 The database's authentication code doesn't match, indicating 

112 either wrong credentials or data tampering. 

113 """ 

114 

115 def __init__( 

116 self, message: str = "Authentication failed - wrong credentials or corrupted data" 

117 ) -> None: 

118 super().__init__(message) 

119 

120 

121class KdfError(CryptoError): 

122 """Error in key derivation function. 

123 

124 Problems with KDF parameters, unsupported KDF types, 

125 or KDF computation failures. 

126 """ 

127 

128 

129class UnknownCipherError(CryptoError): 

130 """Unknown or unsupported cipher algorithm. 

131 

132 The database uses a cipher that this library doesn't recognize. 

133 """ 

134 

135 def __init__(self, cipher_uuid: bytes) -> None: 

136 self.cipher_uuid = cipher_uuid 

137 super().__init__(f"Unknown cipher: {cipher_uuid.hex()}") 

138 

139 

140class TwofishNotAvailableError(CryptoError): 

141 """Twofish cipher requested but oxifish package not installed. 

142 

143 The database uses Twofish encryption, which requires the optional 

144 oxifish package. Install it with: pip install kdbxtool[twofish] 

145 """ 

146 

147 def __init__(self) -> None: 

148 super().__init__( 

149 "Twofish cipher requires the oxifish package. " 

150 "Install with: pip install kdbxtool[twofish]" 

151 ) 

152 

153 

154# --- Credential Errors --- 

155 

156 

157class CredentialError(KdbxError): 

158 """Error with database credentials. 

159 

160 Base class for credential-related errors. Messages are kept 

161 generic to avoid information disclosure about which credential 

162 component is incorrect. 

163 """ 

164 

165 

166class InvalidPasswordError(CredentialError): 

167 """Invalid or missing password. 

168 

169 Note: This is only raised when we can definitively determine 

170 the password is wrong without revealing information about 

171 other credential components. 

172 """ 

173 

174 def __init__(self, message: str = "Invalid password") -> None: 

175 super().__init__(message) 

176 

177 

178class InvalidKeyFileError(CredentialError): 

179 """Invalid or missing keyfile. 

180 

181 The keyfile is malformed, has wrong format, or failed 

182 hash verification. 

183 """ 

184 

185 def __init__(self, message: str = "Invalid keyfile") -> None: 

186 super().__init__(message) 

187 

188 

189class MissingCredentialsError(CredentialError): 

190 """No credentials provided. 

191 

192 At least one credential (password or keyfile) is required 

193 to open or create a database. 

194 """ 

195 

196 def __init__(self) -> None: 

197 super().__init__("At least one credential (password or keyfile) is required") 

198 

199 

200# --- YubiKey Errors --- 

201 

202 

203class YubiKeyError(CredentialError): 

204 """Error communicating with YubiKey. 

205 

206 Base class for YubiKey-related errors. These occur during 

207 challenge-response authentication with a hardware YubiKey. 

208 """ 

209 

210 

211class YubiKeyNotFoundError(YubiKeyError): 

212 """No YubiKey detected. 

213 

214 No YubiKey device was found connected to the system. 

215 Ensure the YubiKey is properly inserted. 

216 """ 

217 

218 def __init__(self, message: str | None = None) -> None: 

219 super().__init__(message or "No YubiKey device found. Ensure it is connected.") 

220 

221 

222class YubiKeySlotError(YubiKeyError): 

223 """YubiKey slot not configured for HMAC-SHA1. 

224 

225 The specified slot on the YubiKey is not configured for 

226 HMAC-SHA1 challenge-response authentication. 

227 """ 

228 

229 def __init__(self, slot: int) -> None: 

230 self.slot = slot 

231 super().__init__(f"YubiKey slot {slot} is not configured for HMAC-SHA1 challenge-response") 

232 

233 

234class YubiKeyTimeoutError(YubiKeyError): 

235 """YubiKey operation timed out. 

236 

237 The YubiKey operation timed out, typically because touch 

238 was required but not received within the timeout period. 

239 """ 

240 

241 def __init__(self, timeout_seconds: float = 15.0) -> None: 

242 self.timeout_seconds = timeout_seconds 

243 super().__init__( 

244 f"YubiKey operation timed out after {timeout_seconds}s. " 

245 "Touch may be required - try again and press the YubiKey button." 

246 ) 

247 

248 

249class YubiKeyNotAvailableError(YubiKeyError): 

250 """YubiKey support requested but yubikey-manager not installed. 

251 

252 The yubikey-manager package is required for YubiKey challenge-response 

253 authentication. Install it with: pip install kdbxtool[yubikey] 

254 """ 

255 

256 def __init__(self) -> None: 

257 super().__init__( 

258 "YubiKey support requires the yubikey-manager package. " 

259 "Install with: pip install kdbxtool[yubikey]" 

260 ) 

261 

262 

263# --- Database Errors --- 

264 

265 

266class DatabaseError(KdbxError): 

267 """Error in database operations. 

268 

269 Base class for errors that occur during database manipulation 

270 after successful decryption. 

271 """ 

272 

273 

274class EntryNotFoundError(DatabaseError): 

275 """Entry not found in database. 

276 

277 The requested entry doesn't exist or was not found 

278 in the specified location. 

279 """ 

280 

281 def __init__(self, message: str = "Entry not found") -> None: 

282 super().__init__(message) 

283 

284 

285class GroupNotFoundError(DatabaseError): 

286 """Group not found in database. 

287 

288 The requested group doesn't exist or was not found 

289 in the database hierarchy. 

290 """ 

291 

292 def __init__(self, message: str = "Group not found") -> None: 

293 super().__init__(message) 

294 

295 

296class InvalidXmlError(DatabaseError): 

297 """Invalid or malformed XML payload. 

298 

299 The decrypted XML content doesn't conform to the expected 

300 KDBX XML schema. 

301 """ 

302 

303 def __init__(self, message: str = "Invalid KDBX XML structure") -> None: 

304 super().__init__(message) 

305 

306 

307class Kdbx3UpgradeRequired(DatabaseError): 

308 """KDBX3 database requires explicit upgrade confirmation. 

309 

310 When saving a KDBX3 database to its original file, explicit 

311 confirmation is required since the save will upgrade it to KDBX4. 

312 Use save(allow_upgrade=True) to confirm the upgrade. 

313 """ 

314 

315 def __init__(self) -> None: 

316 super().__init__( 

317 "Saving a KDBX3 database will upgrade it to KDBX4 format. " 

318 "Use save(allow_upgrade=True) to confirm, or save to a different file." 

319 ) 

320 

321 

322class MergeError(DatabaseError): 

323 """Error during database merge operation. 

324 

325 Raised when a merge operation fails due to incompatible databases, 

326 invalid state, or other merge-specific issues. 

327 """ 

328 

329 def __init__(self, message: str = "Merge operation failed") -> None: 

330 super().__init__(message)