Skip to content

Commit

Permalink
Sync Working
Browse files Browse the repository at this point in the history
  • Loading branch information
0xGingi committed Nov 14, 2024
1 parent 7d47d85 commit 695b3be
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 125 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ services:
rusty-notes-webapp:
build: .
ports:
- "8080:80"
- "8590:80"
restart: unless-stopped
23 changes: 21 additions & 2 deletions src/components/SyncSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { notifications } from '@mantine/notifications';
import { WebStorageService } from '../services/webStorage';
import { SyncSettings as SyncSettingsType } from '../types/sync';
import { CryptoService } from '../services/cryptoService';
import { ApiService } from '../services/apiService';

const DEFAULT_SERVERS = [
{ label: 'Official Server', value: 'https://notes-sync.0xgingi.com' },
Expand Down Expand Up @@ -169,9 +170,26 @@ export function SyncSettings({ onSync }: SyncSettingsProps) {

setSyncing(true);
try {
console.log('Starting sync process...');

// Initialize crypto
await WebStorageService.initializeCrypto(seedPhrase);
console.log('Crypto initialized');

// Validate server health
const isHealthy = await ApiService.healthCheck(selectedServer);
if (!isHealthy) {
throw new Error(`Server ${selectedServer} is not healthy`);
}
console.log('Server health check passed');

// Perform sync
await WebStorageService.syncWithServer(selectedServer);
console.log('Sync completed');

// Save settings
await WebStorageService.saveSyncSettings({ seed_phrase: seedPhrase });
console.log('Settings saved');

if (onSync) {
await onSync();
Expand All @@ -186,14 +204,15 @@ export function SyncSettings({ onSync }: SyncSettingsProps) {
console.error('Sync error:', error);
notifications.show({
title: 'Error',
message: typeof error === 'string' ? error : 'Failed to sync notes',
message: error instanceof Error ? error.message : 'Failed to sync notes',
color: 'red',
autoClose: false,
});
} finally {
setSyncing(false);
}
};

const generateNewSeedPhrase = async () => {
try {
// Use CryptoService's method instead of manual generation
Expand Down
48 changes: 32 additions & 16 deletions src/services/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,16 @@
notes: EncryptedNote[];
}

const DEFAULT_SERVER = 'https://notes-sync.0xgingi.com';
//const DEFAULT_SERVER = 'https://notes-sync.0xgingi.com';

export class ApiService {
private static getEndpoint(serverUrl: string, path: string): string {
// Always use the proxy for the default server
if (serverUrl === DEFAULT_SERVER) {
return `/api${path}`;
}
// For custom servers, use the full URL
return `${serverUrl}/api${path}`;
}

static async healthCheck(serverUrl: string): Promise<boolean> {
try {
const endpoint = this.getEndpoint(serverUrl, '/health');
console.log('Health check endpoint:', endpoint);

const response = await fetch(endpoint, {
headers: {
'Accept': 'application/json',
Expand All @@ -38,20 +31,26 @@
}

const data = await response.json();
return data.status === 'healthy';
return data.status === 'healthy' && data.database === 'connected';
} catch (error) {
console.error('Health check failed:', error);
return false;
}
}

static async syncNotes(
serverUrl: string,
publicKey: string,
encryptedNotes: EncryptedNote[]
): Promise<SyncResponse> {
const endpoint = this.getEndpoint(serverUrl, '/sync');
console.log('Sync endpoint:', endpoint);

console.log('Syncing notes:', {
serverUrl,
endpoint,
notesCount: encryptedNotes.length,
hasPublicKey: !!publicKey
});

try {
const response = await fetch(endpoint, {
Expand All @@ -66,13 +65,30 @@
client_version: '0.1.1'
}),
});


console.log('Server response:', {
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries())
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Sync failed: ${errorText}`);
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.error || `Sync failed with status ${response.status}`);
}

return response.json();

const data = await response.json();
console.log('Sync response data:', data);

// Handle the server's response format
if (!data || !Array.isArray(data.notes)) {
console.error('Invalid response format:', data);
throw new Error('Invalid server response format');
}

return {
notes: data.notes
};
} catch (error) {
console.error('Sync error:', error);
throw error;
Expand Down
88 changes: 40 additions & 48 deletions src/services/cryptoService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Buffer } from 'buffer/';
import { mnemonicToSeedSync, wordlists } from 'bip39';
const WORDLIST = wordlists.english;

interface EncryptedNote {
id: string;
data: string;
Expand Down Expand Up @@ -30,54 +31,67 @@ export class CryptoService {

static generateNewSeedPhrase(): string {
try {
// Generate 12 words
const words = [];
for (let i = 0; i < 12; i++) {
// Generate 2 random bytes for each word (0-2047 range)
const randomBytes = new Uint8Array(2);
crypto.getRandomValues(randomBytes);

// Convert bytes to index (ensuring it's within valid range)
const index = ((randomBytes[0] << 8) | randomBytes[1]) % WORDLIST.length;
words.push(WORDLIST[index]);
}

return words.join(' ');
} catch (error) {
console.error('Failed to generate mnemonic:', error);
throw new Error('Failed to generate seed phrase');
}
}

static async new(seedPhrase: string): Promise<CryptoService> {
// Generate seed from mnemonic
// Generate deterministic seed
const seed = mnemonicToSeedSync(seedPhrase);
const encryptionKey = new Uint8Array(seed.slice(0, 32));

// Use the seed directly for encryption key
const encryptionKey = new Uint8Array(seed.slice(0, 32));

// Generate key pair from seed
const keyPair = await crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256'
namedCurve: 'P-256',
},
true,
['sign', 'verify']
);

// Store the seed hash as the identifier for this user
const publicKeyHash = await crypto.subtle.digest(
'SHA-256',
seed
);

// Store this for user identification
localStorage.setItem('user_id', Buffer.from(publicKeyHash).toString('hex'));

return new CryptoService(
encryptionKey,
keyPair.privateKey,
keyPair.publicKey
);
}

async encryptNote(note: Note): Promise<EncryptedNote> {
// Generate random nonce
if (!note.id) {
throw new Error('Note must have an ID before encryption');
}

const nonceBytes = crypto.getRandomValues(new Uint8Array(12));

// Convert note to JSON string
const noteJson = JSON.stringify(note);
const noteJson = JSON.stringify({
title: note.title,
content: note.content,
created_at: note.created_at,
updated_at: note.updated_at
});

// Import encryption key
const key = await crypto.subtle.importKey(
'raw',
this.encryptionKey,
Expand All @@ -86,7 +100,6 @@ export class CryptoService {
['encrypt']
);

// Encrypt the data
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
Expand All @@ -96,7 +109,6 @@ export class CryptoService {
new TextEncoder().encode(noteJson)
);

// Sign the encrypted data
const signature = await crypto.subtle.sign(
{
name: 'ECDSA',
Expand All @@ -105,21 +117,20 @@ export class CryptoService {
this.signingKey!,
new Uint8Array(encryptedData)
);

return {
id: Buffer.from(BigInt(note.id || 0).toString(16).padStart(16, '0'), 'hex').toString('base64'),
id: note.id.toString(16).padStart(16, '0'),
data: Buffer.from(encryptedData).toString('base64'),
nonce: Buffer.from(nonceBytes).toString('base64'),
timestamp: note.updated_at,
signature: Buffer.from(signature).toString('base64')
};
}

async decryptNote(encrypted: EncryptedNote): Promise<Note> {
const encryptedData = Buffer.from(encrypted.data, 'base64');
const nonceBytes = Buffer.from(encrypted.nonce, 'base64');
async decryptNote(encryptedNote: EncryptedNote): Promise<Note> {
const encryptedData = Buffer.from(encryptedNote.data, 'base64');
const nonce = Buffer.from(encryptedNote.nonce, 'base64');

// Import encryption key
const key = await crypto.subtle.importKey(
'raw',
this.encryptionKey,
Expand All @@ -128,40 +139,21 @@ export class CryptoService {
['decrypt']
);

// Decrypt the data
const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: nonceBytes
iv: nonce
},
key,
encryptedData
);

const note: Note = JSON.parse(new TextDecoder().decode(decryptedData));
const isValid = await crypto.subtle.verify(
{
name: 'ECDSA',
hash: { name: 'SHA-256' },
},
this.verifyingKey!,
Buffer.from(encrypted.signature, 'base64'),
encryptedData
);

if (!isValid) {
throw new Error('Invalid signature');
}

// Verify note ID matches
const noteIdBytes = Buffer.from(encrypted.id, 'base64');
const expectedId = Number(BigInt('0x' + noteIdBytes.toString('hex')));

if (note.id !== expectedId) {
throw new Error('Note ID mismatch');
}

return note;
const noteData = JSON.parse(new TextDecoder().decode(decryptedData));

return {
id: parseInt(encryptedNote.id, 16),
...noteData
};
}

async getPublicKeyBase64(): Promise<string> {
Expand Down
Loading

0 comments on commit 695b3be

Please sign in to comment.