Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.signing-content {
height: 120px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="page">
{{ "Action.AttestContentDescription" | translate }}:
<br><br>

{{ "Action.Wallet" | translate }}: {{walletManager.activeWallet?.name}}<br>
{{ "Action.Account" | translate }}: {{walletManager.activeAccount?.name}}<br>
{{ "Action.DerivationPath" | translate }}: {{networkService.getDerivationPathForAccount(walletManager.activeAccount)}}<br><br>

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Component, ChangeDetectorRef, ApplicationRef, NgZone, OnInit } from '@angular/core';
import { CryptoService, UIState, NetworksService, CommunicationService, AppManager, WalletManager } from '../../services';
import { Router } from '@angular/router';
import { ActionService } from '../../services/action.service';
import { TranslateService } from '@ngx-translate/core';
import { nostrPrivateChannelAccountName } from '../../../shared/handlers/nostr-nip76-wallet-handler';
const { v4: uuidv4 } = require('uuid');

@Component({
selector: 'app-nostr-nip76-root',
templateUrl: './nostr-nip76-root.component.html',
styleUrls: ['./nostr-nip76-root.component.css']
})
export class ActionNostrNip76RootComponent implements OnInit {
content?: string;
parameters?: any;
expiryDate: Date;
callback: string;
result: string;
success?: boolean;
mnemonic: string;

constructor(
public uiState: UIState,
private crypto: CryptoService,
private router: Router,
private app: ApplicationRef,
private actionService: ActionService,
private ngZone: NgZone,
private communication: CommunicationService,
public networkService: NetworksService,
public walletManager: WalletManager,
private manager: AppManager,
private cd: ChangeDetectorRef,
public translate: TranslateService) {

this.actionService.status.title = 'Use an HDK Index';
this.actionService.status.description =
`Uses a separate Hierarchical Deterministic Key on the Account used for signing, indexing and encrypting Nostr Events.`;
this.actionService.consentType = 'regular';
this.actionService.permissionLevel = 'account';
}

ngOnDestroy(): void {

}

async ngOnInit() {
const accountTranslate = await this.translate.get('Account.Account').toPromise();
this.uiState.title = accountTranslate + ': ' + this.walletManager.activeAccount?.name;
this.mnemonic = this.crypto.generateMnemonic();
// this.createPrivateChannelAccount();
}

async createPrivateChannelAccount() {
let account = this.walletManager.activeWallet.accounts.find(x => x.name === nostrPrivateChannelAccountName);
if (!account) {
account = {
identifier: uuidv4(),
type: 'identity',
mode: 'normal',
singleAddress: true,
networkType: 'NOSTR',
name: nostrPrivateChannelAccountName,
index: 1776,
network: 1237,
purpose: 44,
purposeAddress: 340,
icon: 'account_circle',
};
await this.walletManager.addAccount(account, this.walletManager.activeWallet);
}
this.actionService.accountId = account.identifier;
}
}
36 changes: 36 additions & 0 deletions angular/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { ActionNostrDecryptComponent } from './action/nostr.decrypt/nostr.decryp
import { ActionTransactionSendComponent } from './action/transaction.send/transaction.send.component';
import { ActionWalletUnlockComponent } from './action/wallet.unlock/wallet.unlock.component';
import { ActionSwapsSendComponent } from './action/swaps.send/swaps.send.component';
import { ActionNostrNip76RootComponent } from './action/nostr-nip76-root/nostr-nip76-root.component';

const routes: Routes = [
{
Expand Down Expand Up @@ -477,6 +478,41 @@ const routes: Routes = [
data: LoadingResolverService,
},
},
{
path: 'nostr.nip76.index',
component: ActionNostrNip76RootComponent,
resolve: {
data: LoadingResolverService,
},
},
{
path: 'nostr.nip76.event.create',
component: ActionNostrNip76RootComponent,
resolve: {
data: LoadingResolverService,
},
},
{
path: 'nostr.nip76.event.delete',
component: ActionNostrNip76RootComponent,
resolve: {
data: LoadingResolverService,
},
},
{
path: 'nostr.nip76.invite.read',
component: ActionNostrNip76RootComponent,
resolve: {
data: LoadingResolverService,
},
},
{
path: 'nostr.nip76.invite.create',
component: ActionNostrNip76RootComponent,
resolve: {
data: LoadingResolverService,
},
},
{
path: 'transaction.send',
component: ActionTransactionSendComponent,
Expand Down
3 changes: 2 additions & 1 deletion angular/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ import { ActionTransactionSendComponent } from './action/transaction.send/transa
import { SendConfirmationDialog } from './action/transaction.send/send-confirmation-dialog/send-confirmation-dialog';
import { ActionWalletUnlockComponent } from './action/wallet.unlock/wallet.unlock.component';
import { ActionSwapsSendComponent } from './action/swaps.send/swaps.send.component';

import { ActionNostrNip76RootComponent } from './action/nostr-nip76-root/nostr-nip76-root.component';
// required for AOT compilation
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http);
Expand Down Expand Up @@ -216,6 +216,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
SendConfirmationDialog,
ActionWalletUnlockComponent,
ActionSwapsSendComponent,
ActionNostrNip76RootComponent
],
imports: [
BrowserModule,
Expand Down
9 changes: 9 additions & 0 deletions angular/src/shared/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SendTransactionHandler } from './send-transaction-handler';
import { AtomicSwapsKeyHandler } from './atomic-swap-key-handler';
import { AtomicSwapsSecretHandler } from './atomic-swap-secret-handler';
import { AtomicSwapsSendHandler } from './atomic-swap-send-handler';
import { NostrNip76WalletHandler } from './nostr-nip76-wallet-handler';

// TODO: Make this more generic where the handlers are registered as form of factory.
export class Handlers {
Expand Down Expand Up @@ -53,6 +54,14 @@ export class Handlers {
return new NostrEncryptHandler(backgroundManager);
case 'nostr.decrypt':
return new NostrDecryptHandler(backgroundManager);
case 'nostr.nip76.index':
case 'nostr.nip76.event.create':
case 'nostr.nip76.event.delete':
case 'nostr.nip76.invite.read':
case 'nostr.nip76.invite.create':
const handler = new NostrNip76WalletHandler(backgroundManager);
handler.action = [action];
return handler;
case 'transaction.send':
return new SendTransactionHandler(backgroundManager);
case 'atomicswaps.key':
Expand Down
143 changes: 143 additions & 0 deletions angular/src/shared/handlers/nostr-nip76-wallet-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex } from '@noble/hashes/utils';
import { HDKey } from '@scure/bip32';
import {
ContentDocument, getReducedKey, HDKey as Nip76HDKey, HDKIndex, HDKIndexDTO, HDKIndexType, nip19Extension,
NostrEventDocument, SequentialKeysetDTO, Versions, Nip76ProviderIndexArgs, walletRsvpDocumentsOffset
} from 'animiq-nip76-tools';
import { getPublicKey } from 'nostr-tools';
import { BackgroundManager } from '../background-manager';
import { SigningUtilities } from '../identity/signing-utilities';
import { ActionPrepareResult, ActionResponse, Permission } from '../interfaces';
import { ActionHandler, ActionState } from './action-handler';

export const nostrPrivateChannelAccountName = 'Nostr Private Channels';

export class NostrNip76WalletHandler implements ActionHandler {
action = ['nostr.nip76.index'];
utility = new SigningUtilities();

constructor(private backgroundManager: BackgroundManager) { }

async prepare(state: ActionState): Promise<ActionPrepareResult> {
return {
content: [],
consent: true,
};
}

async getKey(permission: Permission, keyId: string): Promise<HDKey> {
const { network, node } = await this.backgroundManager.getKey(permission.walletId, permission.accountId, keyId);
return node as HDKey;
}

async getRootKeyInfo(permission: Permission, keyPage = 0)
: Promise<{ rootKey: Nip76HDKey, wordset: Uint32Array, documentsIndex: HDKIndex }> {

const key0 = await this.getKey(permission, `1776'/0'`);
const rootKey = new Nip76HDKey({ privateKey: key0.privateKey, chainCode: key0.chainCode, version: Versions.nip76API1 });

const wordsetKey = await this.getKey(permission, `1776'/1'`);
const wordsetHash = sha512(wordsetKey.privateKey);
const wordset = new Uint32Array((wordsetHash).buffer);

const key1 = getReducedKey({ root: rootKey, wordset: wordset.slice(0, 4) });
const key2 = getReducedKey({ root: rootKey, wordset: wordset.slice(4, 8) });
const documentsIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, key1, key2, wordset.slice(8));

documentsIndex.getSequentialKeyset(0, keyPage);
documentsIndex.getSequentialKeyset(walletRsvpDocumentsOffset, keyPage);

return { rootKey, wordset, documentsIndex };
}

async execute(state: ActionState, permission: Permission): Promise<ActionResponse> {
const profileKey = await this.getKey(permission, permission.keyId);
const key = getPublicKey(profileKey.privateKey as any);

switch (state.message.request.method) {
case 'nostr.nip76.index': {
const indexArgs: Nip76ProviderIndexArgs = state.message.request.params[1];
const { rootKey, wordset, documentsIndex } = await this.getRootKeyInfo(permission, indexArgs.keyPage);
if (indexArgs.privateIndexId === null) {
documentsIndex.signingParent.wipePrivateData();
documentsIndex.encryptParent.wipePrivateData();
const response = { hdkIndex: documentsIndex.toJSON() };
return { key, response };
} else {
const keyset = documentsIndex.getDocumentKeyset(indexArgs.privateIndexId, bytesToHex(profileKey.privateKey));
const hdkIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, keyset.signingKey, keyset.encryptKey);
hdkIndex.getSequentialKeyset(0, indexArgs.keyPage);
hdkIndex.signingParent.wipePrivateData();
hdkIndex.encryptParent.wipePrivateData();
const response = { hdkIndex: hdkIndex.toJSON() };
return { key, response };
}
}
case 'nostr.nip76.event.create': {
const serializedContent = state.message.request.params[2];
const kind = parseInt(serializedContent.match(/\d+/)![0]);
const doc = HDKIndex.getContentDocument(kind);
doc.deserialize(serializedContent);
doc.content.pubkey = key;
doc.docIndex = state.message.request.params[3];

let hdkIndex: HDKIndex;
const indexArgs: Nip76ProviderIndexArgs = state.message.request.params[1];
if (indexArgs.publicIndex) {
hdkIndex = HDKIndex.fromJSON(indexArgs.publicIndex as HDKIndexDTO);
} else {
const { rootKey, wordset, documentsIndex } = await this.getRootKeyInfo(permission);
if (indexArgs.privateIndexId === null) {
hdkIndex = documentsIndex;
} else {
const keyset = documentsIndex.getDocumentKeyset(indexArgs.privateIndexId, bytesToHex(profileKey.privateKey));
hdkIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, keyset.signingKey, keyset.encryptKey);
}
}
const event = await hdkIndex.createEvent(doc, bytesToHex(profileKey.privateKey));
const response = { event };
return { key, response };
}
case 'nostr.nip76.event.delete': {
const doc = new ContentDocument();
doc.nostrEvent = { id: state.message.request.params[2] } as NostrEventDocument;
doc.docIndex = state.message.request.params[3];

let hdkIndex: HDKIndex;
const indexArgs: Nip76ProviderIndexArgs = state.message.request.params[1];
if (indexArgs.publicIndex) {
hdkIndex = HDKIndex.fromJSON(indexArgs.publicIndex as HDKIndexDTO);
} else {
const { rootKey, wordset, documentsIndex } = await this.getRootKeyInfo(permission);
if (indexArgs.privateIndexId === null) {
hdkIndex = documentsIndex;
} else {
const keyset = documentsIndex.getDocumentKeyset(indexArgs.privateIndexId, bytesToHex(profileKey.privateKey));
hdkIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, keyset.signingKey, keyset.encryptKey);
}
}
const event = await hdkIndex.createDeleteEvent(doc, bytesToHex(profileKey.privateKey));
const response = { event };
return { key, response };
}
case 'nostr.nip76.invite.read': {
const channelPointer = state.message.request.params[0];
const p = await nip19Extension.decode(channelPointer, bytesToHex(profileKey.privateKey));
const pointer = nip19Extension.pointerToDTO(p.data as nip19Extension.PrivateChannelPointer);
const response = { pointer };
return { key, response };
}
case 'nostr.nip76.invite.create': {
const pointer: nip19Extension.PrivateChannelPointerDTO = state.message.request.params[0];
const forPubkey: string = state.message.request.params[1];
const channelPointer = nip19Extension.pointerFromDTO(pointer);
const invitation = await nip19Extension.nprivateChannelEncode(channelPointer, bytesToHex(profileKey.privateKey), forPubkey);
const response = { invitation };
return { key, response };
}
default:
throw new Error(`method '${state.message.request.method}' not recognized.`);
};
}
}
Loading