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