Aller au contenuAller à la navigationAller au contact

Applications

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.

Vault sur Mac et sur Android

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 coffre sur Mac, une connexion ouverte

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.

La création d’un coffre, avec la jauge de force
Le kit de récupération

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é.

Deux appareils, un serveur qui ne fait que transmettre

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.

Mot de passe, clé du coffre, clé de l’élément
Les réglages d’un lien de partage
Le lien créé, avec sa clé

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.

La spécification et ses vecteurs, rejoués par Swift, Kotlin et le serveur
Shared/test-vectors/vault-format-v1.json
{
  "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
      }
    ]
  }
}
VaultCoreTests/SharedVectorTests.swift
@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)")
    }
}
core/otp/TotpGeneratorTest.kt
@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 dessin de l’icône, de la porte de coffre à la roue
Le coffre sur Android
Une connexion et son code à usage unique sur Android
Une carte de crédit sur Android
Un mot de passe en grands caractères

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.

Le thème sombre, sur Mac et sur Android

Des données sensibles à protéger ?Ou le projet que vous n’osez pas lancer.

Un gros projet se mène comme un petit : une étape à la fois. La première, c’est un café à Vevey ou un appel.

Parlons-en