Skip to content

Commit 35029d6

Browse files
committed
Add encryption_algorithm setter for changing cipher on existing databases
- Add setter that accepts string or Cipher enum - Automatically regenerates IV with correct size for new cipher - Works on both KDBX3 and KDBX4 databases - Add 4 tests for cipher setter functionality
1 parent 2c75068 commit 35029d6

3 files changed

Lines changed: 73 additions & 0 deletions

File tree

pykeepass/kdbx_parsing/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
AesKdfConfig,
99
Cipher,
1010
KdfAlgorithm,
11+
IV_SIZES,
1112
)
1213

1314
__all__ = [
@@ -20,4 +21,5 @@
2021
"AesKdfConfig",
2122
"Cipher",
2223
"KdfAlgorithm",
24+
"IV_SIZES",
2325
]

pykeepass/pykeepass.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
AesKdfConfig,
3434
Cipher,
3535
KdfAlgorithm,
36+
IV_SIZES,
3637
)
3738
from .xpath import attachment_xp, entry_xp, group_xp, path_xp
3839

@@ -201,6 +202,18 @@ def encryption_algorithm(self):
201202
Can be one of 'aes256', 'chacha20', or 'twofish'."""
202203
return self.kdbx.header.value.dynamic_header.cipher_id.data
203204

205+
@encryption_algorithm.setter
206+
def encryption_algorithm(self, value):
207+
"""Set encryption algorithm. Regenerates IV with correct size for new cipher."""
208+
if isinstance(value, Cipher):
209+
value = value.value
210+
if value not in IV_SIZES:
211+
raise ValueError(f"Invalid encryption algorithm: {value}. Must be one of {list(IV_SIZES.keys())}")
212+
self.kdbx.header.value.dynamic_header.cipher_id.data = value
213+
# Regenerate IV with correct size for new cipher
214+
self.kdbx.header.value.dynamic_header.encryption_iv.data = os.urandom(IV_SIZES[value])
215+
self._invalidate_header_cache()
216+
204217
@property
205218
def kdf_algorithm(self):
206219
"""`str`: key derivation algorithm used by database during decryption.

tests/tests.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,64 @@ def test_argon2_variant_invalid_value(self):
15841584
kp.argon2_variant = 'invalid'
15851585

15861586

1587+
class CipherSetterTests(unittest.TestCase):
1588+
"""Tests for encryption_algorithm setter"""
1589+
1590+
def test_cipher_setter_kdbx4(self):
1591+
"""Test changing cipher on KDBX4 database"""
1592+
import tempfile
1593+
with tempfile.NamedTemporaryFile(suffix='.kdbx', delete=False) as f:
1594+
db_path = f.name
1595+
try:
1596+
kp = create_database(db_path, password='testpass')
1597+
kp.add_entry(kp.root_group, 'Test Entry', 'user', 'secret123')
1598+
self.assertEqual(kp.encryption_algorithm, 'aes256')
1599+
1600+
kp.encryption_algorithm = 'chacha20'
1601+
kp.save()
1602+
1603+
kp2 = PyKeePass(db_path, password='testpass')
1604+
self.assertEqual(kp2.encryption_algorithm, 'chacha20')
1605+
entry = kp2.find_entries(title='Test Entry', first=True)
1606+
self.assertEqual(entry.password, 'secret123')
1607+
finally:
1608+
os.unlink(db_path)
1609+
1610+
def test_cipher_setter_kdbx3(self):
1611+
"""Test changing cipher on KDBX3 database"""
1612+
import tempfile
1613+
with tempfile.NamedTemporaryFile(suffix='.kdbx', delete=False) as f:
1614+
db_path = f.name
1615+
try:
1616+
kp = create_database(db_path, password='testpass', version=3)
1617+
kp.add_entry(kp.root_group, 'Test Entry', 'user', 'secret123')
1618+
self.assertEqual(kp.encryption_algorithm, 'aes256')
1619+
1620+
kp.encryption_algorithm = 'twofish'
1621+
kp.save()
1622+
1623+
kp2 = PyKeePass(db_path, password='testpass')
1624+
self.assertEqual(kp2.encryption_algorithm, 'twofish')
1625+
entry = kp2.find_entries(title='Test Entry', first=True)
1626+
self.assertEqual(entry.password, 'secret123')
1627+
finally:
1628+
os.unlink(db_path)
1629+
1630+
def test_cipher_setter_with_enum(self):
1631+
"""Test setting cipher using Cipher enum"""
1632+
with BytesIO() as stream:
1633+
kp = create_database(stream, password='testpass')
1634+
kp.encryption_algorithm = Cipher.CHACHA20
1635+
self.assertEqual(kp.encryption_algorithm, 'chacha20')
1636+
1637+
def test_cipher_setter_invalid_value(self):
1638+
"""Test that invalid cipher raises error"""
1639+
with BytesIO() as stream:
1640+
kp = create_database(stream, password='testpass')
1641+
with self.assertRaises(ValueError):
1642+
kp.encryption_algorithm = 'invalid'
1643+
1644+
15871645
class KDBX3CreateDatabaseTests(unittest.TestCase):
15881646
"""Tests for KDBX3 database creation"""
15891647

0 commit comments

Comments
 (0)