KeepassDX est un gestionnaire de mot de passe, c’est-à-dire l’équivalent numérique d’un coffre-fort : vous y mettez tous vos mots de passe et KeepassDX les retient pour vous !
Le seul mot de passe à mémoriser est le mot de passe du coffre-fort en question, c’est-à-dire la base de donnée KeepassDX.
Utilisant quotidiennement le gestionnaire de mot de passe keepassDX sur mon téléphone, j’ai cherché à comprendre comment cette application protège mes mots de passe.
Nous allons donc regarder de près le fonctionnement de ce petit outil.
Installation
Nous étudions la version 4.5.2 de keepassDX. Pour cela, nous créeons une base de donnée avec le contenu suivant :
+-----------------+-------------+-----------------+----------------------+
| Titre | Utilisateur | Mot de passe | URL |
+-----------------+-------------+-----------------+----------------------+
| poulet | cotcot | Vitcotcodec123! | https://poulet.fr |
+-----------------+-------------+-----------------+----------------------+
| société secrète | zorglub | Zorglub123! | https://zorglub.info |
+-----------------+-------------+-----------------+----------------------+
| semoule | couscous | Fr!te | |
+-----------------+-------------+-----------------+----------------------+
Le mot de passe-maître sera « Plouf123! ».
Nous créons cette base de donnée tout de suite après l’installation de l’application.
Une pratique de sécurité usuelle dans une application android est d’interdire la prise de capture d’écran des activités exposant une donnée sensible.
Si KeepassDX interdit bien les captures d’écran des mots de passe stockés dans la base de donnée, il est amusant de noter qu’il reste possible de prendre une capture d’écran du choix du master password (alors que c’est le mot de passe le plus important !).

Premières manipulations
À ce stade nous n’avons aucune idée du fonctionnement de la logique de chiffrement de la base de donnée.
Il est donc possible qu’elle implique un secret stocké dans une enclave matérielle du téléphone, ou plus simplement, un secret stocké dans un fichier de configuration de l’application.
Un bon moyen de trancher cette question est de tenter d’ouvrir la base de donnée sur un autre terminal.
Commençons par retrouver la base de donnée :
lynx:/ # find /data -name keepass.kdbx -exec md5sum {} \; 2>/dev/null
5855166608dcfee3ecd82ef3ff21a374 /data/media/0/Download/keepass.kdbx
On la rappatrie ensuite par un simple adb pull.
Il s’avère qu’il est bien possible d’ouvrir la base de donnée sur un pc :

Le chiffrement de la base de donnée repose donc uniquement sur le mot de passe et éventuellement des éléments stockés en clair dans la base de donnée.
Examen sur téléphone
Regardons un peu l’application depuis un shell adb sur un téléphone rooté :
On commence par identifier l’uid de l’application :
lynx:/ # ps -elf | grep -i keepass | grep -v grep
u0_a286 13372 1013 1 19:44:07 ? 00:00:54 com.kunzisoft.keepass.free
lynx:/ # ps -elfn | grep -i keepass | grep -v grep
10286 13372 1013 1 19:44:06 ? 00:00:54 com.kunzisoft.keepass.free
Chose qui nous permet de localiser les données de l’application :
lynx:/ # find /data/data/ -type d -user u0_a286 2>/dev/null
/data/data/com.kunzisoft.keepass.free
/data/data/com.kunzisoft.keepass.free/cache
/data/data/com.kunzisoft.keepass.free/cache/oat_primary
/data/data/com.kunzisoft.keepass.free/cache/oat_primary/arm64
/data/data/com.kunzisoft.keepass.free/code_cache
/data/data/com.kunzisoft.keepass.free/shared_prefs
/data/data/com.kunzisoft.keepass.free/databases
/data/data/com.kunzisoft.keepass.free/files
/data/data/com.kunzisoft.keepass.free/no_backup
On récupère ensuite cette base de donnée (toujours via adb pull), afin de la regarder de plus près.
On trouve une base de donnée sqlite
(fichier /data/data/com.kunzisoft.keepass.free/databases/com.kunzisoft.keepass.database),
que l’on examine avec sqlitebrower.
On y trouve une table file_database_history :

Une table room_master_table :

À première vue, ces deux tables ne contiennent rien de très intéressant.
Deux autres tables, cipher_database et android_metadata, vides, sont présentes dans le fichier.
Deux fichiers xml sont aussi contenus dans les shared_prefs :
$ ls -l
/data/data/com.kunzisoft.keepass.free/shared_prefs:
total 8
-rw-rw---- 1 u0_a286 u0_a286 1259 2026-08-19 19:49 com.kunzisoft.keepass.free_preferences.xml
-rw-rw---- 1 u0_a286 u0_a286 552 2026-08-19 19:48 kdbxeducation.xml
Le premier semble porter la configuration utilisateur de l’application :
$ cat shared_prefs/com.kunzisoft.keepass.free_preferences.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="lock_database_back_root_key" value="false" />
<boolean name="search_option_username_key" value="true" />
<boolean name="search_option_recycle_bin_key" value="false" />
<boolean name="clipboard_otp_notifications_key" value="false" />
<boolean name="search_option_regex_key" value="false" />
<boolean name="search_option_password_key" value="false" />
<boolean name="read_only_key" value="false" />
<boolean name="show_recent_files_key" value="true" />
<set name="search_option_selected_tags_key" />
<string name="app_timeout_key">300000</string>
<boolean name="lock_database_show_button_key" value="true" />
<boolean name="search_option_other_key" value="true" />
<boolean name="user_verification_mode_key" value="true" />
<boolean name="search_option_url_key" value="true" />
<boolean name="search_option_otp_key" value="false" />
<boolean name="search_option_templates_key" value="false" />
<boolean name="remember_hardware_key_key" value="true" />
<boolean name="remember_keyfile_locations_key" value="true" />
<boolean name="search_option_tag_key" value="false" />
<boolean name="enable_screenshot_mode_key" value="false" />
<boolean name="allow_no_password_key" value="false" />
<boolean name="enable_auto_save_database_key" value="true" />
<boolean name="settings_credential_provider_enable_key" value="false" />
<boolean name="magic_keyboard_key" value="false" />
<boolean name="search_option_case_sensitive_key" value="false" />
<boolean name="search_option_application_id_key" value="false" />
<boolean name="remember_database_locations_key" value="true" />
<boolean name="search_option_uuid_key" value="false" />
<boolean name="search_option_title_key" value="true" />
<boolean name="search_option_expired_key" value="true" />
<string name="clip_timeout_key">20000</string>
<boolean name="hide_broken_locations_key" value="false" />
<boolean name="search_option_searchable_group_key" value="true" />
<boolean name="enable_keep_screen_on_key" value="true" />
<boolean name="user_verification_device_credential_key" value="true" />
<boolean name="subdomain_search_key" value="false" />
<long name="timeout_backup_key" value="1788462778608" />
<boolean name="search_option_current_group_key" value="false" />
<boolean name="auto_focus_search_key" value="false" />
<boolean name="lock_database_screen_off_key" value="true" />
<boolean name="search_option_note_key" value="true" />
<boolean name="allow_copy_password_first_time_key" value="false" />
<boolean name="delete_entered_password_key" value="true" />
<boolean name="allow_copy_password_key" value="true" />
</map>
Le deuxième ne contient lui non plus rien qui nous intéresse :
$ cat shared_prefs/kdbxeducation.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="education_lock_key" value="true" />
<boolean name="education_create_db_key" value="true" />
<boolean name="education_search_key" value="true" />
<boolean name="education_biometric_key" value="true" />
<boolean name="education_unlock_key" value="true" />
<boolean name="education_select_db_key" value="true" />
<boolean name="education_read_only_key" value="true" />
<boolean name="education_entry_new_field_key" value="true" />
<boolean name="education_password_generator_key" value="true" />
<boolean name="education_entry_edit_key" value="true" />
<boolean name="education_user_verification_key" value="true" />
<boolean name="education_add_attachment_key" value="true" />
<boolean name="education_new_node_key" value="true" />
<boolean name="education_sort_key" value="true" />
</map>
Reste un fichier, profileInstalled, qui ne semble pas contenir grand chose non plus :
$ hexdump -vC files/profileInstalled
00000000 00 00 00 01 00 00 00 01 00 00 01 a0 73 e3 32 53 |............s.2S|
00000010 00 00 00 00 00 00 00 00 |........|
00000018
L’étude de la sandbox de l’application ne donne donc rien, nous allons continuer en mélant instrumentation dynamique avec frida, analyse du code-source et lecture de la spécification du format des bases de donnée keepass.
Analyse avec frida, documentation et code-source
KeepassDX chiffre et déchiffre des données.
Une méthode d’analyse qui n’est peut-être pas particulièrement raffinée mais est tout de même relativement efficace est de commencer par tracer un peu au hasard les méthodes java réalisant des opérations cryptographiques.
L’important est d’arriver à trouver une ou des fonctions qui manipulent des données intéressantes (typiquement le mot de passe maître, ou bien, à l’autre bout de la chaîne, les mots de passe déchiffrés).
Les fonctions ainsi identifiées serviront de point d’entrée pour comprendre un peu mieux le code-source de l’application.
On commence donc par tenter de tracer les méthodes AES-quelque chose (après tout, il est plus que probable que keepass fasse du déchiffrement de donnée, déchiffrement qui impliquera probablement AES) en lançant
frida-trace -U -p 10211 -j '*!*AES*'
(10211 étant alors le pid de l’application).
Rien de particulièrement explicite ne se dégage des données dumpées par frida-trace.
Une autre hypothèse naturelle est que keepassDX va très certainement dériver une clé symétrique à partir du mot de passe maître.
Une telle opération fera probablement intervenir une fonction de hachage.
On lance donc un filet relativement lâche en exécutant :
frida-trace -U -p 10211 -j '*!*256*'
qui pourrait capturer les appels à SHA256. Cette commande donne un retour assez volumineux, sur lequel on reviendra.
frida-trace -U -p 10211 -j '*!*512*'
ne donne à contrario par grand chose, et
frida-trace -U -p 10211 -j ‘*!128‘ non plus.
Un premier coup d’oeil à la spécification du format de base de donnée kdbx (https://keepass.info/help/kb/kdbx.html) laisse penser qu’une étape de chiffrement avec Salsa20 ou Chach20 pourrait être impliquée, même si à ce stade on ne sait pas si le format décrit dans ce document est bien le format de la base de donnée qui nous intéresse ici.
On trace donc les appels potentiels à Salsa20 :
$ frida-trace -U -p 10211 -j '*!*alsa*'
Instrumenting...
Salsa20Engine.salsaCore: Auto-generated handler at "/home/thomas/articles/keepassDX/__handlers__/org.bouncycastle.crypto.engines.Salsa20Engine/salsaCore.js"
HashManager.getSalsa20: Auto-generated handler at "/home/thomas/articles/keepassDX/__handlers__/com.kunzisoft.encrypt.HashManager/getSalsa20.js"
Started tracing 2 functions. Web UI available at http://localhost:43229/
/* TID 0x281d */
17978 ms HashManager.getSalsa20([-18,-9,63,44,-29,80,-75,75,65,89,-26,-43,125,-61,69,1,-9,124,109,-44,-32,70,121,61,8,-45,66,-55,-116,-66,-82,-31])
17979 ms | Salsa20Engine.salsaCore(20, [1634760805,1671086938,-154089819,-1048709155,-1009904921,857760878,1258893544,710746263,0,0,2036477234,-534980242,-1657576997,2017375097,1057842232,1797285236], [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])
17984 ms <= "<instance: com.kunzisoft.encrypt.StreamCipher>"
Cet appel nous donne un nom de classe, com.kunzisoft.encrypt.StreamCipher, que l’on garde également de coté.
Revenons sur le résultat de frida-trace -U -p 10211 -j '*!*256*',
qui paraissait prometteur. Il est temps de récupérer le code-source de l’application (autant profiter du fait qu’elle soit open-source).
On clone donc le dépôt https://github.com/Kunzisoft/KeePassDX, et l’on se met sur le tag 4.5.2 qui correspond à la version étudiée.
Une recherche de « 256 » donne plusieurs centaines de résultats. Si l’on exclut de ce résultat volumineux :
- tout les fichiers qui ne contiennent pas de code-source
- les fichiers contenant du code de test
- les fichiers contenant du code natif
On aboutit à un nombre plus raisonnable de résultats. On raffine cette recherche en recherchant « sha_256 », « sha-256 » et « sha256 ».
Tout ceci finit par nous orienter sur une fonction retrievePasswordKey présente dans le fichier fichier MasterCredential.kt :
/**
* Retrieve the key from a password.
* @param key The password to hash.
* @param encoding The character encoding to use.
* @return The SHA-256 hash of the password.
* @throws IOException If the encoding or hashing fails.
*/
@Throws(IOException::class)
fun retrievePasswordKey(
key: CharArray,
encoding: Charset
): ByteArray {
val byteBuffer = encoding.encode(CharBuffer.wrap(key))
val bKey = ByteArray(byteBuffer.remaining())
byteBuffer.get(bKey)
val hash = HashManager.sha256(bKey)
bKey.clear()
if (byteBuffer.hasArray()) {
byteBuffer.array().clear()
}
return hash
}
Le commentaire avant la fonction indique que le premier argument de cette méthode est le mot de passe, visiblement le mot de passe maître, ce qui est de très bon augure !
Traçons donc cette méthode :
$ frida-trace -U -p 10211 -j '*!*retrievePasswordKey*'
Instrumenting...
MasterCredential$CREATOR.retrievePasswordKey: Auto-generated handler at "/home/thomas/articles/keepassDX/__handlers__/com.kunzisoft.keepass.database.element.MasterCredential_CREATOR/retrievePasswordKey.js"
Started tracing 1 function. Web UI available at http://localhost:46481/
/* TID 0x281f */
9784 ms MasterCredential$CREATOR.retrievePasswordKey(["P","l","o","u","f","1","2","3","!"], "<instance: java.nio.charset.Charset, $className: com.android.icu.charset.CharsetICU>")
9785 ms <= [-67,44,125,121,69,78,58,-88,-98,55,-105,-36,-19,109,58,25,-92,101,-105,125,-52,-41,-71,74,46,3,-115,-115,12,64,-27,36]
9786 ms MasterCredential$CREATOR.retrievePasswordKey(["P","l","o","u"], "<instance: java.nio.charset.Charset, $className: com.android.icu.charset.CharsetICU>")
9787 ms <= [89,-64,-116,-43,84,122,-84,-49,90,-100,-47,-5,10,-115,75,108,-16,-77,-4,-83,-11,109,-38,-103,-63,12,-59,-15,-70,-14,-11,-45]
Cette méthode reçoit bien le mot de passe maître en entrée !
Le site cyberchef (https://gchq.github.io/CyberChef/#recipe=Encode_text('UTF-8%20(65001)')SHA2('256',64,160)&input=UGxvdWYxMjMh))
nous permet de conclure que le résultat retourné par retrievePasswordKey est le haché du mot de passe lorsque ce dernier est encodé en UTF-8.
À ce stade, nous avons réussi une première étape décisive, trouver une extrémité de la pelote.
On pourrait d’ores et déjà commencer à la dérouler, mais nous avons vu plus haut que salsa20 semble utilisé d’une manière ou d’une autre, et que cela implique une classe com.kunzisoft.encrypt.StreamCipher :
package com.kunzisoft.encrypt
import org.bouncycastle.crypto.CipherParameters
import org.bouncycastle.crypto.DataLengthException
/**
* Stream cipher to process data
*/
class StreamCipher(private val streamCipher: org.bouncycastle.crypto.StreamCipher) {
/**
* Initialise the cipher.
*
* @param forEncryption if true the cipher is initialised for
* encryption, if false for decryption.
* @param params the key and other data required by the cipher.
* @exception IllegalArgumentException if the params argument is
* inappropriate.
*/
@Throws(IllegalArgumentException::class)
fun init(forEncryption: Boolean, params: CipherParameters?) {
streamCipher.init(forEncryption, params)
}
/**
* process a block of bytes from in putting the result into out.
*
* @param data the input byte array.
* @return the output buffer.
* @exception DataLengthException if the output buffer is too small.
*/
@Throws(DataLengthException::class)
fun processBytes(data: ByteArray): ByteArray {
val size = data.size
val out = ByteArray(size)
streamCipher.processBytes(data, 0, size, out, 0)
return out
}
}
On y voit une méthode processBytes, nnous qui suggère une méthode chiffrant ou déchiffrant des données. Nous allons donc tracer cette méthode :
$ frida-trace -U -p 10058 -j '*!*deriveMasterKey*' -j '*!*alsa20*' -j '*!*processBytes*'
Instrumenting...
DatabaseKDB.deriveMasterKey: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/com.kunzisoft.keepass.database.element.database.DatabaseKDB/deriveMasterKey.js"
DatabaseKDBX.deriveMasterKey: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/com.kunzisoft.keepass.database.element.database.DatabaseKDBX/deriveMasterKey.js"
HashManager.getSalsa20: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/com.kunzisoft.encrypt.HashManager/getSalsa20.js"
Salsa20Engine.processBytes: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/org.bouncycastle.crypto.engines.Salsa20Engine/processBytes.js"
StreamCipher.processBytes: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/org.bouncycastle.crypto.StreamCipher/processBytes.js"
StreamCipher.processBytes: Auto-generated handler at "/home/thomas/articles/keepassDX/__handlers__/com.kunzisoft.encrypt.StreamCipher/processBytes.js"
AEADCipher.processBytes: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/com.android.org.bouncycastle.crypto.modes.AEADCipher/processBytes.js"
BufferedBlockCipher.processBytes: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/com.android.org.bouncycastle.crypto.BufferedBlockCipher/processBytes.js"
BaseBlockCipher$AEADGenericBlockCipher.processBytes: Auto-generated handler at "/home/thomas/articles/keepassDX/__handlers__/com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher_AEADGenericBlockCipher/processBytes.js"
BaseBlockCipher$BufferedGenericBlockCipher.processBytes: Auto-generated handler at "/home/thomas/articles/keepassDX/__handlers__/com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher_BufferedGenericBlockCipher/processBytes.js"
PaddedBufferedBlockCipher.processBytes: Loaded handler at "/home/thomas/articles/keepassDX/__handlers__/com.android.org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher/processBytes.js"
BaseBlockCipher$GenericBlockCipher.processBytes: Auto-generated handler at "/home/thomas/articles/keepassDX/__handlers__/com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher_GenericBlockCipher/processBytes.js"
Started tracing 12 functions. Web UI available at http://localhost:46517/
/* TID 0x277c */
8602 ms DatabaseKDBX.deriveMasterKey("<instance: com.kunzisoft.keepass.database.element.MasterCredential>", "<instance: kotlin.jvm.functions.Function2, $className: com.kunzisoft.keepass.services.DatabaseTaskNotificationService$$ExternalSyntheticLambda1>")
8672 ms HashManager.getSalsa20([-18,-9,63,44,-29,80,-75,75,65,89,-26,-43,125,-61,69,1,-9,124,109,-44,-32,70,121,61,8,-45,66,-55,-116,-66,-82,-31])
8675 ms <= "<instance: com.kunzisoft.encrypt.StreamCipher>"
8678 ms StreamCipher.processBytes([28,-57,23,113,5,-72,-116,116,-46,1,2,79,0,52,120])
8678 ms | Salsa20Engine.processBytes([28,-57,23,113,5,-72,-116,116,-46,1,2,79,0,52,120], 0, 15, [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 0)
8679 ms | <= 15
8679 ms <= [86,105,116,99,111,116,99,111,100,101,99,49,50,51,33]
8680 ms StreamCipher.processBytes([])
8680 ms | Salsa20Engine.processBytes([], 0, 0, [], 0)
8680 ms | <= 0
8680 ms <= []
8681 ms StreamCipher.processBytes([-117,-50,-27,-18,-103,68,67,-3,-7,-114,84])
8681 ms | Salsa20Engine.processBytes([-117,-50,-27,-18,-103,68,67,-3,-7,-114,84], 0, 11, [0,0,0,0,0,0,0,0,0,0,0], 0)
8681 ms | <= 11
8681 ms <= [90,111,114,103,108,117,98,49,50,51,33]
8681 ms StreamCipher.processBytes([])
8682 ms | Salsa20Engine.processBytes([], 0, 0, [], 0)
8682 ms | <= 0
8682 ms <= []
8682 ms StreamCipher.processBytes([76,95,98,-81,30])
8682 ms | Salsa20Engine.processBytes([76,95,98,-81,30], 0, 5, [0,0,0,0,0], 0)
8682 ms | <= 5
8682 ms <= [70,114,33,116,101]
8683 ms StreamCipher.processBytes([])
8683 ms | Salsa20Engine.processBytes([], 0, 0, [], 0)
8683 ms | <= 0
8683 ms <= []
Les tableaux retournés par processBytes contiennent des octets dont la valeur (ici en décimal) est l’encodage ascii des caractères des mots de passe présents dans la base de donnée : Cette fonction déchiffre les mots de passe !
Nous avions déjà identifié le premier bout de la pelote, la fonction recevant le mot de passe maître. Nous tenons maintenant la deuxième extrémité, la fonction qui retourne les mots de passe déchiffrés.
Résultats
De fil en aiguille, on reconstitue la quasi-totalité du processus de déchiffrement de la base de donnée KeepassDX et de son format.
Le processus de déchiffrement est le suivant :
- Calcul du haché SHA256 du mot de passe.
- Ce haché est concaténé avec les autres secrets initiaux éventuellement fournis par un fichier et/ou par une enclave materielle. Cette concaténation est hachée avec SHA256.
- Le haché obtenu est traité par une KDF. Cette KDF exécute un certain nombre d’itérations d’AES en mode ECB, la clé de chiffrement provenant d’un en-tête présent au début de la base de donnée (header de type 5). La première itération chiffre le haché, les suivantes chiffrent le résultat de l’itération précédente. Le nombre d’iérations effectuées est spécifié par le header de type6. Ici, 500000 itérations sont effectuées, ce qui est le nombre par défaut présent dans le code.
- La KDF se termine par un appel à SHA256 du résultat des itérations AES. Ce haché est concaténé avec le contenu du header de type 4 et un nouvel appel à SHA256 est passé sur cette concaténation.
- La clé AES256 ainsi obtenue est utilisée pour déchiffrer le contenu proprement dit de la base de donnée. Le mode opératoire est CBC, le vecteur d’initialisation provenant de l’en-tête de type 7.
- Le contenu déchiffré est décompressé avec l’algorithme de décompression spécifié par l’en-tête n°3 de la base de donnée. On retrouve alors un fichier XML, contenant des noeuds
Entry. - Chaque mot de passe de la base de donnée fait l’objet d’un noeud
Entry. Un tel noeud contient un mot de passe chiffré puis encodé en base64, le nom d’utilisateur et l’url associés, ainsi que diverses métadonnées.
Voici un exemple de noeud Entry :
<Entry>
<UUID>1kJz/BdODNzUaW8O0U7CiQ==</UUID>
<IconID>0</IconID>
<ForegroundColor></ForegroundColor>
<BackgroundColor></BackgroundColor>
<OverrideURL></OverrideURL>
<Times>
<LastModificationTime>2026-08-19T17:47:44Z</LastModificationTime>
<CreationTime>2026-08-19T17:46:59Z</CreationTime>
<LastAccessTime>2026-08-19T17:47:44Z</LastAccessTime>
<ExpiryTime>2999-11-28T23:59:59Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>2</UsageCount>
<LocationChanged>2026-08-19T17:47:44Z</LocationChanged>
</Times>
<String>
<Key>Title</Key>
<Value>poulet</Value>
</String>
<String>
<Key>UserName</Key>
<Value>cotcot</Value>
</String>
<String>
<Key>Password</Key>
<Value Protected="True">HMcXcQW4jHTSAQJPADR4</Value>
</String>
<String>
<Key>URL</Key>
<Value>https://poulet.fr</Value>
</String>
<String>
<Key>Notes</Key>
<Value Protected="True"></Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
<History />
</Entry>
Pour retrouver le mot de passe en question (« Vitcotcodec123! »), il suffit de décoder en base64 le contenu de la String « Password », puis de le déchiffrer avec Salsa20.
Le déchiffrement utilise le SHA256 d’une clé présente dans le header n°8, ainsi qu’un nonce fixe présent dans le code
/**
* Get a Salsa20 stream cipher instance.
*
* @param key Master key to derive the cipher key
* @return Initialized Salsa20 StreamCipher
*/
fun getSalsa20(key: ByteArray): StreamCipher {
// Build stream cipher key
val key32 = sha256(key)
val keyParam = KeyParameter(key32)
val salsaIV = byteArrayOf(
0xE8.toByte(),
0x30,
0x09,
0x4B,
0x97.toByte(),
0x20,
0x5D,
0x2A
)
val ivParam = ParametersWithIV(keyParam, salsaIV)
val cipher = Salsa20Engine()
cipher.init(true, ivParam)
return StreamCipher(cipher)
}
Conclusions
Nous pouvons maintenant dégager une vue d’ensemble du déchiffrement d’une base de donnée KeepassDX :

On aboutit également à un script frida qui trace l’intégralité des opérations impliquées par l’ouverture d’une base de donnée KeepassDX, ainsi qu’un script python reproduisant ces calculs.
Notons que ce résultat est partiel : La base de donnée étudiée utilise la version 3.1 du format kbdx.
Le script python développé ne peut donc pas en l’état ouvrir de base de donnée dans d’autres versions du format (le plus récent étant kbdx 4).
Toutefois, ce format est relativement apparenté à celui étudié, et les résultats du présent article pourront servir de base de départ d’une analyse de ces autres versions du format kbdx.
Annexes
Les scripts frida et python : https://github.com/T0lva/dekeepass