Skip to main content

SignedFile Format

All files stored in Blankspace's Supabase Storage buckets use the SignedFile wrapper format. This provides integrity verification, optional encryption, and metadata tracking.

Overview

The SignedFile format wraps file contents with:

  • Signature - Ed25519 signature for integrity verification
  • Public Key - Identity of the file creator
  • Encryption Flag - Whether the content is encrypted
  • Timestamp - When the file was created/modified
  • File Type - Content type (usually "json")

Format Structure

interface SignedFile {
publicKey: string; // Ed25519 public key of the signer
fileData: string; // The actual content (may be encrypted)
fileType: string; // Content type, typically "json"
isEncrypted: boolean; // Whether fileData is encrypted
timestamp: string; // ISO 8601 timestamp
signature: string; // Ed25519 signature of the content hash
}

Example: Unencrypted File

Public space tab configurations are stored unencrypted:

{
"publicKey": "abc123def456...",
"fileData": "{\"fidgetInstanceDatums\":{},\"layoutID\":\"grid\",\"layoutDetails\":{...},\"theme\":{...}}",
"fileType": "json",
"isEncrypted": false,
"timestamp": "2024-01-15T10:30:00.000Z",
"signature": "xyz789..."
}

Example: Encrypted File

Homebase (private space) files are encrypted:

{
"publicKey": "abc123def456...",
"fileData": "0a1b2c3d4e5f...",
"fileType": "json",
"isEncrypted": true,
"timestamp": "2024-01-15T10:30:00.000Z",
"signature": "xyz789..."
}

When isEncrypted: true, the fileData field contains hex-encoded XChaCha20-Poly1305 ciphertext.

Creating a SignedFile

For Public Files (Unencrypted)

import stringify from 'fast-json-stable-stringify';
import moment from 'moment';

function createSignedFile(
data: any,
publicKey: string,
privateKey: string
): SignedFile {
const fileData = stringify(data);

// Hash the content
const contentHash = blake3(fileData);

// Sign the hash
const signature = ed25519.sign(contentHash, privateKey);

return {
publicKey,
fileData,
fileType: 'json',
isEncrypted: false,
timestamp: moment().toISOString(),
signature: bytesToHex(signature),
};
}

For Private Files (Encrypted)

function createEncryptedSignedFile(
data: any,
publicKey: string,
privateKey: string,
salt: string
): SignedFile {
const plaintext = stringify(data);

// Derive encryption key from private key + salt
const encryptionKey = hkdf(sha256, privateKey, salt, '', 32);

// Encrypt with XChaCha20-Poly1305
const nonce = randomBytes(24);
const ciphertext = xchacha20poly1305(encryptionKey, nonce).encrypt(plaintext);

// Combine nonce + ciphertext
const encrypted = concat(nonce, ciphertext);
const fileData = bytesToHex(encrypted);

// Hash and sign the encrypted content
const contentHash = blake3(fileData);
const signature = ed25519.sign(contentHash, privateKey);

return {
publicKey,
fileData,
fileType: 'json',
isEncrypted: true,
timestamp: moment().toISOString(),
signature: bytesToHex(signature),
};
}

Reading a SignedFile

Verifying Signature

function verifySignedFile(file: SignedFile): boolean {
const contentHash = blake3(file.fileData);
return ed25519.verify(
hexToBytes(file.signature),
contentHash,
hexToBytes(file.publicKey)
);
}

Decrypting (if encrypted)

function decryptSignedFile(
file: SignedFile,
privateKey: string,
salt: string
): any {
if (!file.isEncrypted) {
return JSON.parse(file.fileData);
}

const encrypted = hexToBytes(file.fileData);

// Split nonce and ciphertext
const nonce = encrypted.slice(0, 24);
const ciphertext = encrypted.slice(24);

// Derive decryption key
const decryptionKey = hkdf(sha256, privateKey, salt, '', 32);

// Decrypt
const plaintext = xchacha20poly1305(decryptionKey, nonce).decrypt(ciphertext);

return JSON.parse(new TextDecoder().decode(plaintext));
}

System-Generated Files

Some files are generated by the system (seed scripts, migrations) rather than users. These use a special signature:

function createSystemSignedFile(fileData: string): SignedFile {
return {
fileData,
fileType: 'json',
isEncrypted: false,
timestamp: moment().toISOString(),
publicKey: 'nounspace',
signature: 'not applicable, machine generated file',
};
}

Files That Use This Format

File TypeLocationEncrypted
Tab configsspaces/{spaceId}/tabs/{tabName}No
Homebaseprivate/{identityKey}/homebaseYes
Homebase tabsprivate/{identityKey}/tabs/{tabName}Yes

Files That Don't Use This Format

The tabOrder file is NOT wrapped in SignedFile format:

{
"spaceId": "uuid-here",
"tabOrder": ["Home", "Gallery", "Links"],
"timestamp": "2024-01-15T10:30:00.000Z"
}

Cryptographic Libraries

Blankspace uses the @noble family of cryptographic libraries:

  • @noble/curves - Ed25519 signatures
  • @noble/ciphers - XChaCha20-Poly1305 encryption
  • @noble/hashes - BLAKE3 hashing, HKDF key derivation

Security Considerations

  1. Signature Verification: Always verify signatures before trusting file contents
  2. Key Management: Private keys must be securely stored and never exposed
  3. Salt Storage: The encryption salt is stored with the identity, not the file
  4. Replay Protection: Timestamps help detect stale/replayed files
  • src/common/lib/signedFiles.ts - SignedFile type definitions and utilities
  • src/common/data/stores/app/space/spaceStore.ts - File upload/download logic