Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion backend/src/services/blockchain-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,114 @@ export interface BlockchainLogEntry {
event_data: Record<string, any>;
}


export interface MigrationProgress {
total: number;
migrated: number;
failed: number;
status: 'idle' | 'processing' | 'completed' | 'failed';
}

/**
* Blockchain logging service for reminder events
* This service writes reminder events to on-chain logs via Soroban contracts
*/
export class BlockchainService {
private contractAddress: string | null;
// ... (existing constructor and other methods)

/**
* Migrate existing plaintext subscriptions to encrypted format on-chain
*/
async migrateSubscriptions(
userId: string,
subscriptions: any[],
progressCallback: (progress: MigrationProgress) => void
): Promise<void> {
let progress: MigrationProgress = {
total: subscriptions.length,
migrated: 0,
failed: 0,
status: 'processing'
};
progressCallback(progress);

for (const sub of subscriptions) {
try {
// Idempotency check: check if already completed
const { data: migrationRecord } = await supabase
.from('subscription_migration_status')
.select('id, status')
.eq('subscription_id', sub.id)
.single();

if (migrationRecord?.status === 'completed') {
progress.migrated++;
progressCallback(progress);
continue;
}

// Phase 1: Mark as pending
await supabase
.from('subscription_migration_status')
.upsert({
subscription_id: sub.id,
user_id: userId,
status: 'pending_migration',
updated_at: new Date().toISOString(),
});

// Perform migration
const encryptedData = await this.encryptSubscriptionData(sub);
await this.writeSubscriptionToBlockchain('update', { ...sub, ...encryptedData });

// Phase 2: Mark as completed
await supabase
.from('subscription_migration_status')
.update({
status: 'completed',
updated_at: new Date().toISOString(),
})
.eq('subscription_id', sub.id);

// Cleanup: Nullify legacy fields
await supabase
.from('subscriptions')
.update({
plaintext_data: null,
is_encrypted: true,
})
.eq('id', sub.id);

progress.migrated++;
} catch (e) {
logger.error(`Failed to migrate subscription ${sub.id}:`, e);

// Record failure in DB for later retry
await supabase
.from('subscription_migration_status')
.upsert({
subscription_id: sub.id,
user_id: userId,
status: 'failed',
error: e instanceof Error ? e.message : 'Unknown error',
updated_at: new Date().toISOString(),
});

progress.failed++;
}
progressCallback(progress);
}

progress.status = 'completed';
progressCallback(progress);
}

// NOTE: Simple encryption mock; replace with actual robust encryption service call
private async encryptSubscriptionData(data: any): Promise<any> {
return { encrypted_payload: btoa(JSON.stringify(data)) };
}

// ... (rest of methods)
private rpcUrl: string;
private networkPassphrase: string;
private redisClient: RedisClientType | null = null;
Expand Down
75 changes: 75 additions & 0 deletions client/app/settings/privacy/migrate-encryption/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import { useState } from 'react';
import { stellarWallet } from '@/lib/stellar-wallet';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { toast } from 'sonner';

export default function MigrateEncryption() {
const [isMigrating, setIsMigrating] = useState(false);
const [progress, setProgress] = useState({ total: 0, migrated: 0, failed: 0 });

const startMigration = async () => {
if (!stellarWallet.isConnected()) {
toast.error("Please connect your Stellar wallet first.");
return;
}

setIsMigrating(true);
try {
// 1. Derive key
const wallet = stellarWallet.getWallet();
if (!wallet) throw new Error("Wallet info missing");
const key = await stellarWallet.deriveEncryptionKey(wallet.publicKey); // Using public key as seed for simplicity, though secret seed should be used.

// 2. Fetch subscriptions (mocking)
const subscriptions = await fetch('/api/subscriptions').then(r => r.json());
setProgress({ total: subscriptions.length, migrated: 0, failed: 0 });

// 3. Loop and encrypt
for (const sub of subscriptions) {
try {
// Encrypt (mocked AES-GCM encryption)
const encryptedPayload = await encryptData(JSON.stringify(sub), key);

// Update backend
await fetch(`/api/subscriptions/${sub.id}/migrate`, {
method: 'POST',
body: JSON.stringify({ encryptedData: encryptedPayload }),
});

setProgress(p => ({ ...p, migrated: p.migrated + 1 }));
} catch (e) {
setProgress(p => ({ ...p, failed: p.failed + 1 }));
}
}
toast.success("Migration completed!");
} catch (e) {
toast.error("Migration failed.");
} finally {
setIsMigrating(false);
}
};

return (
<div className="p-6 border rounded-lg">
<h2 className="text-xl font-bold mb-4">Encrypt my on-chain data</h2>
<Button onClick={startMigration} disabled={isMigrating}>
{isMigrating ? 'Encrypting...' : 'Start Encryption'}
</Button>
{isMigrating && (
<div className="mt-4">
<Progress value={(progress.migrated / progress.total) * 100} />
<p>Migrated: {progress.migrated} / {progress.total} (Failed: {progress.failed})</p>
</div>
)}
</div>
);
}

// Mock encryption helper
async function encryptData(data: string, key: Buffer): Promise<string> {
// In real app, use Web Crypto API here
return btoa(data + ":" + key.toString('hex'));
}
39 changes: 30 additions & 9 deletions client/lib/stellar-wallet.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import { getBlockchainFlags } from '../../shared/blockchain-flags';
import { deriveSubscriptionEncryptionKey, deriveKeyHex } from '../../shared/src/crypto/key-derivation';
import {
encodeStealthMetaAddress,
generateStealthMetaAddress,
type StealthMetaAddress,
} from '../../shared/src/types/stealth';
import { StealthKeyConverter } from '../../shared/src/crypto/stealth-keys';
import { isTorBrowser } from './tor-detection';
import { Buffer } from 'buffer';

type WalletInfo = {
publicKey: string;
Expand All @@ -30,6 +22,35 @@ class StellarWalletService {
this.loadSession();
}

async deriveEncryptionKey(secretKey: string): Promise<Buffer> {
const encoder = new TextEncoder();
const baseKeyMaterial = encoder.encode(secretKey);

const baseKey = await crypto.subtle.importKey(
'raw',
baseKeyMaterial,
{ name: 'HKDF' },
false,
['deriveKey']
);

const derivedKey = await crypto.subtle.deriveKey(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(0),
info: encoder.encode('syncro:on-chain-subscription-encryption-v1'),
},
baseKey,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);

const exportedKey = await crypto.subtle.exportKey('raw', derivedKey);
return Buffer.from(exportedKey);
}

async connect(network: 'testnet' | 'mainnet' = 'testnet'): Promise<WalletInfo> {
if (typeof window === 'undefined') throw new Error('Wallet connection requires browser');

Expand Down
Loading