Vault
Un gestionnaire de mots de passe fait maison.
Vault garde les mots de passe, les cartes, les documents et les codes de double authentification. Tout est chiffré sur l’appareil, avant de partir : le serveur ne stocke que des données qu’il ne peut pas ouvrir.
Nous avons commencé Vault fin août 2026 : une application Mac, iPhone et iPad en SwiftUI, une application Android en Kotlin, et un serveur Laravel hébergé en Suisse.
Avant la première ligne de l’application, il y a eu un contrat : la spécification du format du coffre, et des vecteurs de test que chaque plateforme rejoue. C’est lui qui garantit que trois programmes écrits séparément se comprennent.
- Pour
- Particuliers et indépendants
- Plateformes
- Mac
- iPhone et iPad
- Android
- Technologies
- Swift
- SwiftUI
- Kotlin
- Jetpack Compose
- Laravel
- PostgreSQL
Privé et sécurisé, de bout en bout.

Chiffré à l’écriture
Une règle décide de tout le reste : aucune clé capable d’ouvrir un élément ne quitte l’appareil. Le chiffrement se fait à l’écriture, pas au transport. Les données arrivent chiffrées, sont stockées chiffrées et repartent chiffrées.
Le serveur ne voit en clair que l’identifiant de chaque élément, sa catégorie, ses dates et la taille de ses blocs. Il sait qu’un élément est une carte de crédit, jamais laquelle.

Le mot de passe ne part jamais
À la connexion, l’application prouve qu’elle connaît le mot de passe sans l’envoyer, même haché : c’est le protocole SRP-6a. Un serveur compromis ne peut donc pas les récolter au passage. Les paramètres de dérivation sont retenus par l’application, pour qu’un serveur malveillant ne puisse pas les affaiblir à distance.
La clé du coffre sort de deux millions d’itérations PBKDF2. Une copie de la base ne livre que des blocs chiffrés, et chaque essai pour deviner un mot de passe coûte ce calcul. C’est pourquoi la création du coffre refuse un mot de passe trop faible, et impose un kit de récupération, qui revient à l’écran à chaque ouverture tant qu’il n’a pas été mis à l’abri.


Hors connexion, tout fonctionne
L’application lit et écrit dans une base locale, qui fait foi. Consulter, chercher, créer, modifier ou supprimer : rien n’attend le réseau. Seules deux choses en ont besoin, créer un lien de partage et ouvrir le coffre sur un nouvel appareil.
La synchronisation tourne en arrière-plan. Quand deux appareils modifient le même élément, la dernière écriture gagne, et l’autre version reste dans l’historique de l’élément. Une suppression laisse une trace, pour qu’un appareil resté longtemps hors ligne ne ressuscite pas ce qui a été effacé.

Une clé par élément
Chaque élément a sa propre clé, elle-même chiffrée par la clé du coffre. C’est ce qui rend le partage possible : partager un mot de passe transmet la clé de cet élément, et rien d’autre du coffre ne devient lisible.
Le lien porte la clé après le signe #, une partie de l’adresse que le navigateur n’envoie jamais au serveur. Le lien expire après une heure, un jour, sept jours ou jamais, peut ne s’ouvrir qu’une fois, ou être réservé à quelques adresses e-mail, qui reçoivent un code. Les codes de double authentification et les clés d’accès ne se partagent pas.



Deux langages, un seul contrat
Swift et Kotlin réimplémentent chacun la même cryptographie, sans une ligne de code en commun et sans bibliothèque tierce : chaque algorithme retenu existe nativement sur les deux plateformes. Sur Apple, un seul noyau sert le Mac, l’iPhone et l’iPad.
Ce qui les fait se comprendre, ce sont les vecteurs du contrat : des valeurs calculées une fois à partir d’entrées figées. Si Android déchiffre ce que le Mac a chiffré, et l’inverse, l’interopérabilité est prouvée à chaque exécution des tests. Toute modification du format change la spécification et les vecteurs dans le même commit.

{
"oneTimePassword": {
"codes": [
{
"algorithm": "SHA1",
"code": "94287082",
"digits": 8,
"period": 30,
"secretBase32": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
"unixTime": 59
},
{
"algorithm": "SHA256",
"code": "46119246",
"digits": 8,
"period": 30,
"secretBase32": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA====",
"unixTime": 59
}
]
}
}@Test("les codes à usage unique du contrat sont recalculés à l'identique")
func oneTimePasswordCodes() throws {
let root = try storedVectors()
let section = try #require(root["oneTimePassword"] as? [String: Any])
let cases = try #require(section["codes"] as? [[String: Any]])
#expect(!cases.isEmpty)
for entry in cases {
let secret = try #require(entry["secretBase32"] as? String)
let rawAlgorithm = try #require(entry["algorithm"] as? String)
let algorithm = try #require(OTPAlgorithm(rawValue: rawAlgorithm))
let digits = try #require(entry["digits"] as? Int)
let period = try #require(entry["period"] as? Int)
let time = try #require(entry["unixTime"] as? Int)
let expected = try #require(entry["code"] as? String)
let password = OneTimePassword(
secretBase32: secret, algorithm: algorithm, digits: digits, period: period
)
let computed = TOTPGenerator.code(for: password, at: Date(timeIntervalSince1970: TimeInterval(time)))
#expect(computed == expected, "t=\(time) \(algorithm.rawValue) : \(computed) au lieu de \(expected)")
}
}@Test
fun `les codes du contrat sont reproduits`() {
for (entry in codes) {
val row = entry.jsonObject
val password = OneTimePassword(
secretBase32 = OneTimePassword.normalizeSecret(
row.getValue("secretBase32").jsonPrimitive.content,
),
algorithm = OtpAlgorithm.fromRawValue(
row.getValue("algorithm").jsonPrimitive.content,
)!!,
digits = row.getValue("digits").jsonPrimitive.int,
period = row.getValue("period").jsonPrimitive.int,
)
val time = row.getValue("unixTime").jsonPrimitive.long
assertEquals(
"${password.algorithm.rawValue} à $time",
row.getValue("code").jsonPrimitive.content,
TotpGenerator.code(password, time),
)
}
}
Trois implémentations, une seule spécification.
- Ligne de code partagée entre Swift et Kotlin
- 0
- Ligne de code partagée entre Swift et Kotlin
- Itérations pour dériver la clé
- 2 M
- Itérations pour dériver la clé
- Tests en Swift et en Kotlin
- 1 900+
- Tests en Swift et en Kotlin
- Avant d’effacer un secret copié
- 90 s
- Avant d’effacer un secret copié
Une interface faite maison
Aucun composant système, sur aucune plateforme : ni Material Design sur Android, ni boutons ou listes standards sur Mac, ni police ou icônes du système. Chaque contrôle est dessiné à partir des mêmes jetons de couleur, et la même police à chasse fixe signe l’application partout. Sur Android, le build échoue dès qu’un import Material apparaît.
L’interface ne commente pas ce qu’elle montre. Les valeurs sensibles se révèlent une par une, un mot de passe s’affiche en grands caractères numérotés pour être recopié, et un secret copié quitte le presse-papiers au bout de 90 secondes.





Le coffre se referme seul
Après une à cinq minutes ou une heure sans activité dans l’application, ou quand l’ordinateur se met en veille, le coffre efface ses clés de la mémoire. Lire une page web pendant vingt minutes ne le garde pas ouvert.
L’empreinte ou Touch ID rouvrent le coffre grâce à une clé gardée par la puce de sécurité de l’appareil. Elle est invalidée dès qu’une empreinte est ajoutée. Sur Mac comme sur Android, Vault remplit les formulaires et sert de fournisseur de clés d’accès pour les sites.
