From 4fb30ba36f0fc686c6ef0f99f954c78eb0f3f47f Mon Sep 17 00:00:00 2001 From: Terry Yiu <963907+tyiu@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:32:17 +0200 Subject: [PATCH] Add support for receiver aliases for gift wrap creation to avoid exposing identities and improve test coverage --- .../Events/GiftWrap/GiftWrapEvent.swift | 20 ++++++------ .../Events/GiftWrap/GiftWrapEventTests.swift | 32 ++++++++++++++++++- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift b/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift index c05b75f..2cfe48f 100644 --- a/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift +++ b/Sources/NostrSDK/Events/GiftWrap/GiftWrapEvent.swift @@ -87,36 +87,38 @@ public extension EventCreating { /// Creates a ``GiftWrapEvent`` that takes a rumor, an unsigned ``NostrEvent``, and seals it in a signed ``SealEvent``, and then wraps that seal encrypted in the content of the gift wrap. /// /// - Parameters: - /// - withRumor: a ``NostrEvent`` that is not signed. - /// - toRecipient: the ``PublicKey`` of the receiver of the event. This pubkey will automatically be added as a tag to the ``GiftWrapEvent``. + /// - rumor: a ``NostrEvent`` that is not signed. + /// - recipient: the ``PublicKey`` of the receiver of the event. This pubkey will be used to encrypt the rumor. If `recipientAlias` is not provided, this pubkey will automatically be added as a tag to the ``GiftWrapEvent``. + /// - recipientAlias: optional ``PublicKey`` of the receiver's alias used to receive gift wraps without exposing the receiver's identity. It is not used to encrypt the rumor. If it is provided, this pubkey will automatically be added as a tag to the ``GiftWrapEvent``. /// - tags: the list of tags to add to the ``GiftWrapEvent`` in addition to the pubkey tag from `toRecipient`. This list should include any information needed to route the event to its intended recipient, such as [NIP-13 Proof of Work](https://github.com/nostr-protocol/nips/blob/master/13.md). /// - createdAt: the creation timestamp of the seal. Note that this timestamp SHOULD be tweaked to thwart time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps SHOULD be in the past. By default, if `createdAt` is not provided, a random timestamp within 2 days in the past will be chosen. /// - keypair: The real ``Keypair`` to sign the seal with. Note that a different random one-time use key is used to sign the gift wrap. func giftWrap( withRumor rumor: NostrEvent, toRecipient recipient: PublicKey, + recipientAlias: PublicKey? = nil, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800)), signedBy keypair: Keypair ) throws -> GiftWrapEvent { let seal = try seal(withRumor: rumor, toRecipient: recipient, signedBy: keypair) - return try giftWrap(withSeal: seal, toRecipient: recipient, tags: tags, createdAt: createdAt, signedBy: keypair) + return try giftWrap(withSeal: seal, toRecipient: recipient, recipientAlias: recipientAlias, tags: tags, createdAt: createdAt) } /// Creates a ``GiftWrapEvent`` that takes a signed ``SealEvent``, and then wraps that seal encrypted in the content of the gift wrap. /// /// - Parameters: - /// - withSeal: a signed ``SealEvent``. - /// - toRecipient: the ``PublicKey`` of the receiver of the event. + /// - seal: a signed ``SealEvent``. + /// - recipient: the ``PublicKey`` of the receiver of the event. This pubkey will be used to encrypt the rumor. If `recipientAlias` is not provided, this pubkey will automatically be added as a tag to the ``GiftWrapEvent``. + /// - recipientAlias: optional ``PublicKey`` of the receiver's alias used to receive gift wraps without exposing the receiver's identity. It is not used to encrypt the rumor. If it is provided, this pubkey will automatically be added as a tag to the ``GiftWrapEvent``. /// - tags: the list of tags. /// - createdAt: the creation timestamp of the seal. Note that this timestamp SHOULD be tweaked to thwart time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps SHOULD be in the past. By default, if `createdAt` is not provided, a random timestamp within 2 days in the past will be chosen. - /// - keypair: The real ``Keypair`` to sign the seal with. Note that a different random one-time use key is used to sign the gift wrap. func giftWrap( withSeal seal: SealEvent, toRecipient recipient: PublicKey, + recipientAlias: PublicKey? = nil, tags: [Tag] = [], - createdAt: Int64 = Int64(Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800)), - signedBy keypair: Keypair + createdAt: Int64 = Int64(Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800)) ) throws -> GiftWrapEvent { let jsonData = try JSONEncoder().encode(seal) guard let stringifiedJSON = String(data: jsonData, encoding: .utf8) else { @@ -127,7 +129,7 @@ public extension EventCreating { throw GiftWrapError.keypairGenerationFailed } - let combinedTags = [Tag(name: .pubkey, value: recipient.hex)] + tags + let combinedTags = [Tag(name: .pubkey, value: (recipientAlias ?? recipient).hex)] + tags let encryptedSeal = try encrypt(plaintext: stringifiedJSON, privateKeyA: randomKeypair.privateKey, publicKeyB: recipient) return try GiftWrapEvent(content: encryptedSeal, tags: combinedTags, createdAt: createdAt, signedBy: randomKeypair) diff --git a/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift b/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift index 9cfeeb4..cdaef96 100644 --- a/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift +++ b/Tests/NostrSDKTests/Events/GiftWrap/GiftWrapEventTests.swift @@ -17,9 +17,39 @@ final class GiftWrapEventTests: XCTestCase, EventCreating, EventVerifying, Fixtu func testCreateGiftWrapSucceeds() throws { let rumor: NostrEvent = try decodeFixture(filename: "rumor") - let giftWrapEvent = try giftWrap(withRumor: rumor, toRecipient: GiftWrapEventTests.recipient.publicKey, signedBy: .test) + let tags = [ + Tag(name: "randomTag1", value: "value"), + Tag(name: "randomTag2", value: "value") + ] + + let giftWrapEvent = try giftWrap(withRumor: rumor, toRecipient: GiftWrapEventTests.recipient.publicKey, tags: tags, signedBy: .test) try verifyEvent(giftWrapEvent) + XCTAssertNotEqual(giftWrapEvent.pubkey, Keypair.test.publicKey.hex) + XCTAssertEqual(giftWrapEvent.firstValueForTagName(.pubkey), GiftWrapEventTests.recipient.publicKey.hex) + XCTAssertEqual(giftWrapEvent.tags, [Tag.pubkey(GiftWrapEventTests.recipient.publicKey.hex)] + tags) + + let unsealedRumor = try giftWrapEvent.unsealedRumor(using: GiftWrapEventTests.recipient.privateKey) + + XCTAssertEqual(rumor, unsealedRumor) + } + + func testCreateGiftWrapWithRecipientAliasSucceeds() throws { + let rumor: NostrEvent = try decodeFixture(filename: "rumor") + let recipientAlias = try XCTUnwrap(Keypair()?.publicKey) + + let tags = [ + Tag(name: "randomTag1", value: "value"), + Tag(name: "randomTag2", value: "value") + ] + + let giftWrapEvent = try giftWrap(withRumor: rumor, toRecipient: GiftWrapEventTests.recipient.publicKey, recipientAlias: recipientAlias, tags: tags, signedBy: .test) + try verifyEvent(giftWrapEvent) + + XCTAssertNotEqual(giftWrapEvent.pubkey, Keypair.test.publicKey.hex) + XCTAssertEqual(giftWrapEvent.firstValueForTagName(.pubkey), recipientAlias.hex) + XCTAssertEqual(giftWrapEvent.tags, [Tag.pubkey(recipientAlias.hex)] + tags) + let unsealedRumor = try giftWrapEvent.unsealedRumor(using: GiftWrapEventTests.recipient.privateKey) XCTAssertEqual(rumor, unsealedRumor)