Skip to content

Integration Guide: File Encryption

This guide demonstrates how to build a complete file encryption system using the Grizzly SDK. You'll learn how to encrypt files of any size, manage encrypted storage, and implement secure file sharing workflows.

Overview

File encryption with Grizzly provides:

  • Stream-based encryption - Handle files of any size without loading into memory
  • Metadata preservation - Embed file information (name, type, owner) as Assets
  • Secure storage - Files remain encrypted at rest
  • Audit trail - All operations tracked in Activity feed
  • Key rotation - Seamless key updates without re-encrypting files

Asset Naming for Dashboard Display

Understanding the Asset "name" Property

When encrypting files, you can include metadata as an Asset. The Asset's name property has special significance: it determines how the asset appears in the Grizzly Dashboard.

Key Points:

  • The name property is displayed in the Dashboard UI for easy identification
  • Without a name property, assets appear as hash:<asset-content-hash> in the Dashboard
  • The hash-based display makes it difficult to identify specific files without additional context
  • Choose meaningful names that help you identify files at a glance
  • The name can be different from the original filename

Example:

// With "name" property - shows as "Q4 Financial Report" in Dashboard
const encrypted = await ring.encrypt(fileStream, {
   asset: {
      id: 'file-12345',
      name: 'Q4 Financial Report',  // ← Displayed in Dashboard
      type: 'file',
      filename: 'q4-2024-report.pdf',
      owner: 'finance-team'
   }
})

// Without "name" property - shows as "hash:a3f5e8..." in Dashboard
const encrypted = await ring.encrypt(fileStream, {
   asset: {
      id: 'file-12345',
      type: 'file',
      filename: 'q4-2024-report.pdf'
      // ← Dashboard displays: hash:<computed-hash-of-asset-metadata>
   }
})

Why This Matters:

The hash-based display is a cryptographic hash of the asset's metadata, which ensures integrity but makes manual identification difficult. Always include a name property for better usability in the Dashboard.

Best Practices:

  • Always include a name property for files you'll need to identify in the Dashboard
  • Use human-readable names for better Dashboard usability
  • Include context in the name (e.g., "Legal Contract - Acme Corp" instead of just "contract.pdf")
  • Keep names concise but descriptive
  • Consider including dates or versions when relevant (e.g., "Q4 2024 Financial Report")

Basic File Encryption

Encrypt and Decrypt Single File
import { KeyClient } from '@grizzlycbg/grizzly-sdk'
import fs from 'fs'
import path from 'path'

const client = await KeyClient.create({
   host: 'https://your-tenant.grizzlycbg.io',
   apikey: process.env.GRIZZLY_API_KEY
})

async function encryptFile(inputPath, outputPath, keyringName) {
   const ring = await client.ring.get(keyringName)

   if (!ring) {
      throw new Error(`KeyRing '${keyringName}' not found`)
   }

   // Create read stream from source file
   const readStream = fs.createReadStream(inputPath)

   // Encrypt the stream
   const encryptedStream = await ring.encrypt(readStream, {
      asset: {
         id: `file-${Date.now()}`,
         name: path.basename(inputPath),  // Display name in Dashboard
         type: 'file',
         filename: path.basename(inputPath),
         originalSize: fs.statSync(inputPath).size,
         encryptedAt: new Date().toISOString()
      }
   })

   // Write encrypted stream to output file
   return new Promise((resolve, reject) => {
      const writeStream = fs.createWriteStream(outputPath)

      encryptedStream.pipe(writeStream)

      writeStream.on('finish', () => {
         console.log(`File encrypted: ${outputPath}`)
         resolve({
            inputPath,
            outputPath,
            size: fs.statSync(outputPath).size
         })
      })

      writeStream.on('error', (error) => {
         console.error('Encryption failed:', error)
         reject(error)
      })

      encryptedStream.on('error', (error) => {
         console.error('Stream error:', error)
         reject(error)
      })
   })
}

async function decryptFile(inputPath, outputPath) {
   // Read encrypted file as stream
   const encryptedStream = fs.createReadStream(inputPath)

   // Decrypt (KeyRing automatically determined from header)
   const decryptedStream = await client.decrypt(encryptedStream)

   return new Promise((resolve, reject) => {
      const writeStream = fs.createWriteStream(outputPath)

      decryptedStream.pipe(writeStream)

      writeStream.on('finish', () => {
         console.log(`File decrypted: ${outputPath}`)
         resolve({
            inputPath,
            outputPath,
            size: fs.statSync(outputPath).size
         })
      })

      writeStream.on('error', reject)
      decryptedStream.on('error', reject)
   })
}

// Usage
await encryptFile('./document.pdf', './document.enc', 'Documents')
await decryptFile('./document.enc', './document-restored.pdf')

Production File Storage Service

Complete File Management System
import { KeyClient } from '@grizzlycbg/grizzly-sdk'
import fs from 'fs/promises'
import fsSync from 'fs'
import path from 'path'
import crypto from 'crypto'

class SecureFileService {
   constructor(client, options = {}) {
      this.client = client
      this.storageDir = options.storageDir || './encrypted-storage'
      this.keyringName = options.keyringName || 'FileStorage'
      this.maxFileSize = options.maxFileSize || 100 * 1024 * 1024 // 100MB default
   }

   async initialize() {
      // Ensure storage directory exists
      await fs.mkdir(this.storageDir, { recursive: true })

      // Ensure KeyRing exists
      let ring = await this.client.ring.get(this.keyringName)

      if (!ring) {
         ring = await this.client.ring.create(this.keyringName, {
            config: {
               props: {
                  service: 'file-storage',
                  version: '1.0'
               },
               algos: {
                  aes256: {
                     maxEncryptCount: 50000
                  }
               }
            }
         })
         console.log(`Created KeyRing: ${this.keyringName}`)
      }

      return this
   }

   async upload(filePath, metadata = {}) {
      // Validate file exists
      const stats = await fs.stat(filePath)

      if (stats.size > this.maxFileSize) {
         throw new Error(`File exceeds maximum size: ${this.maxFileSize} bytes`)
      }

      // Generate unique file ID
      const fileId = crypto.randomUUID()
      const filename = path.basename(filePath)
      const extension = path.extname(filename)

      // Build Asset metadata
      const asset = {
         id: fileId,
         name: filename,  // Display name in Dashboard
         type: 'file',
         filename,
         extension,
         mimeType: this.getMimeType(extension),
         size: stats.size,
         uploadedAt: new Date().toISOString(),
         ...metadata
      }

      // Get KeyRing
      const ring = await this.client.ring.get(this.keyringName)

      // Encrypt file
      const readStream = fsSync.createReadStream(filePath)
      const encryptedStream = await ring.encrypt(readStream, { asset })

      // Store encrypted file
      const storagePath = path.join(this.storageDir, `${fileId}.enc`)

      await new Promise((resolve, reject) => {
         const writeStream = fsSync.createWriteStream(storagePath)

         encryptedStream.pipe(writeStream)

         writeStream.on('finish', resolve)
         writeStream.on('error', reject)
         encryptedStream.on('error', reject)
      })

      // Store metadata separately for quick lookup
      const metadataPath = path.join(this.storageDir, `${fileId}.meta.json`)
      await fs.writeFile(metadataPath, JSON.stringify({
         fileId,
         filename,
         originalSize: stats.size,
         encryptedSize: (await fs.stat(storagePath)).size,
         uploadedAt: asset.uploadedAt,
         metadata
      }, null, 2))

      console.log(`File uploaded: ${fileId}`)

      return {
         fileId,
         filename,
         size: stats.size,
         storagePath
      }
   }

   async download(fileId, outputPath) {
      // Check file exists
      const storagePath = path.join(this.storageDir, `${fileId}.enc`)

      try {
         await fs.access(storagePath)
      } catch (error) {
         throw new Error(`File not found: ${fileId}`)
      }

      // Decrypt file
      const encryptedStream = fsSync.createReadStream(storagePath)
      const decryptedStream = await this.client.decrypt(encryptedStream)

      await new Promise((resolve, reject) => {
         const writeStream = fsSync.createWriteStream(outputPath)

         decryptedStream.pipe(writeStream)

         writeStream.on('finish', resolve)
         writeStream.on('error', reject)
         decryptedStream.on('error', reject)
      })

      console.log(`File downloaded: ${outputPath}`)

      return {
         fileId,
         outputPath,
         size: (await fs.stat(outputPath)).size
      }
   }

   async getMetadata(fileId) {
      const metadataPath = path.join(this.storageDir, `${fileId}.meta.json`)

      try {
         const content = await fs.readFile(metadataPath, 'utf-8')
         return JSON.parse(content)
      } catch (error) {
         throw new Error(`Metadata not found: ${fileId}`)
      }
   }

   async delete(fileId) {
      const storagePath = path.join(this.storageDir, `${fileId}.enc`)
      const metadataPath = path.join(this.storageDir, `${fileId}.meta.json`)

      await Promise.all([
         fs.unlink(storagePath).catch(() => {}),
         fs.unlink(metadataPath).catch(() => {})
      ])

      console.log(`File deleted: ${fileId}`)
   }

   async list() {
      const files = await fs.readdir(this.storageDir)

      const metadataFiles = files.filter(f => f.endsWith('.meta.json'))

      const fileList = await Promise.all(
         metadataFiles.map(async (file) => {
            const content = await fs.readFile(
               path.join(this.storageDir, file),
               'utf-8'
            )
            return JSON.parse(content)
         })
      )

      return fileList
   }

   getMimeType(extension) {
      const mimeTypes = {
         '.pdf': 'application/pdf',
         '.doc': 'application/msword',
         '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
         '.xls': 'application/vnd.ms-excel',
         '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
         '.jpg': 'image/jpeg',
         '.jpeg': 'image/jpeg',
         '.png': 'image/png',
         '.gif': 'image/gif',
         '.txt': 'text/plain',
         '.csv': 'text/csv',
         '.json': 'application/json',
         '.zip': 'application/zip'
      }

      return mimeTypes[extension.toLowerCase()] || 'application/octet-stream'
   }
}

// Usage
const client = await KeyClient.create({
   host: 'https://your-tenant.grizzlycbg.io',
   apikey: process.env.GRIZZLY_API_KEY
})

const fileService = new SecureFileService(client, {
   storageDir: './secure-files',
   keyringName: 'FileStorage',
   maxFileSize: 100 * 1024 * 1024 // 100MB
})

await fileService.initialize()

// Upload file with custom metadata
const result = await fileService.upload('./contract.pdf', {
   owner: 'user-123',
   department: 'legal',
   classification: 'confidential'
})

console.log('Uploaded:', result.fileId)

// List all files
const files = await fileService.list()
console.log('Files:', files)

// Download file
await fileService.download(result.fileId, './contract-restored.pdf')

// Get metadata
const metadata = await fileService.getMetadata(result.fileId)
console.log('Metadata:', metadata)

// Delete file
await fileService.delete(result.fileId)

Secure File Sharing

Multi-User File Access Control
import { KeyClient } from '@grizzlycbg/grizzly-sdk'
import crypto from 'crypto'

class SecureFileSharing {
   constructor(client, storageService) {
      this.client = client
      this.storage = storageService
      this.shares = new Map() // In production, use database
   }

   async shareFile(fileId, sharedWith, permissions = {}) {
      // Verify file exists
      const metadata = await this.storage.getMetadata(fileId)

      // Create share record
      const shareId = crypto.randomUUID()
      const share = {
         shareId,
         fileId,
         filename: metadata.filename,
         sharedWith,
         sharedBy: permissions.sharedBy,
         sharedAt: new Date().toISOString(),
         expiresAt: permissions.expiresAt || null,
         canDownload: permissions.canDownload !== false,
         downloadLimit: permissions.downloadLimit || null,
         downloadCount: 0
      }

      this.shares.set(shareId, share)

      console.log(`File shared: ${shareId}`)

      return share
   }

   async downloadShared(shareId, outputPath) {
      const share = this.shares.get(shareId)

      if (!share) {
         throw new Error('Share not found or expired')
      }

      // Check expiration
      if (share.expiresAt && new Date(share.expiresAt) < new Date()) {
         throw new Error('Share has expired')
      }

      // Check download permissions
      if (!share.canDownload) {
         throw new Error('Download not permitted')
      }

      // Check download limit
      if (share.downloadLimit && share.downloadCount >= share.downloadLimit) {
         throw new Error('Download limit exceeded')
      }

      // Download file
      await this.storage.download(share.fileId, outputPath)

      // Increment download count
      share.downloadCount++

      console.log(`Shared file downloaded: ${shareId} (${share.downloadCount} downloads)`)

      return {
         shareId,
         fileId: share.fileId,
         filename: share.filename,
         outputPath
      }
   }

   async revokeShare(shareId) {
      const share = this.shares.get(shareId)

      if (!share) {
         throw new Error('Share not found')
      }

      this.shares.delete(shareId)

      console.log(`Share revoked: ${shareId}`)
   }

   async listShares(fileId) {
      const shares = Array.from(this.shares.values())
         .filter(s => !fileId || s.fileId === fileId)

      return shares
   }
}

// Usage
const sharing = new SecureFileSharing(client, fileService)

// Share file with time limit
const share = await sharing.shareFile('file-123', 'user-456', {
   sharedBy: 'user-789',
   expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
   canDownload: true,
   downloadLimit: 5
})

console.log('Share URL:', `https://app.example.com/shared/${share.shareId}`)

// Download shared file
await sharing.downloadShared(share.shareId, './downloaded-file.pdf')

// Revoke access
await sharing.revokeShare(share.shareId)

Batch File Operations

Process Multiple Files Efficiently
async function batchEncryptFiles(inputDir, outputDir, keyringName, options = {}) {
   const { concurrency = 5 } = options

   const files = await fs.readdir(inputDir)
   const ring = await client.ring.get(keyringName)

   if (!ring) {
      throw new Error(`KeyRing '${keyringName}' not found`)
   }

   // Ensure output directory exists
   await fs.mkdir(outputDir, { recursive: true })

   const results = []

   // Process files in batches
   for (let i = 0; i < files.length; i += concurrency) {
      const batch = files.slice(i, i + concurrency)

      const batchResults = await Promise.allSettled(
         batch.map(async (filename) => {
            const inputPath = path.join(inputDir, filename)
            const outputPath = path.join(outputDir, `${filename}.enc`)

            const stats = await fs.stat(inputPath)

            if (!stats.isFile()) {
               return { status: 'skipped', filename, reason: 'not a file' }
            }

            try {
               const readStream = fsSync.createReadStream(inputPath)
               const encryptedStream = await ring.encrypt(readStream, {
                  asset: {
                     id: `batch-${Date.now()}-${filename}`,
                     name: filename,  // Display name in Dashboard
                     type: 'file',
                     filename,
                     batchOperation: true,
                     encryptedAt: new Date().toISOString()
                  }
               })

               await new Promise((resolve, reject) => {
                  const writeStream = fsSync.createWriteStream(outputPath)

                  encryptedStream.pipe(writeStream)

                  writeStream.on('finish', resolve)
                  writeStream.on('error', reject)
                  encryptedStream.on('error', reject)
               })

               return {
                  status: 'success',
                  filename,
                  inputPath,
                  outputPath,
                  size: stats.size
               }

            } catch (error) {
               return {
                  status: 'failed',
                  filename,
                  error: error.message
               }
            }
         })
      )

      results.push(...batchResults.map(r => r.value || r.reason))

      console.log(`Processed batch ${Math.floor(i / concurrency) + 1}/${Math.ceil(files.length / concurrency)}`)
   }

   // Summary
   const summary = {
      total: results.length,
      success: results.filter(r => r.status === 'success').length,
      failed: results.filter(r => r.status === 'failed').length,
      skipped: results.filter(r => r.status === 'skipped').length
   }

   console.log('Batch encryption complete:', summary)

   return { results, summary }
}

// Usage
const result = await batchEncryptFiles(
   './documents',
   './encrypted-documents',
   'Documents',
   { concurrency: 10 }
)

console.log(`Encrypted ${result.summary.success} files`)

Error Handling Best Practices

Robust Error Management
async function encryptFileWithRetry(inputPath, outputPath, keyringName, maxRetries = 3) {
   for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
         // Validate input file
         try {
            await fs.access(inputPath, fsSync.constants.R_OK)
         } catch (error) {
            throw new Error(`Cannot read input file: ${inputPath}`)
         }

         // Get KeyRing
         const ring = await client.ring.get(keyringName)

         if (!ring) {
            throw new Error(`KeyRing '${keyringName}' not found. Create it first.`)
         }

         // Encrypt
         const readStream = fsSync.createReadStream(inputPath)
         const encryptedStream = await ring.encrypt(readStream)

         await new Promise((resolve, reject) => {
            const writeStream = fsSync.createWriteStream(outputPath)

            encryptedStream.pipe(writeStream)

            writeStream.on('finish', resolve)

            // Handle all error sources
            writeStream.on('error', (error) => {
               readStream.destroy()
               reject(new Error(`Write error: ${error.message}`))
            })

            encryptedStream.on('error', (error) => {
               readStream.destroy()
               writeStream.destroy()
               reject(new Error(`Encryption error: ${error.message}`))
            })

            readStream.on('error', (error) => {
               encryptedStream.destroy()
               writeStream.destroy()
               reject(new Error(`Read error: ${error.message}`))
            })
         })

         console.log(`File encrypted successfully: ${outputPath}`)

         return {
            success: true,
            inputPath,
            outputPath,
            attempts: attempt
         }

      } catch (error) {
         console.error(`Attempt ${attempt}/${maxRetries} failed:`, error.message)

         if (attempt < maxRetries) {
            // Exponential backoff
            const delay = Math.pow(2, attempt - 1) * 1000
            console.log(`Retrying in ${delay}ms...`)
            await new Promise(resolve => setTimeout(resolve, delay))

            // Clean up failed output file
            try {
               await fs.unlink(outputPath)
            } catch {}

            continue
         }

         // All retries exhausted
         return {
            success: false,
            error: error.message,
            attempts: maxRetries
         }
      }
   }
}

// Usage with comprehensive error handling
try {
   const result = await encryptFileWithRetry(
      './important-document.pdf',
      './important-document.enc',
      'Documents'
   )

   if (result.success) {
      console.log('Encryption successful')
   } else {
      console.error('Encryption failed after retries:', result.error)
      // Implement fallback or alerting
   }
} catch (error) {
   console.error('Unexpected error:', error)
}

Performance Optimization

High-Performance File Processing
import { pipeline } from 'stream/promises'

class OptimizedFileEncryption {
   constructor(client, keyringName) {
      this.client = client
      this.keyringName = keyringName
   }

   async encryptLargeFile(inputPath, outputPath) {
      const ring = await this.client.ring.get(this.keyringName)

      const readStream = fsSync.createReadStream(inputPath, {
         highWaterMark: 64 * 1024 // 64KB chunks for better performance
      })

      const encryptedStream = await ring.encrypt(readStream)

      const writeStream = fsSync.createWriteStream(outputPath, {
         highWaterMark: 64 * 1024
      })

      // Use pipeline for automatic error handling and cleanup
      await pipeline(
         readStream,
         encryptedStream,
         writeStream
      )

      console.log(`Large file encrypted: ${outputPath}`)
   }

   async streamEncryptToCloud(inputPath, uploadFunction) {
      const ring = await this.client.ring.get(this.keyringName)

      const readStream = fsSync.createReadStream(inputPath)
      const encryptedStream = await ring.encrypt(readStream)

      // Stream directly to cloud storage (S3, GCS, etc.)
      // No intermediate file needed
      await uploadFunction(encryptedStream)

      console.log('File encrypted and uploaded to cloud')
   }
}

// Usage
const optimizer = new OptimizedFileEncryption(client, 'LargeFiles')

// Encrypt large file efficiently
await optimizer.encryptLargeFile('./large-video.mp4', './large-video.enc')

// Stream directly to cloud storage
await optimizer.streamEncryptToCloud('./backup.tar.gz', async (stream) => {
   // Example: Upload to S3
   // const s3 = new S3Client()
   // await s3.upload({ Bucket: 'my-bucket', Key: 'backup.enc', Body: stream })
})

Next Steps