<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tamilselvi K</title>
    <description>The latest articles on DEV Community by Tamilselvi K (@tamilselvi_k_31fa58602adb).</description>
    <link>https://dev.to/tamilselvi_k_31fa58602adb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3655280%2F2bd74e45-bcaa-4b7e-b8af-e822fce9c2d4.png</url>
      <title>DEV Community: Tamilselvi K</title>
      <link>https://dev.to/tamilselvi_k_31fa58602adb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tamilselvi_k_31fa58602adb"/>
    <language>en</language>
    <item>
      <title>Building a Zero-Knowledge File Sharing Platform with Client-Side Encryption</title>
      <dc:creator>Tamilselvi K</dc:creator>
      <pubDate>Wed, 10 Dec 2025 10:56:19 +0000</pubDate>
      <link>https://dev.to/tamilselvi_k_31fa58602adb/building-a-zero-knowledge-file-sharing-platform-with-client-side-encryption-4g93</link>
      <guid>https://dev.to/tamilselvi_k_31fa58602adb/building-a-zero-knowledge-file-sharing-platform-with-client-side-encryption-4g93</guid>
      <description>&lt;p&gt;Building FileBee: A Zero-Knowledge File Sharing Platform with Client-Side Encryption&lt;br&gt;
published: true&lt;br&gt;
description: How I built a secure file-sharing service with end-to-end encryption, zero-knowledge architecture, and automatic 7-day deletion using Angular and IndexedDB&lt;br&gt;
tags: webdev, security, javascript, angular&lt;br&gt;
Building FileBee: A Zero-Knowledge File Sharing Platform with Client-Side Encryption&lt;br&gt;
Ever wondered why most file-sharing services can access your files? When you upload to Dropbox, Google Drive, or WeTransfer, those companies technically have the ability to decrypt and view your data. This bothered me enough to build something different.&lt;br&gt;
Today, I want to share how I built FileBee - a file-sharing platform where even I (the developer) can't access your files. Let's dive into the technical architecture that makes this possible.&lt;br&gt;
🎯 The Core Problem&lt;br&gt;
Traditional file-sharing services use server-side encryption:&lt;/p&gt;

&lt;p&gt;Files are encrypted on the server&lt;br&gt;
The service holds the encryption keys&lt;br&gt;
They can decrypt files anytime (for scanning, compliance, legal requests)&lt;/p&gt;

&lt;p&gt;This creates a fundamental trust problem. You're trusting the company's security, policies, and intentions.&lt;br&gt;
💡 The Solution: Zero-Knowledge Architecture&lt;br&gt;
FileBee implements client-side encryption with a zero-knowledge approach:&lt;/p&gt;

&lt;p&gt;Encryption happens in the browser before upload&lt;br&gt;
Keys are stored locally in IndexedDB&lt;br&gt;
Server only stores encrypted blobs it can't decrypt&lt;br&gt;
If users delete their keys, files become permanently inaccessible&lt;/p&gt;

&lt;p&gt;Even if my database is compromised, attackers get nothing but encrypted data.&lt;br&gt;
🏗️ Technical Architecture&lt;br&gt;
Stack Overview&lt;br&gt;
Frontend: Angular 15+&lt;br&gt;
Storage: IndexedDB (for keys), Cloud Storage (for encrypted files)&lt;br&gt;
Encryption: Web Crypto API&lt;br&gt;
UI Framework: Angular Material&lt;br&gt;
Key Components&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client-Side Encryption Flow
javascript// Simplified encryption flow
async function encryptFile(file: File, encryptionKey: CryptoKey): Promise {
const fileBuffer = await file.arrayBuffer();
const iv = crypto.getRandomValues(new Uint8Array(12)); // GCM IV&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;const encryptedData = await crypto.subtle.encrypt(&lt;br&gt;
    {&lt;br&gt;
      name: 'AES-GCM',&lt;br&gt;
      iv: iv&lt;br&gt;
    },&lt;br&gt;
    encryptionKey,&lt;br&gt;
    fileBuffer&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;// Prepend IV to encrypted data for decryption later&lt;br&gt;
  return concatenate(iv, new Uint8Array(encryptedData));&lt;br&gt;
}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Key Generation &amp;amp; Storage
javascript// Generate encryption key for user
async function generateEncryptionKey(): Promise {
return await crypto.subtle.generateKey(
{
  name: 'AES-GCM',
  length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
}&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// Store in IndexedDB (never sent to server)&lt;br&gt;
async function storeKeyLocally(keyId: string, key: CryptoKey): Promise {&lt;br&gt;
  const exportedKey = await crypto.subtle.exportKey('jwk', key);&lt;/p&gt;

&lt;p&gt;await db.keys.put({&lt;br&gt;
    id: keyId,&lt;br&gt;
    key: exportedKey,&lt;br&gt;
    createdAt: Date.now()&lt;br&gt;
  });&lt;br&gt;
}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload Flow Architecture
User selects file
 ↓
Generate/retrieve encryption key (IndexedDB)
 ↓
Encrypt file in browser (Web Crypto API)
 ↓
Upload encrypted blob to server
 ↓
Server generates short URL
 ↓
Return URL + QR code to user
 ↓
User shares URL + encryption key separately&lt;/li&gt;
&lt;li&gt;Download Flow Architecture
User opens URL
 ↓
Prompt for encryption key
 ↓
Fetch encrypted blob from server
 ↓
Decrypt in browser using provided key
 ↓
Trigger download of decrypted file
🔐 Security Features Implemented&lt;/li&gt;
&lt;li&gt;AES-256-GCM Encryption
Using the Web Crypto API's AES-GCM mode provides:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Strong 256-bit encryption&lt;br&gt;
Built-in authentication (prevents tampering)&lt;br&gt;
Unique IV per file encryption&lt;/p&gt;

&lt;p&gt;javascriptconst algorithm = {&lt;br&gt;
  name: 'AES-GCM',&lt;br&gt;
  length: 256&lt;br&gt;
};&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local Key Management
Keys never leave the browser:
javascript// IndexedDB schema for key storage
interface EncryptionKey {
id: string;           // Unique key identifier
key: JsonWebKey;      // Exported key data
createdAt: number;    // Timestamp
files: string[];      // Associated file IDs
}&lt;/li&gt;
&lt;li&gt;Automatic Deletion
Implemented server-side cron job:
javascript// Pseudo-code for cleanup job
async function cleanupExpiredFiles() {
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;const expiredFiles = await db.files&lt;br&gt;
    .where('uploadedAt')&lt;br&gt;
    .below(sevenDaysAgo)&lt;br&gt;
    .toArray();&lt;/p&gt;

&lt;p&gt;for (const file of expiredFiles) {&lt;br&gt;
    await storage.deleteFile(file.id);&lt;br&gt;
    await db.files.delete(file.id);&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Run every hour&lt;br&gt;
setInterval(cleanupExpiredFiles, 60 * 60 * 1000);&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Key Deletion = Data Destruction
javascriptasync function deleteEncryptionKey(keyId: string): Promise {
// Get all files associated with this key
const key = await db.keys.get(keyId);&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// Delete files from server&lt;br&gt;
  for (const fileId of key.files) {&lt;br&gt;
    await api.deleteFile(fileId);&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;// Delete key from IndexedDB&lt;br&gt;
  await db.keys.delete(keyId);&lt;/p&gt;

&lt;p&gt;// Files are now permanently inaccessible&lt;br&gt;
}&lt;br&gt;
🎨 User Experience Challenges&lt;br&gt;
Building secure software often means compromising UX. Here's how I tackled that:&lt;br&gt;
Challenge 1: Key Management&lt;br&gt;
Problem: Users need to save encryption keys, but they're random strings like k3j2h4kj234h23k4j23h4k23j4h&lt;br&gt;
Solution:&lt;/p&gt;

&lt;p&gt;Display keys prominently after upload&lt;br&gt;
Provide copy-to-clipboard button&lt;br&gt;
Generate QR codes for mobile sharing&lt;br&gt;
Warn users before key deletion&lt;/p&gt;

&lt;p&gt;typescript// Key display component&lt;br&gt;
@Component({&lt;br&gt;
  selector: 'app-encryption-key-display',&lt;br&gt;
  template: &lt;code&gt;&lt;br&gt;
    &amp;lt;div class="key-container"&amp;gt;&lt;br&gt;
      &amp;lt;mat-icon&amp;gt;vpn_key&amp;lt;/mat-icon&amp;gt;&lt;br&gt;
      &amp;lt;code&amp;gt;{{ encryptionKey }}&amp;lt;/code&amp;gt;&lt;br&gt;
      &amp;lt;button mat-icon-button (click)="copyKey()"&amp;gt;&lt;br&gt;
        &amp;lt;mat-icon&amp;gt;content_copy&amp;lt;/mat-icon&amp;gt;&lt;br&gt;
      &amp;lt;/button&amp;gt;&lt;br&gt;
    &amp;lt;/div&amp;gt;&lt;br&gt;
    &amp;lt;mat-card class="warning"&amp;gt;&lt;br&gt;
      &amp;lt;mat-icon&amp;gt;warning&amp;lt;/mat-icon&amp;gt;&lt;br&gt;
      &amp;lt;p&amp;gt;Save this key! Without it, files cannot be decrypted.&amp;lt;/p&amp;gt;&lt;br&gt;
    &amp;lt;/mat-card&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
})&lt;br&gt;
Challenge 2: Large File Handling&lt;br&gt;
Problem: Encrypting 50MB files in-browser can freeze the UI&lt;br&gt;
Solution: Use Web Workers for encryption&lt;br&gt;
javascript// encryption.worker.ts&lt;br&gt;
self.addEventListener('message', async (e) =&amp;gt; {&lt;br&gt;
  const { file, key } = e.data;&lt;/p&gt;

&lt;p&gt;const encrypted = await encryptFile(file, key);&lt;/p&gt;

&lt;p&gt;self.postMessage({ &lt;br&gt;
    encrypted,&lt;br&gt;
    progress: 100 &lt;br&gt;
  });&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;// Main thread&lt;br&gt;
const worker = new Worker('./encryption.worker.ts');&lt;br&gt;
worker.postMessage({ file, key });&lt;/p&gt;

&lt;p&gt;worker.addEventListener('message', (e) =&amp;gt; {&lt;br&gt;
  uploadEncryptedFile(e.data.encrypted);&lt;br&gt;
});&lt;br&gt;
Challenge 3: Folder Uploads&lt;br&gt;
Problem: Maintaining directory structure while encrypting each file&lt;br&gt;
Solution: Store metadata separately&lt;br&gt;
typescriptinterface FolderMetadata {&lt;br&gt;
  files: Array&amp;lt;{&lt;br&gt;
    path: string;        // Original path: "project/src/index.ts"&lt;br&gt;
    encryptedId: string; // Server file ID&lt;br&gt;
    size: number;&lt;br&gt;
    mimeType: string;&lt;br&gt;
  }&amp;gt;;&lt;br&gt;
  commonUrl?: string;    // Optional common URL for all files&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Encrypt and upload folder&lt;br&gt;
async function uploadFolder(folder: File[]): Promise {&lt;br&gt;
  const metadata: FolderMetadata = { files: [] };&lt;/p&gt;

&lt;p&gt;for (const file of folder) {&lt;br&gt;
    const encrypted = await encryptFile(file, key);&lt;br&gt;
    const { id } = await uploadToServer(encrypted);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metadata.files.push({
  path: file.webkitRelativePath || file.name,
  encryptedId: id,
  size: file.size,
  mimeType: file.type
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;return metadata;&lt;br&gt;
}&lt;br&gt;
📊 Storage Limits &amp;amp; Optimization&lt;br&gt;
Limits Implemented:&lt;/p&gt;

&lt;p&gt;50MB per file&lt;br&gt;
250MB total storage per user&lt;br&gt;
7-day automatic deletion&lt;/p&gt;

&lt;p&gt;Storage Quota Check&lt;br&gt;
typescriptasync function checkStorageQuota(user: string): Promise {&lt;br&gt;
  const userFiles = await db.files&lt;br&gt;
    .where('userId')&lt;br&gt;
    .equals(user)&lt;br&gt;
    .toArray();&lt;/p&gt;

&lt;p&gt;const totalSize = userFiles.reduce((sum, f) =&amp;gt; sum + f.size, 0);&lt;br&gt;
  const MAX_STORAGE = 250 * 1024 * 1024; // 250MB&lt;/p&gt;

&lt;p&gt;return totalSize &amp;lt; MAX_STORAGE;&lt;br&gt;
}&lt;br&gt;
File Size Validation&lt;br&gt;
typescriptfunction validateFileSize(file: File): boolean {&lt;br&gt;
  const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB&lt;/p&gt;

&lt;p&gt;if (file.size &amp;gt; MAX_FILE_SIZE) {&lt;br&gt;
    throw new Error(&lt;code&gt;File exceeds 50MB limit: ${file.name}&lt;/code&gt;);&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return true;&lt;br&gt;
}&lt;br&gt;
🎯 QR Code Generation&lt;br&gt;
Auto-generate QR codes for easy mobile sharing:&lt;br&gt;
typescriptimport * as QRCode from 'qrcode';&lt;/p&gt;

&lt;p&gt;async function generateQRCode(url: string, key: string): Promise {&lt;br&gt;
  // Combine URL and key for one-scan sharing&lt;br&gt;
  const shareData = JSON.stringify({ url, key });&lt;/p&gt;

&lt;p&gt;const qrDataUrl = await QRCode.toDataURL(shareData, {&lt;br&gt;
    width: 300,&lt;br&gt;
    margin: 2,&lt;br&gt;
    color: {&lt;br&gt;
      dark: '#667eea',&lt;br&gt;
      light: '#ffffff'&lt;br&gt;
    }&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;return qrDataUrl;&lt;br&gt;
}&lt;br&gt;
🚀 Performance Optimizations&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Chunked Upload for Large Files
typescriptasync function uploadLargeFile(
encrypted: ArrayBuffer, 
chunkSize = 5 * 1024 * 1024 // 5MB chunks
): Promise {
const chunks = Math.ceil(encrypted.byteLength / chunkSize);
const uploadId = generateUploadId();&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;for (let i = 0; i &amp;lt; chunks; i++) {&lt;br&gt;
    const start = i * chunkSize;&lt;br&gt;
    const end = Math.min(start + chunkSize, encrypted.byteLength);&lt;br&gt;
    const chunk = encrypted.slice(start, end);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await uploadChunk(uploadId, i, chunk);
updateProgress((i + 1) / chunks * 100);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;return uploadId;&lt;br&gt;
}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lazy Loading Material Components
typescript// Only load heavy components when needed
const MatDialogModule = await import('@angular/material/dialog');
const QRCodeModule = await import('./qr-code/qr-code.module');&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service Worker for Offline Support&lt;br&gt;
javascript// sw.js - Cache static assets&lt;br&gt;
self.addEventListener('install', (event) =&amp;gt; {&lt;br&gt;
event.waitUntil(&lt;br&gt;
caches.open('filebee-v1').then((cache) =&amp;gt; {&lt;br&gt;
  return cache.addAll([&lt;br&gt;
    '/',&lt;br&gt;
    '/index.html',&lt;br&gt;
    '/styles.css',&lt;br&gt;
    '/main.js'&lt;br&gt;
  ]);&lt;br&gt;
})&lt;br&gt;
);&lt;br&gt;
});&lt;br&gt;
🔧 Testing Strategy&lt;br&gt;
Unit Tests for Encryption&lt;br&gt;
typescriptdescribe('File Encryption', () =&amp;gt; {&lt;br&gt;
it('should encrypt and decrypt file correctly', async () =&amp;gt; {&lt;br&gt;
const originalFile = new File(['test content'], 'test.txt');&lt;br&gt;
const key = await generateEncryptionKey();&lt;/p&gt;

&lt;p&gt;const encrypted = await encryptFile(originalFile, key);&lt;br&gt;
const decrypted = await decryptFile(encrypted, key);&lt;/p&gt;

&lt;p&gt;expect(decrypted).toEqual(await originalFile.arrayBuffer());&lt;br&gt;
});&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;it('should fail decryption with wrong key', async () =&amp;gt; {&lt;br&gt;
    const file = new File(['secret'], 'secret.txt');&lt;br&gt;
    const key1 = await generateEncryptionKey();&lt;br&gt;
    const key2 = await generateEncryptionKey();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const encrypted = await encryptFile(file, key1);

await expectAsync(
  decryptFile(encrypted, key2)
).toBeRejected();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
});&lt;br&gt;
Integration Tests&lt;br&gt;
typescriptdescribe('Upload Flow', () =&amp;gt; {&lt;br&gt;
  it('should complete full upload-download cycle', async () =&amp;gt; {&lt;br&gt;
    const testFile = new File(['content'], 'test.txt');&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Upload
const { url, key } = await uploadFile(testFile);

// Download
const downloaded = await downloadFile(url, key);

expect(downloaded.name).toBe('test.txt');
expect(await downloaded.text()).toBe('content');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
});&lt;br&gt;
🐛 Lessons Learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Browser Compatibility Gotchas
Web Crypto API support varies. I added fallback detection:
typescriptfunction checkCryptoSupport(): boolean {
if (!window.crypto || !window.crypto.subtle) {
alert('Your browser doesn\'t support Web Crypto API');
return false;
}
return true;
}&lt;/li&gt;
&lt;li&gt;IndexedDB Pitfalls
IndexedDB can be quirky. Always wrap operations:
typescriptasync function safeIndexedDBOperation(
operation: () =&amp;gt; Promise
): Promise {
try {
return await operation();
} catch (error) {
if (error.name === 'QuotaExceededError') {
  alert('Storage quota exceeded. Please clear some data.');
}
console.error('IndexedDB error:', error);
return null;
}
}&lt;/li&gt;
&lt;li&gt;Mobile Safari Limitations
iOS Safari has strict storage limits. I added checks:
typescriptif (navigator.storage &amp;amp;&amp;amp; navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate();
console.log(&lt;code&gt;Using ${usage} of ${quota} bytes&lt;/code&gt;);
}
📈 Metrics &amp;amp; Monitoring
Key metrics I track:
typescriptinterface Metrics {
filesUploaded: number;
totalDataEncrypted: number;
averageEncryptionTime: number;
failedUploads: number;
activeUsers: number;
}&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// Track encryption performance&lt;br&gt;
async function trackEncryption(file: File, key: CryptoKey) {&lt;br&gt;
  const startTime = performance.now();&lt;br&gt;
  const encrypted = await encryptFile(file, key);&lt;br&gt;
  const duration = performance.now() - startTime;&lt;/p&gt;

&lt;p&gt;analytics.track('file_encrypted', {&lt;br&gt;
    fileSize: file.size,&lt;br&gt;
    duration,&lt;br&gt;
    throughput: file.size / duration&lt;br&gt;
  });&lt;br&gt;
}&lt;br&gt;
🎓 Key Takeaways&lt;/p&gt;

&lt;p&gt;Client-side encryption is viable for web apps with Web Crypto API&lt;br&gt;
Zero-knowledge architecture requires thoughtful key management UX&lt;br&gt;
IndexedDB works well for local key storage&lt;br&gt;
Web Workers are essential for handling large file encryption without blocking UI&lt;br&gt;
Security and UX don't have to be mutually exclusive&lt;/p&gt;

&lt;p&gt;🔮 Future Enhancements&lt;br&gt;
Planning to add:&lt;/p&gt;

&lt;p&gt;Password-protected links (additional encryption layer)&lt;br&gt;
Expiring links (custom deletion times)&lt;br&gt;
File sharing analytics (download counts, access logs)&lt;br&gt;
Progressive Web App (full offline support)&lt;br&gt;
E2EE chat (discuss files securely)&lt;/p&gt;

&lt;p&gt;🌐 Try It Out&lt;br&gt;
Check out FileBee and let me know what you think!&lt;br&gt;
Features:&lt;/p&gt;

&lt;p&gt;✅ 50MB per file, 250MB total storage&lt;br&gt;
✅ End-to-end encryption with local key management&lt;br&gt;
✅ 7-day automatic deletion&lt;br&gt;
✅ QR code generation&lt;br&gt;
✅ Folder uploads with directory structure&lt;br&gt;
✅ No registration required&lt;br&gt;
✅ 100% free&lt;/p&gt;

&lt;p&gt;💬 Discussion&lt;br&gt;
Questions for the community:&lt;/p&gt;

&lt;p&gt;What other security features would you want in a file-sharing service?&lt;br&gt;
Have you built similar encryption systems? What challenges did you face?&lt;br&gt;
What's your take on client-side vs server-side encryption trade-offs?&lt;/p&gt;

&lt;p&gt;Drop a comment below! I'm happy to discuss the technical details or help if you're building something similar.&lt;br&gt;
Website: &lt;a href="https://filebee.in" rel="noopener noreferrer"&gt;https://filebee.in&lt;/a&gt;&lt;br&gt;
Tags: #webdev #security #javascript #angular #encryption #privacy #opensource&lt;/p&gt;

</description>
      <category>angular</category>
      <category>security</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
