Implementing Data Portability
Implementing Data Portability
Data portability enables users to receive their personal data in a structured, commonly used, and machine-readable format. This right facilitates user autonomy and prevents vendor lock-in. Implementation requires careful consideration of what data to include, how to structure it for maximum utility, and how to deliver it securely. The challenge lies in balancing completeness with privacy and security concerns.
// Data portability implementation
class DataPortabilityService {
constructor() {
this.exportFormats = {
JSON: new JSONExporter(),
CSV: new CSVExporter(),
XML: new XMLExporter()
};
this.dataCategories = {
profile: { include: true, sensitive: true },
content: { include: true, sensitive: false },
activity: { include: true, sensitive: false },
preferences: { include: true, sensitive: false },
connections: { include: true, sensitive: true }
};
}
// Process portability request
async processPortabilityRequest(request) {
const exportJob = {
jobId: this.generateJobId(),
requestId: request.id,
userId: request.userId,
format: request.format || 'JSON',
status: 'collecting',
progress: 0
};
try {
// Collect portable data
const portableData = await this.collectPortableData(request.userId, exportJob);
// Structure data according to standards
const structuredData = await this.structureData(portableData);
// Export in requested format
const exported = await this.exportData(structuredData, exportJob.format);
// Package securely
const package = await this.packageData(exported, request);
// Create secure delivery
const delivery = await this.createSecureDelivery(package, request);
exportJob.status = 'completed';
exportJob.deliveryDetails = delivery;
return exportJob;
} catch (error) {
exportJob.status = 'failed';
exportJob.error = error.message;
throw error;
}
}
// Collect data that is portable
async collectPortableData(userId, job) {
const portableData = {};
const categories = Object.entries(this.dataCategories)
.filter(([_, config]) => config.include);
let processed = 0;
for (const [category, config] of categories) {
try {
// Update progress
job.progress = Math.floor((processed / categories.length) * 50);
await this.updateJob(job);
// Collect category data
const collector = this.getCollector(category);
const data = await collector.collect(userId);
// Filter portable data only
const filtered = this.filterPortableData(data, category);
// Sanitize if sensitive
if (config.sensitive) {
portableData[category] = await this.sanitizeSensitiveData(filtered);
} else {
portableData[category] = filtered;
}
processed++;
} catch (error) {
console.error(`Failed to collect ${category}:`, error);
portableData[category] = {
error: 'Failed to collect',
reason: error.message
};
}
}
return portableData;
}
// Structure data according to portability standards
async structureData(data) {
return {
'@context': 'https://schema.org',
'@type': 'DataPortabilityPackage',
version: '1.0',
created: new Date().toISOString(),
dataController: {
'@type': 'Organization',
name: process.env.COMPANY_NAME,
url: process.env.COMPANY_URL
},
dataSubject: {
'@type': 'Person',
identifier: data.profile?.id
},
datasets: Object.entries(data).map(([category, categoryData]) => ({
'@type': 'Dataset',
name: category,
description: this.getCategoryDescription(category),
distribution: {
'@type': 'DataDownload',
encodingFormat: 'application/json',
contentSize: JSON.stringify(categoryData).length
},
data: categoryData
}))
};
}
// Create secure data package
async packageData(exportedData, request) {
// Encrypt data
const encryptedData = await this.encryptPackage(exportedData, request.userId);
// Add integrity verification
const integrity = {
algorithm: 'sha256',
hash: crypto.createHash('sha256').update(exportedData).digest('hex')
};
// Create manifest
const manifest = {
version: '1.0',
created: new Date().toISOString(),
requestId: request.id,
format: request.format,
encryption: {
algorithm: 'aes-256-gcm',
keyDerivation: 'pbkdf2'
},
integrity,
instructions: this.getExtractionInstructions(request.format)
};
// Package everything
const zip = new JSZip();
zip.file('manifest.json', JSON.stringify(manifest, null, 2));
zip.file('data.encrypted', encryptedData);
zip.file('README.txt', this.generateReadme(request));
return await zip.generateAsync({ type: 'nodebuffer' });
}
// Create secure delivery mechanism
async createSecureDelivery(package, request) {
// Generate secure download token
const token = crypto.randomBytes(32).toString('hex');
// Store package temporarily
const storageKey = `export_${request.id}_${token}`;
await this.secureStorage.store(storageKey, package, {
ttl: 7 * 24 * 60 * 60 // 7 days
});
// Create download URL
const downloadUrl = `${process.env.APP_URL}/privacy/download/${token}`;
// Send notification with download link
await this.notificationService.send(request.userId, {
type: 'data_export_ready',
subject: 'Your data export is ready',
downloadUrl,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
passwordHint: 'Use your account password to decrypt'
});
return {
method: 'secure_download',
url: downloadUrl,
token,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
};
}
}
// JSON exporter with privacy considerations
class JSONExporter {
export(data) {
return JSON.stringify(data, this.replacer, 2);
}
replacer(key, value) {
// Exclude internal fields
if (key.startsWith('_')) return undefined;
// Format dates consistently
if (value instanceof Date) {
return value.toISOString();
}
// Exclude null values for cleaner export
if (value === null) return undefined;
return value;
}
}