Skip to content

Latest commit

 

History

History
223 lines (158 loc) · 8.54 KB

File metadata and controls

223 lines (158 loc) · 8.54 KB

Video Encryption in SnapSafe

This document describes the SECV (Secure Encrypted Camera Video) file format and the philosophy behind video capture and encryption in SnapSafe.

The Challenge

Video encryption presents unique challenges compared to photo encryption:

  1. Memory constraints: Videos can be gigabytes in size - loading an entire video into memory for encryption is not feasible on mobile devices.

  2. CameraX limitations: Android's CameraX Recording API writes directly to a file. There's no byte stream access during recording, so we cannot encrypt on-the-fly during capture.

  3. Playback requirements: Videos need random access for seeking. Traditional stream ciphers don't support this well.

  4. Authentication: AES-GCM computes its authentication tag over the entire message. A single-shot approach would require reading the entire file twice (encrypt, then authenticate).

Our Approach: Chunked Post-Recording Encryption

We use a chunked encryption strategy:

  1. During recording: CameraX writes to a temporary unencrypted .mp4 file in app-private storage (protected by Android's File-Based Encryption).

  2. After recording: We stream-encrypt the temp file in 1MB chunks, each with its own IV and authentication tag.

  3. Cleanup: The temp file is deleted after encryption completes.

  4. Playback: ExoPlayer uses a custom DataSource that decrypts chunks on-the-fly, enabling seeking without decrypting the entire file.

Why Post-Recording?

We considered several alternatives:

  • Real-time encryption: Not possible with CameraX's file-based recording API.
  • Custom camera implementation: Would sacrifice quality, stability, and features that CameraX provides.

Post-recording encryption is a pragmatic trade-off. The temp file exists unencrypted briefly, but only in app-private storage which is already protected by Android FBE when the device is locked. This should be sufficent as the window of vulnerability is brief. (Only during recording, the video is encrypted immedately afterward.)

SECV File Format (Version 1)

+--------------------------------------------------+
|                 HEADER (64 bytes)                |
+--------------------------------------------------+
| Magic: "SECV"              | 4 bytes             |
| Version: 1                 | 2 bytes (uint16)    |
| Chunk Size                 | 4 bytes (uint32)    |
| Total Chunks               | 8 bytes (uint64)    |
| Original Size              | 8 bytes (uint64)    |
| Final Chunk Size           | 4 bytes (uint32)    |
| Reserved                   | 34 bytes            |
+--------------------------------------------------+
|              ENCRYPTED CHUNKS                    |
+--------------------------------------------------+
| Chunk 0: [IV 12B][Ciphertext][Auth Tag 16B]      |
| Chunk 1: [IV 12B][Ciphertext][Auth Tag 16B]      |
| ...                                              |
| Chunk N-1 (final): [IV 12B][Ciphertext][Tag]     |
+--------------------------------------------------+

Chunk Offset Calculation

Since AES-GCM preserves plaintext size (no padding), all full chunks are identical size:

Full chunk encrypted size = chunk_size + 28 bytes (12 IV + 16 auth tag)
Chunk offset = 64 + (chunk_index × (chunk_size + 28))

Only the final chunk may be smaller (if video size isn't a multiple of chunk_size):

Final chunk encrypted size = final_chunk_plaintext_size + 28 bytes

Design Rationale

Header pre-allocation: The encryptor writes 64 zero bytes at the start, then writes chunks. On close, it seeks back to position 0 and fills in the header with final metadata (total chunks, original size, final chunk size). This approach allows all writing to happen in-place, no shifting once the chunks are written.

Fixed header size (64 bytes): Allows quick validation and metadata extraction without parsing variable-length structures.

Final chunk plaintext size: Since the last chunk may be smaller, we store its plaintext size in the header. This allows the decryptor to read the correct number of bytes for the final chunk.

O(1) seeking: To play from position X, we calculate which chunk contains X using chunk_index = X / chunk_size, calculate its offset using 64 + (chunk_index × (chunk_size + 28)), and decrypt just that chunk.

Per-chunk IV: Each chunk gets a fresh 12-byte IV from SecureRandom. This prevents nonce reuse even across millions of chunks.

Per-chunk authentication: Tampering is detected at the chunk level. A corrupted chunk fails authentication without invalidating the entire file.

1MB default chunk size: Balances memory usage against seek granularity. Smaller chunks mean finer seeking but more overhead; larger chunks reduce overhead but increase memory pressure during decryption.

Encryption Details

  • Algorithm: AES-256-GCM
  • Key: Derived from user PIN using PBKDF2 (same as photo encryption)
  • IV: 12 bytes, randomly generated per chunk
  • Authentication tag: 16 bytes (128-bit), appended to ciphertext

Each chunk is independently encrypted:

ciphertext = AES-GCM-Encrypt(key, iv, plaintext_chunk)
stored = iv || ciphertext || auth_tag

Seeking to Position X

To decrypt data at plaintext position X (byte offset):

// 1. Calculate which chunk contains position X
chunk_index = X / chunk_size
offset_in_chunk = X % chunk_size

// 2. Calculate file offset for that chunk
file_offset = 64 + (chunk_index × (chunk_size+28))

// 3. Determine encrypted size
if (chunk_index == total_chunks - 1) {
   // Final chunk
   encrypted_size = final_chunk_plaintext_size + 28
} else {
   // Full chunk
   encrypted_size = chunk_size + 28
}

// 4. Read and decrypt chunk at file_offset
// 5. Extract bytes starting at offset_in_chunk

Playback Architecture

sequenceDiagram
    participant EP as ExoPlayer
    participant DS as EncryptedVideoDataSource
    participant SD as StreamingDecryptor
    participant F as SECV File

    EP->>DS: open(dataSpec)
    DS->>SD: get totalSize
    SD-->>DS: file size

    loop Playback / Seeking
        EP->>DS: read(buffer, offset, length)
        DS->>SD: read(position, buffer, length)
        SD->>SD: calculate chunk index
        SD->>F: read encrypted chunk
        F-->>SD: [IV][ciphertext][tag]
        SD->>SD: AES-GCM decrypt
        SD-->>DS: decrypted bytes
        DS-->>EP: bytes read
    end

    EP->>DS: close()
Loading

The StreamingDecryptor maintains a single-chunk cache, so sequential reads (normal playback) don't re-decrypt the same chunk repeatedly.

Security Considerations

Temporary File Exposure

The unencrypted temp file exists during recording and until encryption completes. After deletion, the data may persist on flash storage due to wear leveling until the SSD controller garbage collects those blocks.

Why we don't attempt "secure deletion": On flash storage (SSDs, eMMC, UFS), overwriting a file with zeros doesn't overwrite the original physical cells. The storage controller maps the write to new blocks and marks the old ones for eventual garbage collection. "Secure deletion" via overwriting is ineffective on modern storage.

Actual mitigations:

  1. App-private storage: Only accessible to SnapSafe (unless device is rooted or compromised).
  2. Android FBE (File-Based Encryption): On devices with FBE (Android 7+), app storage is encrypted with a key derived from the user's lock screen credential. When the device is locked, app files are cryptographically inaccessible - including any remnant temp file blocks.
  3. Minimal window: Encryption starts immediately after recording stops.

Chunk-Level Tampering

An attacker with file access could:

  • Reorder chunks (detected: playback produces garbage)
  • Truncate file (detected: missing chunks)
  • Modify chunk data (detected: GCM authentication failure)

They cannot:

  • Read video content without the key
  • Forge valid chunks without the key
  • Silently corrupt data (authentication prevents this)

Key Management

Video encryption uses the same derived key as photo encryption, managed by ShardedKey for in-memory protection. The key is never written to disk in plaintext.

File Extension

Encrypted videos use the .secv extension (Secure Encrypted Camera Video). The gallery recognizes only .secv files as ready-to-play videos. It detects unencrypted mp4, and partially encrypted secv.encrypting files in order to show encryption status.

Future Considerations

  • Streaming during recording: If CameraX adds byte-stream access, we could encrypt during capture and close the only security hole that currently exists.