Open Specification

The .sv Format

Selvum Vault Format — v1.0 · Stable

Implemented in shared/src/commonMain/kotlin/com/selvum/shared/VaultSerializer.kt

Overview

The .sv format is the binary container used by Selvum to store encrypted vaults on disk. It is designed to be:

A .sv file is not a ZIP, not a database, and not a proprietary binary blob with undocumented structure. The entire format fits in this document.

File Structure

Header size 39 bytes
Minimum file size 55 bytes
Max payload 10 MB
Offset Size Field Description
04MagicASCII "SELV"0x53 0x45 0x4C 0x56
42Version0x00 0x01 (big-endian uint16)
61FlagsKDF selector (see Flags section)
716SaltRandom KDF salt (CSPRNG)
2312NonceAES-GCM nonce (CSPRNG, unique per save operation)
354PayloadLengthTotal bytes that follow (ciphertext + 16-byte GCM tag), big-endian uint32
39NPayloadAES-256-GCM ciphertext with 16-byte auth tag appended at the end

Fields

Magic [0..3]

53 45 4C 56

The ASCII string SELV. Any file not starting with this sequence is rejected immediately with a "corrupted file" error. This allows quick identification without relying on file extension.

Version [4..5]

00 01

Big-endian unsigned 16-bit integer. Currently only 0x00 0x01 is accepted. If the app encounters a higher major version it shows a user-friendly prompt: "This vault was created with a newer version of Selvum. Please update the app."

Flags [6]

Single byte indicating which key derivation function was used to produce the master key.

ValueKDFPlatform
0x00Argon2idAndroid
0x01PBKDF2-SHA512iOS
0x02+Reserved

The receiving platform reads this byte before decryption and dispatches to the correct KDF implementation. Both platforms can open vaults created by the other.

Salt [7..22]

16 bytes of cryptographically random data generated at vault creation time using a platform CSPRNG (java.security.SecureRandom on Android, SecRandomCopyBytes on iOS). The salt is stored in the header so the master key can be re-derived at each unlock without storing the key itself.

The same salt is reused across unlock operations for the same vault. A new salt is only generated when a new vault is created or when a vault is re-keyed.

Nonce [23..34]

12 bytes (96 bits) of cryptographically random data, generated fresh by the CSPRNG on every save operation. AES-GCM requires each (key, nonce) pair to be used at most once; regenerating the nonce on every save guarantees this property.

PayloadLength [35..38]

Unsigned 32-bit integer (big-endian) equal to the byte length of the Payload field. This value includes the 16-byte GCM authentication tag that is appended at the end of the ciphertext. Maximum accepted value is 10,485,760 (10 MB); values outside this range are rejected.

Payload [39..39+N]

N bytes of AES-256-GCM ciphertext. The last 16 bytes of this field are the GCM authentication tag. The tag is not stored as a separate field — it is appended directly to the ciphertext, which is the standard output format of the AES/GCM/NoPadding cipher.

The plaintext is the UTF-8 JSON encoding of the Vault object (see Plaintext Structure section).

Encryption Algorithm

Cipher:      AES-256-GCM
Key size:    256 bits (32 bytes)
Nonce size:  96 bits  (12 bytes)
Auth tag:    128 bits (16 bytes), appended to ciphertext
AAD:         none

Authenticated encryption guarantees both confidentiality and integrity. Any modification to the header, ciphertext, or tag causes decryption to fail with a wrong-passphrase error before any plaintext is returned.

Key Derivation

Android — Argon2id Flags = 0x00

The master key is derived using Argon2id as specified in RFC 9106.

Algorithm:   Argon2id
Memory:      65,536 KiB (64 MB)
Iterations:  3
Parallelism: 1
Output:      32 bytes (256-bit master key)
Input:       UTF-8 encoding of the 12 BIP-39 seed words joined by single spaces
Salt:        16-byte CSPRNG value from header field Salt

Argon2id is memory-hard and resistant to GPU/ASIC brute-force attacks. Implementation: BouncyCastle org.bouncycastle:bcprov-jdk18on.

iOS — PBKDF2-SHA512 Flags = 0x01

iOS does not expose Argon2id in its system cryptography libraries. iOS vaults use PBKDF2 with a high iteration count to approximate equivalent resistance.

Algorithm:   PBKDF2-HMAC-SHA512
Iterations:  600,000
Output:      32 bytes (256-bit master key)
Input:       UTF-8 encoding of the 12 BIP-39 seed words joined by single spaces
Salt:        16-byte CSPRNG value from header field Salt

Implementation: CommonCrypto CCKeyDerivationPBKDF with kCCPRFHmacAlgSHA512.

Recovery Kit — PBKDF2-SHA512

RESCATE_OFFLINE.html uses the same PBKDF2-SHA512 parameters as iOS (600,000 iterations via the WebCrypto API), making it compatible with both Android vaults and iOS vaults through a single JS implementation. When opening an Android vault (Flags = 0x00), the recovery tool automatically runs PBKDF2 in its place; this is an intentional trade-off for browser compatibility.

Plaintext Structure

After successful decryption and authentication, the payload is parsed as UTF-8 JSON conforming to the Vault schema:

{
  "entries": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "title": "My Bank",
      "category": "BANKS",
      "fields": [
        { "key": "bankEntity",   "value": "Example Bank", "type": "TEXT" },
        { "key": "bankPassword", "value": "s3cr3t",       "type": "SECRET_TEXT" }
      ],
      "notes": null,
      "createdAt": 1717200000000,
      "updatedAt": 1717200000000,
      "expiresAt": null,
      "strength": 42
    }
  ]
}

Byte-Level Example

The following shows a minimal valid .sv file for illustration. All values are hexadecimal.

Offset  Bytes                                   Description
──────  ──────────────────────────────────────  ─────────────────────────────
00      53 45 4C 56                             Magic "SELV"
04      00 01                                   Version 1.0
06      00                                      Flags: Argon2id
07      a1 b2 c3 d4 e5 f6 07 08                Salt (16 bytes, random)
        09 0a 0b 0c 0d 0e 0f 10
23      11 22 33 44 55 66 77 88                Nonce (12 bytes, random)
        99 aa bb cc
35      00 00 01 2C                             PayloadLength = 300 bytes
39      [300 bytes of AES-256-GCM ciphertext]  Last 16 bytes are the GCM tag

Total file size for this example: 39 + 300 = 339 bytes.

Validation Rules

When reading a .sv file, the following checks are performed in order:

  1. File size ≥ 55 bytes (header + minimum 16-byte tag).
  2. Bytes [0..3] equal 53 45 4C 56.
  3. Bytes [4..5] equal 00 01 (unsupported version → friendly upgrade message).
  4. PayloadLength ≥ 16 and ≤ 10,485,760.
  5. PayloadLength ≤ file_size − 39.
  6. GCM decryption succeeds (tag mismatch → wrong passphrase error).
  7. Decrypted bytes are valid UTF-8 JSON.

A file failing any check except step 3 is reported as corrupted.

Compatibility

PlatformWrites (flags)Reads (flags)
Android0x00 (Argon2id)0x00, 0x01
iOS0x01 (PBKDF2)0x00, 0x01
Recovery Kit— (read only)0x00, 0x01

Cross-platform compatibility is verified by an automated test suite that encrypts a known vault on Android and decrypts it on iOS and vice versa before each release.

Source References

shared/src/commonMain/kotlin/com/selvum/shared/VaultSerializer.kt — serializer/deserializer
shared/src/androidMain/kotlin/com/selvum/shared/AesGcm.android.kt — AES-256-GCM Android
shared/src/iosMain/kotlin/com/selvum/shared/AesGcm.ios.kt — AES-256-GCM iOS (CryptoKit bridge)
shared/src/androidMain/kotlin/com/selvum/shared/Argon2id.android.kt — Argon2id Android
iosApp/iosApp/CryptoHelper.swift — PBKDF2-SHA512 iOS
docs/landing/RESCATE_OFFLINE.html — offline recovery tool (PBKDF2-SHA512 via WebCrypto)