diff --git a/src/app/components/auth/logout/logout.component.ts b/src/app/components/auth/logout/logout.component.ts index 9795ca3..43d89de 100644 --- a/src/app/components/auth/logout/logout.component.ts +++ b/src/app/components/auth/logout/logout.component.ts @@ -42,6 +42,7 @@ export class LogoutComponent implements OnInit, OnDestroy { } logout(): void { + this._signerService.clearPassword(); this._signerService.logout(); console.log("User logged out and keys removed from localStorage."); } diff --git a/src/app/components/chat/chat.service.ts b/src/app/components/chat/chat.service.ts index 83ea3fd..a14a268 100644 --- a/src/app/components/chat/chat.service.ts +++ b/src/app/components/chat/chat.service.ts @@ -20,7 +20,7 @@ export class ChatService implements OnDestroy { private isDecrypting = false; private recipientPublicKey: string; private message: string; - + private decryptedPrivateKey:string; private _chat: BehaviorSubject = new BehaviorSubject(null); private _chats: BehaviorSubject = new BehaviorSubject(null); private _contact: BehaviorSubject = new BehaviorSubject(null); @@ -92,31 +92,34 @@ export class ChatService implements OnDestroy { } } + getContacts(): Observable { return new Observable((observer) => { this._indexedDBService.getAllUsers() .then((cachedContacts: Contact[]) => { - if (cachedContacts.length > 0) { - - cachedContacts.forEach(contact => { + if (cachedContacts && cachedContacts.length > 0) { + const validatedContacts = cachedContacts.map(contact => { if (!contact.pubKey) { console.error('Contact is missing pubKey:', contact); } contact.name = contact.name ? contact.name : 'Unknown'; + return contact; }); - this._contacts.next(cachedContacts); - observer.next(cachedContacts); + this._contacts.next(validatedContacts); + observer.next(validatedContacts); + } else { + observer.next([]); } + observer.complete(); }) .catch((error) => { console.error('Error loading cached contacts from IndexedDB:', error); - observer.error(error); + observer.next([]); + observer.complete(); }); - return () => { - console.log('Unsubscribing from contacts updates.'); - }; + return { unsubscribe() {} }; }); } @@ -165,7 +168,7 @@ export class ChatService implements OnDestroy { async getChats(): Promise> { const pubkey = this._signerService.getPublicKey(); const useExtension = await this._signerService.isUsingExtension(); - const decryptedPrivateKey = await this._signerService.getSecretKey("123"); + this.decryptedPrivateKey = await this._signerService.getDecryptedSecretKey(); const storedChats = await this._indexedDBService.getAllChats(); if (storedChats && storedChats.length > 0) { @@ -187,7 +190,7 @@ export class ChatService implements OnDestroy { console.error('Error updating chat contacts metadata:', error); } }, 0); - this.subscribeToChatList(pubkey, useExtension, decryptedPrivateKey); + this.subscribeToChatList(pubkey, useExtension, this.decryptedPrivateKey); return this.getChatListStream(); } @@ -346,7 +349,7 @@ export class ChatService implements OnDestroy { recipientPublicKey: string ): Promise { if (useExtension) { - return await this._signerService.decryptDMWithExtension(recipientPublicKey, event.content); + return await this._signerService.decryptMessageWithExtension(recipientPublicKey, event.content); } else { return await this._signerService.decryptMessage(decryptedSenderPrivateKey, recipientPublicKey, event.content); } @@ -368,12 +371,12 @@ export class ChatService implements OnDestroy { const decryptedMessage = await this.decryptReceivedMessage( event, await this._signerService.isUsingExtension(), - await this._signerService.getSecretKey("123"), + this.decryptedPrivateKey, senderOrRecipientPubKey ); if (decryptedMessage) { - const messageTimestamp = Math.floor(event.created_at / 1000); + const messageTimestamp = Math.floor(event.created_at ); this.addOrUpdateChatList(pubKey, decryptedMessage, messageTimestamp, isSentByMe); @@ -430,11 +433,9 @@ export class ChatService implements OnDestroy { this.recipientPublicKey = id; const pubkeyPromise = this._signerService.getPublicKey(); - const useExtensionPromise = this._signerService.isUsingExtension(); - const decryptedSenderPrivateKeyPromise = this._signerService.getSecretKey('123'); - return from(Promise.all([pubkeyPromise, useExtensionPromise, decryptedSenderPrivateKeyPromise])).pipe( - switchMap(([pubkey, useExtension, decryptedSenderPrivateKey]) => { + return from(Promise.all([pubkeyPromise])).pipe( + switchMap(() => { return this.chats$.pipe( take(1), distinctUntilChanged(), @@ -478,9 +479,7 @@ export class ChatService implements OnDestroy { this.recipientPublicKey = contact.pubKey; - const pubkey = this._signerService.getPublicKey(); - const useExtension = this._signerService.isUsingExtension(); - const decryptedSenderPrivateKey = this._signerService.getSecretKey('123'); + return this.chats$.pipe( take(1), @@ -536,7 +535,7 @@ export class ChatService implements OnDestroy { if (useExtension) { await this.handleMessageSendingWithExtension(); } else { - const decryptedSenderPrivateKey = await this._signerService.getSecretKey("123"); + if (!this.isValidMessageSetup()) { console.error('Message, sender private key, or recipient public key is not properly set.'); @@ -544,14 +543,14 @@ export class ChatService implements OnDestroy { } const encryptedMessage = await this._signerService.encryptMessage( - decryptedSenderPrivateKey, + this.decryptedPrivateKey, this.recipientPublicKey, this.message ); const messageEvent = this._signerService.getUnsignedEvent(4, [['p', this.recipientPublicKey]], encryptedMessage); - const signedEvent = this._signerService.getSignedEvent(messageEvent, decryptedSenderPrivateKey); + const signedEvent = this._signerService.getSignedEvent(messageEvent, this.decryptedPrivateKey); const published = await this._relayService.publishEventToRelays(signedEvent); diff --git a/src/app/components/settings/profile/profile.component.ts b/src/app/components/settings/profile/profile.component.ts index 2f7efc8..608e36b 100644 --- a/src/app/components/settings/profile/profile.component.ts +++ b/src/app/components/settings/profile/profile.component.ts @@ -16,8 +16,7 @@ import { RelayService } from 'app/services/relay.service'; import { SignerService } from 'app/services/signer.service'; import { UnsignedEvent, NostrEvent, finalizeEvent } from 'nostr-tools'; import { PasswordDialogComponent } from 'app/shared/password-dialog/password-dialog.component'; -import { PasswordService } from 'app/services/password.service'; - + @Component({ selector: 'settings-profile', templateUrl: './profile.component.html', @@ -48,8 +47,7 @@ export class SettingsProfileComponent implements OnInit { private relayService: RelayService, private router: Router, private dialog: MatDialog, - private password: PasswordService - ) { } + ) { } ngOnInit(): void { this.profileForm = this.fb.group({ @@ -99,7 +97,7 @@ export class SettingsProfileComponent implements OnInit { this.content = JSON.stringify(profileData); if (this.signerService.isUsingSecretKey()) { - const storedPassword = this.password.getPassword(); + const storedPassword = this.signerService.getPassword(); if (storedPassword) { try { const privateKey = await this.signerService.getSecretKey(storedPassword); @@ -107,8 +105,6 @@ export class SettingsProfileComponent implements OnInit { } catch (error) { console.error(error); } - - } else { const dialogRef = this.dialog.open(PasswordDialogComponent, { width: '300px', @@ -121,7 +117,7 @@ export class SettingsProfileComponent implements OnInit { const privateKey = await this.signerService.getSecretKey(result.password); this.signEvent(privateKey); if (result.duration != 0) { - this.password.savePassword(result.password, result.duration); + this.signerService.savePassword(result.password, result.duration); } } catch (error) { console.error(error); diff --git a/src/app/components/settings/security/security.component.ts b/src/app/components/settings/security/security.component.ts index f108dfe..ad46382 100644 --- a/src/app/components/settings/security/security.component.ts +++ b/src/app/components/settings/security/security.component.ts @@ -16,8 +16,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { PasswordService } from 'app/services/password.service'; -import { SignerService } from 'app/services/signer.service'; + import { SignerService } from 'app/services/signer.service'; @Component({ selector: 'settings-security', @@ -43,8 +42,7 @@ export class SettingsSecurityComponent implements OnInit { */ constructor( private _formBuilder: UntypedFormBuilder, - private _passwordService: PasswordService, - private _signerService: SignerService + private _signerService: SignerService ) {} /** @@ -80,7 +78,7 @@ export class SettingsSecurityComponent implements OnInit { const savePassword = this.securityForm.get('savePassword')?.value; try { - const success = await this._passwordService.changePassword( + const success = await this._signerService.changePassword( currentPassword, newPassword, savePassword // Save password toggle value diff --git a/src/app/services/password.service.ts b/src/app/services/password.service.ts deleted file mode 100644 index f45b36f..0000000 --- a/src/app/services/password.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Injectable } from '@angular/core'; -import { SignerService } from './signer.service'; - -@Injectable({ - providedIn: 'root', -}) -export class PasswordService { - - private storageKey = 'userPassword'; - - constructor(private signerService: SignerService) {} - - savePassword(password: string, durationInMinutes: number): void { - const expirationTime = Date.now() + durationInMinutes * 60 * 1000; - const passwordData = { - password, - expirationTime - }; - sessionStorage.setItem(this.storageKey, JSON.stringify(passwordData)); - } - - getPassword(): string | null { - const passwordData = sessionStorage.getItem(this.storageKey); - if (!passwordData) return null; - - const { password, expirationTime } = JSON.parse(passwordData); - - if (Date.now() > expirationTime) { - this.clearPassword(); - return null; - } - - return password; - } - - clearPassword(): void { - sessionStorage.removeItem(this.storageKey); - } - - async changePassword(currentPassword: string, newPassword: string , savePassword:boolean): Promise { - try { - const secretKey = await this.signerService.getSecretKey(currentPassword); - if (!secretKey) { - throw new Error('Incorrect current password.'); - } - - await this.signerService.setSecretKey(secretKey, newPassword); - - const nsec = await this.signerService.getNsec(currentPassword); - if (nsec) { - await this.signerService.setNsec(nsec, newPassword); - } - - this.clearPassword(); - - if (savePassword) { - this.savePassword(newPassword, 60); - } - - return true; - } catch (error) { - console.error("Failed to change password: ", error); - return false; - } - } - -} diff --git a/src/app/services/signer.service.ts b/src/app/services/signer.service.ts index 0157d76..397e60b 100644 --- a/src/app/services/signer.service.ts +++ b/src/app/services/signer.service.ts @@ -4,6 +4,8 @@ import { Buffer } from 'buffer'; import { privateKeyFromSeedWords } from 'nostr-tools/nip06'; import { SecurityService } from './security.service'; import { hexToBytes } from '@noble/hashes/utils'; +import { MatDialog } from '@angular/material/dialog'; +import { PasswordDialogComponent } from 'app/shared/password-dialog/password-dialog.component'; @Injectable({ providedIn: 'root' @@ -14,8 +16,68 @@ export class SignerService { localStoragePublicKeyName: string = "publicKey"; localStorageNpubName: string = "npub"; localStorageNsecName: string = "nsec"; + private storageKey = 'userPassword'; + + constructor( + private securityService: SecurityService, + private dialog: MatDialog + ) { } + + savePassword(password: string, durationInMinutes: number): void { + const expirationTime = Date.now() + durationInMinutes * 60 * 1000; + const passwordData = { + password, + expirationTime + }; + sessionStorage.setItem(this.storageKey, JSON.stringify(passwordData)); + } + + getPassword(): string | null { + const passwordData = sessionStorage.getItem(this.storageKey); + if (!passwordData) return null; + + const { password, expirationTime } = JSON.parse(passwordData); + + if (Date.now() > expirationTime) { + this.clearPassword(); + return null; + } + + return password; + } + + clearPassword(): void { + sessionStorage.removeItem(this.storageKey); + } + + async changePassword(currentPassword: string, newPassword: string , savePassword:boolean): Promise { + try { + const secretKey = await this.getSecretKey(currentPassword); + if (!secretKey) { + throw new Error('Incorrect current password.'); + } + + await this.setSecretKey(secretKey, newPassword); + + const nsec = await this.getNsec(currentPassword); + if (nsec) { + await this.setNsec(nsec, newPassword); + } + + this.clearPassword(); + + if (savePassword) { + this.savePassword(newPassword, 60); + } + + return true; + } catch (error) { + console.error("Failed to change password: ", error); + return false; + } + } + - constructor(private securityService: SecurityService) { } getUsername(pubkey: string) { if (pubkey.startsWith("npub")) { @@ -29,6 +91,15 @@ export class SignerService { return nip19.npubEncode(pubkey); } + async requestPassword(): Promise { + const dialogRef = this.dialog.open(PasswordDialogComponent, { + width: '300px', + disableClose: true + }); + + return dialogRef.afterClosed().toPromise(); + } + async nsec(password: string) { if (this.usingSecretKey()) { let secretKey = await this.getSecretKey(password); @@ -38,6 +109,7 @@ export class SignerService { return ""; } + pubkey(npub: string) { return nip19.decode(npub).data.toString(); } @@ -76,6 +148,31 @@ export class SignerService { return await this.securityService.decryptData(encryptedSecretKey, password); } + async getDecryptedSecretKey(): Promise { + try { + const storedPassword = this.getPassword(); + if (storedPassword) { + return await this.getSecretKey(storedPassword); + } + + const result = await this.requestPassword(); + if (result?.password) { + const decryptedPrivateKey = await this.getSecretKey(result.password); + if (result.duration !== 0) { + this.savePassword(result.password, result.duration); + } + return decryptedPrivateKey; + } + + console.error('Password not provided'); + return null; + + } catch (error) { + console.error('Error decrypting private key:', error); + return null; + } + } + //nsec=============== async setNsec(nsec: string, password: string) { const encryptedNsec = await this.securityService.encryptData(nsec, password); @@ -210,36 +307,37 @@ export class SignerService { - // Messaging (NIP-04) - async decryptMessageWithExtension(encryptedContent: string, senderPubKey: string): Promise { + + async encryptMessage(privateKey: string, recipientPublicKey: string, message: string): Promise { + console.log(message); try { - const gt = globalThis as any; - const decryptedMessage = await gt.nostr.nip04.decrypt(senderPubKey, encryptedContent); - return decryptedMessage; + const encryptedMessage = await nip04.encrypt(privateKey, recipientPublicKey, message); + return encryptedMessage; } catch (error) { - console.error('Error decrypting message with extension:', error); - throw new Error('Failed to decrypt message with Nostr extension.'); + console.error('Error encrypting message:', error); + throw error; } } - async encryptMessageWithExtension(content: string, pubKey: string): Promise { const gt = globalThis as any; const encryptedMessage = await gt.nostr.nip04.encrypt(pubKey, content); return encryptedMessage; } - async encryptMessage(privateKey: string, recipientPublicKey: string, message: string): Promise { - console.log(message); - try { - const encryptedMessage = await nip04.encrypt(privateKey, recipientPublicKey, message); - return encryptedMessage; - } catch (error) { - console.error('Error encrypting message:', error); - throw error; + + // Messaging (NIP-04) + async decryptMessageWithExtension(pubkey: string, ciphertext: string): Promise { + const gt = globalThis as any; + if (gt.nostr && gt.nostr.nip04?.decrypt) { + const decryptedContent = await gt.nostr.nip04.decrypt(pubkey, ciphertext) + .catch((error: any) => { + return "*Failed to Decrypted Content*" + }); + return decryptedContent; } + return "Attempted Nostr Window decryption and failed." } - // NIP-04: Decrypting Direct Messages async decryptMessage(privateKey: string, senderPublicKey: string, encryptedMessage: string): Promise { try { @@ -293,41 +391,13 @@ export class SignerService { throw new Error("Failed to Sign with extension"); } - async decryptDMWithExtension(pubkey: string, ciphertext: string): Promise { - const gt = globalThis as any; - if (gt.nostr && gt.nostr.nip04?.decrypt) { - const decryptedContent = await gt.nostr.nip04.decrypt(pubkey, ciphertext) - .catch((error: any) => { - return "*Failed to Decrypted Content*" - }); - return decryptedContent; - } - return "Attempted Nostr Window decryption and failed." - } - - async decryptWithPrivateKey(pubkey: string, ciphertext: string, password: string): Promise { - try { - // Get the stored private key in hex format - let privateKey = await this.getSecretKey("password"); - - // Ensure the private key is in Uint8Array format - const privateKeyUint8Array = new Uint8Array(Buffer.from(privateKey, 'hex')); - - // Decrypt the message using the private key and public key - return await nip04.decrypt(privateKeyUint8Array, pubkey, ciphertext); - } catch (error) { - console.error("Error during decryption: ", error); - return "*Failed to Decrypted Content*"; - } - } - - public async isUsingExtension(): Promise { const globalContext = globalThis as any; if (globalContext.nostr && globalContext.nostr.getPublicKey) { try { - const pubkey = await globalContext.nostr.getPublicKey(); - return !!pubkey; + const secretKey = localStorage.getItem(this.localStorageSecretKeyName); + return !secretKey; + } catch (error) { console.error("Failed to check Nostr extension:", error); return false;