VivoKey Vault

VivoKey Vault is a secure file storage application for Android that uses VivoKey NFC implants and our Verify API for authentication and encryption. Files are encrypted with AES-256-GCM and can only be accessed by physically tapping your registered chip, ensuring your data remains protected even if your device is compromised. The app integrates with Android's file system, allowing you to open vault files directly in other applications while maintaining end-to-end encryption.

Configuration

Once VivoKey Vault is installed, you will need to obtain a developer account ID and API key from our API page via email. Your developer ID is tied to master key derivation.

Online Consideration

Because VivoKey Vault requires the use of the Verify API, your device must be online in order to unlock your vault. Once unlocked, file access does not require access to the Verify API. It is possible to unlock your vault in definitely (until phone restart), then go offline and maintain access to your vault contents until it is locked or your device is restarted.

Multiple Vault Support

New vaults can be created by scanning a new, unregistered VivoKey NFC chip. With a focus on privacy and security, multiple vaults can silently exist on the device with no indication of how many vaults may be created. If necessary, this allows a kind of "duress vault" to be shown by scanning one VivoKey NFC chip, while keeping a different vault associated with a different VivoKey NFC chip.

Multiple Chip Support

By using key slots and key wrapping, it's possible to register multiple VivoKey NFC chips with a single vault. However, due to the way multi-vault support works, you cannot register a single VivoKey NFC chip with more than one vault.

Backup and Restore

Vault backups are exported as encrypted .vkv files that contain all your files, metadata, and cryptographic key slots. Each backup includes the vault's master encryption key wrapped (encrypted) separately for every chip that was registered to the vault at the time of export. Without physical access to one of those registered chips, the backup file is cryptographically useless—there are no passwords or recovery codes that can decrypt it.

There are two ways to use a backup file depending on your situation. If you don't have a vault yet, you can restore a complete vault from a backup by scanning any chip that was registered when the backup was created—this recreates the vault with all its files and preserves the original vault name, but only registers the chip you used to restore (any other chips from the original vault must be re-added manually). If you already have an unlocked vault, you can import files from a backup into it. The import will identify files that don't already exist (by content hash) and add only new files, skipping duplicates and leaving your current vault name and registered chips unchanged.

VivoKey Vault Security Architecture

This document provides a detailed technical explanation of the security mechanisms used in VivoKey Vault, including encryption, key management, multi-chip support, and backup/restore security.

Table of Contents

  1. Overview
  2. Cryptographic Primitives
  3. Key Derivation from VivoKey Chips
  4. FIDO2 Offline Authentication
  5. Master Key Generation and Storage
  6. Multi-Chip Key Wrapping Architecture
  7. File Encryption
  8. Auto-Lock and Session Security
  9. Backup File Format
  10. Backup Security Analysis
  11. Threat Model


Overview

VivoKey Vault uses a layered encryption architecture where:

  1. A master key encrypts all vault contents (files and metadata)
  2. The master key is wrapped (encrypted) using keys derived from VivoKey NFC implants
  3. Multiple chips can be registered, each storing their own wrapped copy of the master key
  4. Backups include wrapped key slots, allowing restoration on new devices with any registered chip

The app supports two authentication methods:

  • Online Authentication (Spark, Spark 2) - Chip identity verified via VivoKey API, requires network
  • FIDO2 Offline Authentication (Apex) - Uses FIDO 2.1/CTAP 2.1 with hmac-secret extension, works completely offline

This design ensures that:

  • Vault contents cannot be accessed without physical possession of a registered VivoKey chip
  • Multiple chips can unlock the same vault (backup access)
  • Backups can be restored on new devices without re-registering chips
  • Loss of a single chip doesn't mean loss of data (if other chips are registered)
  • FIDO2-enabled chips can access the vault without any network connectivity


Cryptographic Primitives

All cryptographic operations use industry-standard algorithms:

Purpose Algorithm Key Size Notes
Symmetric Encryption AES-256-GCM 256 bits Authenticated encryption with 128-bit auth tag
Key Derivation SHA-256 256 bits Derives chip keys from chip ID
Content Hashing SHA-256 256 bits For file deduplication
IV/Nonce Random 96 bits Unique per encryption operation
// From VaultCrypto.kt
companion object {
    private const val ALGORITHM = "AES"
    private const val TRANSFORMATION = "AES/GCM/NoPadding"
    private const val GCM_IV_LENGTH = 12      // 96 bits
    private const val GCM_TAG_LENGTH = 128    // 128-bit authentication tag
    private const val KEY_LENGTH = 32         // 256 bits
}

Why AES-256-GCM?

  • Provides both confidentiality and integrity (authenticated encryption)
  • Widely audited and considered secure
  • Hardware acceleration available on modern Android devices
  • Detects tampering or corruption of encrypted data


Key Derivation from VivoKey Chips

When a VivoKey chip authenticates with the app, it provides a unique, stable chip ID (the raw payload from authentication). This chip ID is used to derive an AES-256 key:

// From VaultCrypto.kt
fun deriveKeyFromChipId(chipId: String): SecretKey {
    val digest = MessageDigest.getInstance("SHA-256")
    val keyBytes = digest.digest(chipId.toByteArray(Charsets.UTF_8))
    return SecretKeySpec(keyBytes, ALGORITHM)
}

Security properties:

  • The chip ID is a cryptographic secret that only the physical chip can produce
  • SHA-256 produces a uniformly distributed 256-bit key
  • The same chip always produces the same derived key
  • Different chips produce cryptographically independent keys

Chip identification (for UI display):

To identify chips without exposing the chip ID, we hash it again:

// From VaultCrypto.kt
fun hashChipId(chipId: String): String {
    val digest = MessageDigest.getInstance("SHA-256")
    val hashBytes = digest.digest(chipId.toByteArray(Charsets.UTF_8))
    return hashBytes.joinToString("") { "%02x".format(it) }
}

This hash is used as the filename for key slots and for displaying chip identifiers to users.

Important: NFC UID is NOT used for cryptography

The NFC UID (the 7-byte identifier readable from any NFC tag) is never used for key derivation, salting, or any cryptographic operation. The UID is:

  • Only used for display purposes - Generating default chip nicknames (e.g., "Spark 2 AABBCCDD")
  • Stored in encrypted preferences - Associated with the chip hash for UI display
  • Not included in backup files - Nicknames and UIDs are device-local preferences

The cryptographic key material comes exclusively from:

  • Spark/Spark 2: The sub claim from the authenticated JWT (a cryptographic secret from the VivoKey API)
  • FIDO2 (Apex): The hmac-secret extension output (derived from the credential's private key)


FIDO2 Offline Authentication

VivoKey Vault supports FIDO2 authentication for Apex chips. This enables completely offline vault access without any network connectivity using the hmac-secret extension for deterministic key derivation.

FIDO 2.1 / CTAP 2.1 Requirements

VK Vault requires FIDO 2.1 / CTAP 2.1 compliant authenticators. This is enforced because:

Requirement Why It's Needed
FIDO 2.1 versions Must report FIDO_2_1 or FIDO_2_1_PRE in authenticatorGetInfo
PIN Protocol 2 Uses HKDF-based key derivation (more secure than Protocol 1's SHA-256)
pinUvAuthToken Permission-based tokens for hmac-secret operations
hmac-secret extension Required for deterministic key derivation

Authenticators that only support FIDO 2.0 / CTAP 2.0 are rejected with an error message before PIN entry.

Architecture: Non-Resident Keys + hmac-secret

Unlike traditional FIDO2 implementations that store resident keys on the authenticator, VK Vault uses non-resident keys with server-side credential storage. This provides several advantages:

  • No resident key slots consumed - The FIDO2 token remains stateless
  • Multi-device support - Same token works across unlimited devices
  • Simpler UX - No credential selection needed; app manages credentials

How FIDO2 Authentication Works

Registration Flow

1. makeCredential (rk=false)     -> Get credentialId (contains encrypted private key)
2. getAssertion + hmac-secret    -> Get deterministic secret output
3. Generate random unwrapping key
4. Encrypt unwrapping key with hmac-secret output
5. Store: credentialId + encrypted unwrapping key in .fido2slot file
6. Wrap master key with unwrapping key

Unlock Flow

1. Load all stored credentials from .fido2slot files
2. For each credential:
   a. getAssertion with allowList=[credentialId] + hmac-secret
   b. If NO_CREDENTIALS: try next credential
   c. If success: decrypt unwrapping key -> unwrap master key -> vault unlocked
3. If no match found: create new credential

The hmac-secret Extension

The hmac-secret extension provides deterministic cryptographic output based on the credential's private key:

credentialPrivateKey (never leaves chip)
        |
        v
HMAC-SHA256(wrapperKey, credentialPrivateKey)
        |
        v
hmacSecretKey (deterministic per credential)
        |
        v
HMAC-SHA256(hmacSecretKey, appSalt)
        |
        v
output (deterministic given same credential + salt)

Key properties:

  • Output is deterministic for the same credential and salt
  • Different output with/without user verification (UV)
  • Requires PIN/UV for each operation
  • Private key never leaves the secure element

Credential Storage Format

Each .fido2slot file contains serialized Fido2CredentialData:

+----------------------------------------------------------+
|  Version (1 byte): 0x01                                   |
|  CredentialId Length (2 bytes, big-endian)               |
|  CredentialId (variable, typically 96-144 bytes)         |
|  Encrypted Unwrapping Key:                               |
|    [12-byte IV] + [32-byte key + 16-byte GCM tag]        |
+----------------------------------------------------------+

The unwrapping key is encrypted with AES-256-GCM using the hmac-secret output as the key.

PIN Protection

FIDO2 chips can be configured with a PIN for additional security:

  • PIN required for hmac-secret - The extension requires user verification
  • PIN retry limits - The chip enforces retry limits; after too many failures, the PIN is blocked
  • Same-token validation - After PIN entry, the user must rescan the same token to complete authentication

Security Properties of FIDO2

Property Description
Offline Operation No network required; all authentication happens locally via NFC
Hardware-bound Keys Credential private keys cannot be extracted from the chip
Deterministic Output hmac-secret provides same output for same credential + salt
PIN Protection Required for hmac-secret extension operations
PIN Protocol 2 CTAP 2.1 uses HKDF-based key derivation for PIN operations
Replay Protection CTAP 2.1 includes signature counters to detect cloning
No API Credentials Unlike online authentication, no Developer ID or API Key needed
Stateless Token Non-resident keys mean the token stores nothing

FIDO2 vs Online Authentication

Aspect FIDO2 (Apex) Online (Spark/Apex Online)
Network Required No Yes
API Credentials Not needed Developer ID + API Key
Key Source hmac-secret output Chip ID from JWT sub claim
PIN Support Yes (required for hmac-secret) No
Chip Verification Local (CTAP 2.1) Server-verified
Protocol Version FIDO 2.1 / CTAP 2.1 required N/A
PIN Protocol Protocol 2 (HKDF-based) N/A
Multi-device Same token works everywhere Same token works everywhere
Token Storage None (stateless) None

Why hmac-secret Instead of Signatures?

ECDSA signatures in FIDO2 are non-deterministic (random k value per signature), making them unsuitable for key derivation. The hmac-secret extension provides:

  1. Deterministic output - Same input always produces same output
  2. Hardware-protected secret - Derived from credential's private key
  3. App-controlled salt - VK Vault uses a fixed app-specific salt
  4. PIN-gated access - Requires user verification for each operation


Master Key Generation and Storage

Master Key Generation

When a new vault is created, a random 256-bit master key is generated:

// From VaultCrypto.kt
fun generateMasterKey(): SecretKey {
    val keyGenerator = KeyGenerator.getInstance(ALGORITHM)
    keyGenerator.init(KEY_LENGTH * 8, secureRandom)  // 256 bits
    return keyGenerator.generateKey()
}

This master key is used to encrypt all vault contents. It is never stored in plaintext - only wrapped copies exist on disk.

Key Wrapping

The master key is encrypted (wrapped) using the chip-derived key:

// From VaultCrypto.kt
fun wrapKey(masterKey: SecretKey, wrappingKey: SecretKey): ByteArray {
    return encrypt(masterKey.encoded, wrappingKey)
}

fun unwrapKey(wrappedKey: ByteArray, unwrappingKey: SecretKey): SecretKey {
    val keyBytes = decrypt(wrappedKey, unwrappingKey)
    return SecretKeySpec(keyBytes, ALGORITHM)
}

The wrapped key format is: [12-byte IV] + [AES-GCM encrypted master key + auth tag]

Key Slot Storage

Each registered chip has a "slot" file containing the wrapped master key:

// From KeySlotManager.kt
fun createVault(chipId: String): SecretKey {
    // Generate new master key
    val masterKey = crypto.generateMasterKey()

    // Derive key from chip ID
    val chipKey = crypto.deriveKeyFromChipId(chipId)

    // Wrap master key and save slot
    val wrappedKey = crypto.wrapKey(masterKey, chipKey)
    val chipHash = crypto.hashChipId(chipId)
    File(keyslotsDir, "$chipHash.slot").writeBytes(wrappedKey)

    return masterKey
}

Storage location: {app_private_dir}/keyslots/{chip_hash}.slot

The slot file is named by the chip hash (not the chip ID) to prevent correlation attacks if an attacker gains file system access.



Multi-Chip Key Wrapping Architecture

Multiple chips can be registered to the same vault. Each chip stores its own wrapped copy of the same master key:

                    +-------------------+
                    |   Master Key      |
                    |   (256-bit)       |
                    +---------+---------+
                              |
            +-----------------+-----------------+
            |                 |                 |
            v                 v                 v
     +--------------+  +--------------+  +--------------+
     |   Chip A     |  |   Chip B     |  |   Chip C     |
     |  Derived Key |  |  Derived Key |  |  Derived Key |
     +------+-------+  +------+-------+  +------+-------+
            |                 |                 |
            v                 v                 v
     +--------------+  +--------------+  +--------------+
     |  Wrapped Key |  |  Wrapped Key |  |  Wrapped Key |
     |   Slot A     |  |   Slot B     |  |   Slot C     |
     +--------------+  +--------------+  +--------------+

Adding a New Chip

// From KeySlotManager.kt
fun addChip(chipId: String, masterKey: SecretKey): Boolean {
    return try {
        val chipKey = crypto.deriveKeyFromChipId(chipId)
        val wrappedKey = crypto.wrapKey(masterKey, chipKey)
        val chipHash = crypto.hashChipId(chipId)
        File(keyslotsDir, "$chipHash.slot").writeBytes(wrappedKey)
        true
    } catch (e: Exception) {
        false
    }
}

Unlocking with Any Registered Chip

// From KeySlotManager.kt
fun unlockWithChip(chipId: String): SecretKey? {
    val chipHash = crypto.hashChipId(chipId)
    val slotFile = File(keyslotsDir, "$chipHash.slot")

    if (!slotFile.exists()) {
        return null  // Chip not registered
    }

    return try {
        val wrappedKey = slotFile.readBytes()
        val chipKey = crypto.deriveKeyFromChipId(chipId)
        crypto.unwrapKey(wrappedKey, chipKey)
    } catch (e: Exception) {
        null  // Decryption failed (wrong key or corrupted)
    }
}

Security note: The AES-GCM authentication tag ensures that if an attacker modifies a slot file, decryption will fail rather than producing an incorrect key.



File Encryption

Chunked Encryption Format (VKF2)

Files are encrypted using chunked AES-256-GCM with AAD (Additional Authenticated Data) binding. This format enables seekable random-access decryption while preventing chunk manipulation attacks.

File Structure

+------------------------------------------------------------+
|  HEADER (20 bytes)                                          |
|  +--------------------------------------------------------+ |
|  |  Magic: "VKF2" (4 bytes)                               | |
|  |  Chunk Size: 262144 (4 bytes, 256KB default)           | |
|  |  Total Chunks: N (4 bytes)                             | |
|  |  Base IV: (8 bytes, random per file)                   | |
|  +--------------------------------------------------------+ |
+------------------------------------------------------------+
|  CHUNKS                                                     |
|  +--------------------------------------------------------+ |
|  |  Chunk 0: [Ciphertext (up to 256KB)] [GCM Tag (16b)]   | |
|  |  Chunk 1: [Ciphertext (up to 256KB)] [GCM Tag (16b)]   | |
|  |  ...                                                    | |
|  |  Chunk N-1: [Ciphertext] [GCM Tag (16 bytes)]          | |
|  +--------------------------------------------------------+ |
+------------------------------------------------------------+

Per-Chunk Cryptographic Binding

Each chunk is independently encrypted with unique IV and AAD:

Chunk IV (12 bytes) = BaseIV (8 bytes) || ChunkIndex (4 bytes big-endian)
Chunk AAD (24 bytes) = FileUUID (16 bytes) || ChunkIndex (4 bytes) || TotalChunks (4 bytes)

Security properties of AAD binding:

  • Prevents chunk reordering - Wrong chunk index in AAD causes authentication failure
  • Prevents chunk deletion - TotalChunks mismatch causes authentication failure
  • Prevents cross-file attacks - FileUUID mismatch causes authentication failure
  • Prevents chunk duplication - Each position has unique IV/AAD combination

Why Chunked Encryption?

Traditional streaming AES-GCM cannot support random access - you must decrypt from the beginning. Chunked encryption allows:

  • Seekable media playback - Video/audio players can seek without decrypting the entire file
  • Efficient large file access - Only decrypt the chunks being read
  • Memory efficiency - No need to hold entire decrypted file in memory

Chunk Size Selection

The default chunk size of 256KB provides:

  • Good seeking granularity (~1 second of video at typical bitrates)
  • Reasonable overhead (16-byte tag per 256KB = 0.006% overhead)
  • Efficient memory usage per chunk

Encrypting Files

// From VaultCrypto.kt
fun encryptFileChunkedStreaming(
    inputStream: InputStream,
    outputFile: File,
    key: SecretKey,
    fileId: String,
    plaintextSize: Long,
    chunkSize: Int = 256 * 1024  // 256KB default
): ChunkedFileHeader {
    val baseIv = ByteArray(8)
    secureRandom.nextBytes(baseIv)
    val totalChunks = ((plaintextSize + chunkSize - 1) / chunkSize).toInt()

    // Write header, then encrypt each chunk with unique IV and AAD
    // ...
}

Random-Access Decryption

// From VaultCrypto.kt
fun decryptChunk(
    file: File,
    header: ChunkedFileHeader,
    chunkIndex: Int,
    key: SecretKey,
    fileId: String
): ByteArray {
    // Seek directly to chunk position
    val chunkOffset = HEADER_SIZE + (chunkIndex * encryptedChunkSize)

    // Build IV and AAD for this specific chunk
    val iv = buildChunkIv(header.baseIv, chunkIndex)
    val aad = buildChunkAad(fileIdBytes, chunkIndex, header.totalChunks)

    // Decrypt with authentication
    cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LENGTH, iv))
    cipher.updateAAD(aad)
    return cipher.doFinal(encryptedChunkData)
}

Metadata Encryption

Vault metadata (file names, sizes, timestamps) is encrypted as a single blob:

// From VaultCrypto.kt
fun encrypt(data: ByteArray, key: SecretKey): ByteArray {
    val iv = generateIv()
    val cipher = Cipher.getInstance(TRANSFORMATION)
    cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LENGTH, iv))
    val encrypted = cipher.doFinal(data)
    return iv + encrypted  // IV prepended to ciphertext
}


Auto-Lock and Session Security

The vault automatically locks after a period of inactivity to minimize the window of exposure if a device is left unattended.

Activity-Based Auto-Lock

The auto-lock timer resets on:

  • User interaction - Any touch/tap within the app
  • File access - Files opened via the DocumentsProvider (with 60-second grace period after close)
  • Import/Export operations - Timer is paused during backup operations
// From VaultViewModel.kt
// Don't auto-lock if importing/exporting or if files are open/recently accessed
if (_isImporting.value || _isExporting.value ||
    vaultRepository.hasActiveFileAccess(FILE_ACCESS_GRACE_PERIOD)) {
    lastActivityTime = System.currentTimeMillis()
    continue
}

DocumentsProvider Integration

The vault exposes files to other apps via Android's Storage Access Framework (SAF) through a custom DocumentsProvider. This allows apps like PDF viewers, media players, or document editors to access decrypted file content while the vault is unlocked.

Seekable File Access:

Files are served via StorageManager.openProxyFileDescriptor() with a custom callback that:

  • Reports accurate file size via onGetSize()
  • Handles random-access reads by decrypting the appropriate chunk(s)
  • Caches the most recently accessed chunk for sequential read performance
  • Enables seeking in media players (video scrubbing, audio seeking)
// From SecureFilesProvider.kt
override fun onRead(offset: Long, size: Int, data: ByteArray): Int {
    // Determine which chunk contains the requested offset
    val chunkIndex = (offset / chunkSize).toInt()

    // Decrypt chunk (cached if recently accessed)
    val chunkData = getChunk(chunkIndex)

    // Copy requested portion to output buffer
    // ...
}

Security considerations:

  • Files are decrypted on-the-fly per-chunk (not written to disk)
  • Only the requested chunks are decrypted (not the entire file)
  • Access is only available while the vault is unlocked
  • Open file handles are tracked to prevent auto-lock while files are in use
  • Thumbnails are served from encrypted metadata (base64 encoded, stored with file records)


Backup File Format

The .vkv backup file format (version 3) is designed to allow restoration on new devices while maintaining security.

File Structure

+------------------------------------------------------------+
|                    BACKUP FILE (.vkv)                       |
+------------------------------------------------------------+
|  HEADER (Unencrypted)                                       |
|  +--------------------------------------------------------+ |
|  |  Magic bytes: "VKV2" (4 bytes)                         | |
|  |  Number of wrapped keys: N (4 bytes, big-endian)       | |
|  |  +----------------------------------------------------+| |
|  |  |  Wrapped Key 1 size (4 bytes)                      || |
|  |  |  Wrapped Key 1 data (variable)                     || |
|  |  +----------------------------------------------------+| |
|  |  |  Wrapped Key 2 size (4 bytes)                      || |
|  |  |  Wrapped Key 2 data (variable)                     || |
|  |  +----------------------------------------------------+| |
|  |  |  ... (N wrapped keys total)                        || |
|  |  +----------------------------------------------------+| |
|  +--------------------------------------------------------+ |
+------------------------------------------------------------+
|  PAYLOAD (Encrypted with master key via AES-256-GCM)        |
|  +--------------------------------------------------------+ |
|  |  [12-byte IV] + [Encrypted ZIP + auth tag]             | |
|  |                                                        | |
|  |  ZIP Contents (after decryption):                      | |
|  |  +-- metadata.json (includes base64 thumbnails)        | |
|  |  +-- files/                                            | |
|  |      +-- {uuid1}.enc                                   | |
|  |      +-- {uuid2}.enc                                   | |
|  |      +-- ...                                           | |
|  +--------------------------------------------------------+ |
+------------------------------------------------------------+

Export Implementation

// From VaultBackup.kt
suspend fun exportVault(
    outputStream: OutputStream,
    vaultDir: File,
    metadata: VaultMetadata,  // Includes thumbnails as base64 in VaultFile entries
    key: SecretKey,
    wrappedKeys: List<ByteArray>
): Result<Unit> = withContext(Dispatchers.IO) {
    try {
        // First, create the ZIP in memory
        val zipBytes = ByteArrayOutputStream()
        ZipOutputStream(zipBytes).use { zip ->
            // Add metadata (with embedded thumbnails) and encrypted files to ZIP
            // ...
        }

        // Encrypt the ZIP payload
        val encryptedPayload = crypto.encrypt(zipBytes.toByteArray(), key)

        // Write backup file: magic + wrapped keys + encrypted payload
        java.io.DataOutputStream(outputStream).use { dos ->
            // Magic bytes
            dos.write(MAGIC_V2)  // "VKV2"

            // Number of wrapped key slots
            dos.writeInt(wrappedKeys.size)

            // Each wrapped key blob (size-prefixed)
            for (wrappedKey in wrappedKeys) {
                dos.writeInt(wrappedKey.size)
                dos.write(wrappedKey)
            }

            // Encrypted payload
            dos.write(encryptedPayload)
        }

        Result.success(Unit)
    } catch (e: Exception) {
        Result.failure(e)
    }
}

Restore Implementation

When restoring, the app tries each wrapped key with the scanned chip:

// From KeySlotManager.kt
fun tryUnwrapFromBackup(chipId: String, wrappedKeys: List<ByteArray>): SecretKey? {
    val chipKey = crypto.deriveKeyFromChipId(chipId)

    for (wrappedKey in wrappedKeys) {
        try {
            return crypto.unwrapKey(wrappedKey, chipKey)
        } catch (e: Exception) {
            // This blob wasn't wrapped with this chip's key, try next
            continue
        }
    }
    return null  // No matching wrapped key found
}


Backup Security Analysis

What an Attacker Learns from a Backup File

If an attacker obtains a .vkv backup file, they can determine:

Information Exposed? Notes
Number of registered chips Yes Count of wrapped key blobs in header
Which chips are registered No No chip identifiers stored
File names No Encrypted in payload
File contents No Encrypted in payload
Thumbnails No Encrypted in payload (base64 in metadata)
File metadata No Encrypted in payload

Why Wrapped Keys Are Safe

Each wrapped key blob is:

[12-byte random IV] + [AES-256-GCM encrypted master key]

To unwrap a key, an attacker would need to:

  1. Obtain the chip ID (requires physical NFC authentication with the chip)
  2. Derive the chip key via SHA-256
  3. Decrypt the wrapped key using AES-256-GCM

Without the physical chip, the wrapped key is computationally infeasible to decrypt.

The AES-256 key space is 2^256, and there are no known practical attacks against AES-256-GCM that would allow recovery of the key or plaintext.

No Chip Identifier Correlation

Wrapped key blobs do not contain any chip-identifying information:

  • Different backups from the same vault will have different wrapped key blobs (different IVs)
  • An attacker cannot determine if two backups belong to the same user
  • An attacker cannot determine which specific VivoKey chips are registered


Threat Model

Protected Against

Threat Protection
Device theft (locked vault) Master key not in memory; requires chip to unlock
Device theft (unlocked vault) Master key cleared on lock; activity-based auto-lock timer
Backup file theft Payload encrypted; wrapped keys useless without chip
Cloud storage breach Backup fully encrypted before upload
File system access All vault files encrypted with AES-256-GCM
Tampering with encrypted files GCM authentication tag detects modifications
Brute force on wrapped keys 256-bit key space; computationally infeasible

Not Protected Against

Threat Notes
Physical chip theft + device access Attacker with chip can unlock vault (mitigated by FIDO2 PIN)
Memory forensics on unlocked device Master key in memory while unlocked
Compromised device OS Keyloggers, screen capture, etc.
Side-channel attacks Not specifically hardened against timing attacks
Quantum computing AES-256 has 128-bit post-quantum security (Grover's algorithm)

FIDO2 PIN as Additional Protection

For FIDO2-enabled chips (Apex), enabling a PIN provides additional security against chip theft:

  • Attacker must know the PIN in addition to having the physical chip
  • PIN retry limits prevent brute force attacks
  • After too many failed attempts, the PIN is blocked (requires chip reset)

This converts single-factor (possession) to two-factor (possession + knowledge) authentication.

Recommendations

  1. Register multiple chips - Provides backup access if one chip is lost
  2. Use auto-lock timer - Limits exposure window if device is stolen while unlocked
  3. Store backups securely - While encrypted, backups should still be protected
  4. Keep devices updated - OS security patches protect the runtime environment


Summary

VivoKey Vault implements defense-in-depth security:

  1. AES-256-GCM provides authenticated encryption for all data
  2. Chip-derived keys ensure only physical chip holders can access the vault
  3. Key wrapping enables multi-chip support without exposing the master key
  4. Encrypted backups with wrapped keys allow secure restoration on new devices
  5. No chip identifiers in backups prevents correlation attacks
  6. Activity-based auto-lock minimizes exposure window with smart inactivity detection
  7. Chunked encryption with AAD binding enables seekable media playback while preventing chunk manipulation
  8. On-demand chunk decryption via ProxyFileDescriptor allows random-access file reading without disk exposure
  9. FIDO2 offline authentication enables network-free vault access using hmac-secret extension
  10. Non-resident keys keep FIDO2 tokens stateless while app manages credential storage
  11. hmac-secret extension provides deterministic key derivation from hardware-protected secrets
  12. Required PIN protection for FIDO2 ensures two-factor authentication (possession + knowledge)

The security of the vault ultimately depends on the security of the VivoKey chip authentication mechanism and the physical security of registered chips.

Privacy Policy

VivoKey Vault

Last Updated: January 2025



Overview

VivoKey Vault ("the App") is a secure file storage application developed by VivoKey Technologies ("we", "us", or "our"). We are committed to protecting your privacy. This Privacy Policy explains what information we collect, how we use it, and your rights regarding your data.

The short version: Your files stay on your device. We cannot access them. With FIDO2-enabled chips (Apex), authentication happens entirely offline. For online-verified chips (Spark), we collect minimal data necessary to verify your chip. We do not sell or share your personal information.



Information We Collect

Information Stored Locally (On Your Device Only)

The following data is stored exclusively on your device and is never transmitted to us or any third party:

  • Encrypted Files - All files you add to the vault are encrypted with AES-256-GCM and stored only on your device
  • Vault Metadata - File names, sizes, timestamps, and thumbnails (all encrypted)
  • Encryption Keys - Wrapped master keys stored locally, protected by your VivoKey chip
  • App Preferences - Settings such as auto-lock timeout

We cannot access this data. It is encrypted with keys derived from your VivoKey chip. Without physical access to your registered chip(s), this data is cryptographically inaccessible.

Information Transmitted to VivoKey Services (Online Authentication)

When you authenticate with a Spark or Spark 2 chip using online authentication, the following occurs:

  • Chip Authentication - Your chip communicates with VivoKey's verification API to confirm authenticity
  • API Credentials - Your VivoKey Developer ID and API Key are used to authorize verification requests
  • Chip Identifier - A cryptographic identifier from your chip is verified by VivoKey servers

This authentication verifies that you are using a genuine VivoKey chip. The chip identifier is used solely for authentication and is not stored by the App after the session ends.

For details on how VivoKey handles authentication data, please see the VivoKey Privacy Policy.

FIDO2 Offline Authentication (No Data Transmitted)

When you authenticate with an Apex chip using FIDO2 offline mode:

  • No network communication - All authentication happens locally between your phone and chip via NFC
  • No API credentials required - No Developer ID or API Key needed
  • No data sent to any server - The entire authentication process is offline

FIDO2 authentication uses the CTAP2 protocol to verify your chip locally. The credential ID from the chip's resident key is used to derive encryption keys, but this identifier never leaves your device.



Information We Do NOT Collect

  • We do not collect your files or their contents
  • We do not collect analytics or usage data
  • We do not collect device identifiers or advertising IDs
  • We do not collect location data
  • We do not collect contacts, call logs, or messages
  • We do not use third-party analytics, crash reporting, or advertising SDKs



How We Use Information

Local Data

All locally stored data is used solely to provide the App's functionality:

  • Encrypting and decrypting your files
  • Displaying file listings and thumbnails
  • Managing registered chips
  • Creating and restoring backups

Authentication Data

Chip authentication data is used solely to:

  • Verify your VivoKey chip is genuine
  • Derive encryption keys for vault access



Data Storage and Security

Encryption

  • All files are encrypted using AES-256-GCM (authenticated encryption)
  • Encryption keys are derived from your VivoKey chip's cryptographic identity
  • Keys are wrapped and stored locally; they cannot be accessed without your chip

Local Storage

  • All vault data is stored in the App's private storage directory
  • Data is protected by Android's application sandboxing
  • Data is additionally protected by file-level encryption

Backups

  • Backup files (.vkv) are fully encrypted before export
  • Backups contain wrapped keys that only work with your registered chip(s)
  • You control where backups are stored (local storage, cloud, etc.)
  • We have no access to your backups



Data Sharing

We do not sell, trade, or share your personal information with third parties.

The only external communication is:

  • VivoKey Authentication API - Used to verify chip authenticity during unlock

No file contents, metadata, or personal information is transmitted to any server.



Data Retention

Local Data

Data remains on your device until you:

  • Delete individual files from the vault
  • Delete the vault entirely
  • Uninstall the App

Authentication Data

Chip authentication data (session tokens) are held in memory only during active use and cleared when the vault locks.



Your Rights

You have complete control over your data:

  • Access - All your data is stored locally on your device
  • Export - You can export your files or create encrypted backups at any time
  • Deletion - You can delete individual files or your entire vault
  • Portability - Encrypted backups can be restored on any device with your registered chip



Children's Privacy

VivoKey Vault is not directed at children under 13. We do not knowingly collect information from children under 13. VivoKey implants require a minor surgery and are intended for adults.



Changes to This Policy

We may update this Privacy Policy from time to time. We will notify you of any changes by:

  • Updating the "Last Updated" date at the top of this policy
  • Posting the new policy in the App and on our website

Your continued use of the App after changes constitutes acceptance of the updated policy.



Permissions Explained

VivoKey Vault requests the following Android permissions:

PermissionPurpose
NFCRequired to communicate with your VivoKey chip for authentication
InternetRequired for online chip verification (Spark chips); not used with FIDO2 offline mode (Apex chips)

We do not request permissions for camera, microphone, contacts, location, or other sensitive data.



Contact Us

If you have questions about this Privacy Policy or our privacy practices, please contact us:

VivoKey Technologies



Summary

WhatCollected?Shared?
Your filesNo (stored locally, encrypted)No
File metadataNo (stored locally, encrypted)No
Analytics/usage dataNoNo
Advertising dataNoNo
Chip authentication (online mode)Yes (for verification only)With VivoKey API only
Chip authentication (FIDO2 offline)No (entirely local)No
Device identifiersNoNo
LocationNoNo

Your vault. Your device. Your privacy.