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 Type | Location | Encrypted |
|---|---|---|
| Tab configs | spaces/{spaceId}/tabs/{tabName} | No |
| Homebase | private/{identityKey}/homebase | Yes |
| Homebase tabs | private/{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
- Signature Verification: Always verify signatures before trusting file contents
- Key Management: Private keys must be securely stored and never exposed
- Salt Storage: The encryption salt is stored with the identity, not the file
- Replay Protection: Timestamps help detect stale/replayed files
Related Files
src/common/lib/signedFiles.ts- SignedFile type definitions and utilitiessrc/common/data/stores/app/space/spaceStore.ts- File upload/download logic
Related Documentation
- Private Spaces - Homebase encryption details
- Tab Operations - How tabs are stored
- Authentication - Identity and key management