NAME

Net::Nostr::GiftWrap - NIP-59 gift wrap encryption

SYNOPSIS

use Net::Nostr::GiftWrap;
use Net::Nostr::Key;

my $sender    = Net::Nostr::Key->new;
my $recipient = Net::Nostr::Key->new;

# Create an unsigned rumor
my $rumor = Net::Nostr::GiftWrap->create_rumor(
    pubkey  => $sender->pubkey_hex,
    kind    => 1,
    content => 'Are you going to the party tonight?',
    tags    => [],
);

# Seal and wrap in one step
my $wrap = Net::Nostr::GiftWrap->seal_and_wrap(
    rumor            => $rumor,
    sender_key       => $sender,
    recipient_pubkey => $recipient->pubkey_hex,
);

# Recipient unwraps
my ($unwrapped, $sender_pubkey) = Net::Nostr::GiftWrap->unwrap(
    event         => $wrap,
    recipient_key => $recipient,
);
say $unwrapped->content;  # "Are you going to the party tonight?"
say $sender_pubkey;       # sender's pubkey (from seal)

DESCRIPTION

Implements NIP-59 gift wrapping, a protocol for encapsulating any Nostr event to obscure metadata. Uses three layers:

Rumor - An unsigned event. Provides deniability if leaked.
Seal (kind 13) - Encrypts the rumor with the sender's key. Identifies the author without revealing content or recipient. Tags are always empty.
Gift wrap (kind 1059) - Encrypts the seal with a random one-time key. Includes a p tag for routing to the recipient.

Encryption uses NIP-44. Timestamps on seals and gift wraps are randomized up to two days in the past to thwart time-analysis attacks.

CLASS METHODS

create_rumor

my $rumor = Net::Nostr::GiftWrap->create_rumor(
    pubkey  => $pubkey_hex,
    kind    => 14,
    content => 'Hello!',
    tags    => [['p', $recipient_pubkey]],
);

Creates an unsigned event (rumor). Arguments are passed to Net::Nostr::Event->new and the signature is removed.

seal

my $seal = Net::Nostr::GiftWrap->seal(
    rumor            => $rumor,
    sender_key       => $sender,
    recipient_pubkey => $recipient->pubkey_hex,
);

say $seal->kind;                   # 13
say $seal->pubkey eq $sender->pubkey_hex;  # 1 (signed by sender)
say scalar @{$seal->tags};         # 0 (always empty)

Creates a kind 13 seal event. The rumor is JSON-encoded and encrypted with NIP-44 using the sender's private key and recipient's public key. The seal is signed by the sender. Tags are empty per the spec (except when expiration is explicitly requested, per NIP-17).

Optional arguments: created_at (override random timestamp), expiration (add expiration tag for disappearing messages).

wrap

my $wrap = Net::Nostr::GiftWrap->wrap(
    seal             => $seal,
    recipient_pubkey => $recipient->pubkey_hex,
);

say $wrap->kind;    # 1059
say $wrap->pubkey;  # random one-time-use pubkey (not the sender)

Creates a kind 1059 gift wrap event. The seal is JSON-encoded and encrypted with NIP-44 using a random one-time key. A p tag is added for the recipient.

Optional arguments: wrapper_key (override random key, for testing), created_at (override random timestamp), expiration (add expiration tag).

seal_and_wrap

my $wrap = Net::Nostr::GiftWrap->seal_and_wrap(
    rumor            => $rumor,
    sender_key       => $sender,
    recipient_pubkey => $recipient->pubkey_hex,
);

Convenience method that calls "seal" then "wrap".

Optional arguments: expiration (gift wrap expiration), seal_expiration (seal expiration).

Expiration example (disappearing messages):

my $wrap = Net::Nostr::GiftWrap->seal_and_wrap(
    rumor            => $rumor,
    sender_key       => $sender,
    recipient_pubkey => $recipient->pubkey_hex,
    expiration       => time() + 86400,
    seal_expiration  => time() + 86400,
);

unwrap

my ($unwrapped, $sender_pubkey) = Net::Nostr::GiftWrap->unwrap(
    event         => $gift_wrap_event,
    recipient_key => $recipient,
);

Decrypts a kind 1059 gift wrap event. Returns the rumor (unsigned event) and the sender's public key (from the seal). The caller should verify that $sender_pubkey matches $unwrapped->pubkey for authentication ("receive" in Net::Nostr::DirectMessage does this automatically).

Croaks if the event is not kind 1059 or the seal is not kind 13.

Trust boundary: This method only decrypts and parses the layered structure. It checks kind numbers but does not verify the gift wrap event's signature, event ID hash, or the seal event's signature. The returned rumor is unsigned by design (for deniability). Callers receiving gift wraps from untrusted sources should verify the outer event's signature before calling unwrap.

Multi-recipient wrapping

A single rumor can be wrapped individually for each recipient:

for my $pubkey (@recipient_pubkeys) {
    my $wrap = Net::Nostr::GiftWrap->seal_and_wrap(
        rumor            => $rumor,
        sender_key       => $sender,
        recipient_pubkey => $pubkey,
    );
    # publish $wrap to $pubkey's relays
}

The author can also retain an encrypted copy by wrapping to their own pubkey:

my $self_copy = Net::Nostr::GiftWrap->seal_and_wrap(
    rumor            => $rumor,
    sender_key       => $sender,
    recipient_pubkey => $sender->pubkey_hex,
);

SEE ALSO

NIP-59, Net::Nostr::Encryption, Net::Nostr::DirectMessage