From 87f27e214ebd25dcedc35a995a238303a7d31e4e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 28 Dec 2025 12:49:53 -0300 Subject: [PATCH] use dekey by default on gift wrap and unwrap. --- dekey.go | 36 +++++++++---------- gift.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 112 insertions(+), 31 deletions(-) diff --git a/dekey.go b/dekey.go index f7be06f..5c7fdf3 100644 --- a/dekey.go +++ b/dekey.go @@ -33,16 +33,16 @@ var dekey = &cli.Command{ }, &cli.BoolFlag{ Name: "rotate", - Usage: "force the creation of a new encryption key, effectively invalidating any previous ones", + Usage: "force the creation of a new decoupled encryption key, effectively invalidating any previous ones", }, &cli.BoolFlag{ Name: "authorize-all", Aliases: []string{"yolo"}, - Usage: "do not ask for confirmation, just automatically send the encryption key to all devices that exist", + Usage: "do not ask for confirmation, just automatically send the decoupled encryption key to all devices that exist", }, &cli.BoolFlag{ Name: "reject-all", - Usage: "do not ask for confirmation, just not send the encryption key to any device", + Usage: "do not ask for confirmation, just not send the decoupled encryption key to any device", }, ), Action: func(ctx context.Context, c *cli.Command) error { @@ -93,7 +93,7 @@ var dekey = &cli.Command{ } // check for kind:10044 - log("- checking for user encryption key (kind:10044)\n") + log("- checking for decoupled encryption key (kind:10044)\n") keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{10044}, Authors: []nostr.PubKey{userPub}, @@ -104,7 +104,7 @@ var dekey = &cli.Command{ var generateNewEncryptionKey bool keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}) if !ok { - log("- no user encryption key found, generating new one\n") + log("- no decoupled encryption key found, generating new one\n") generateNewEncryptionKey = true } else { // get the pub from the tag @@ -118,7 +118,7 @@ var dekey = &cli.Command{ return fmt.Errorf("got invalid kind:10044 event, no 'n' tag") } - log(". an encryption public key already exists: %s\n", color.CyanString(ePub.Hex())) + log(". a decoupled encryption public key already exists: %s\n", color.CyanString(ePub.Hex())) if c.Bool("rotate") { log(color.GreenString("rotating it by generating a new one\n")) generateNewEncryptionKey = true @@ -134,12 +134,12 @@ var dekey = &cli.Command{ eKeyPath := filepath.Join(configPath, "dekey", "p", userPub.Hex(), "e", ePub.Hex()) os.MkdirAll(filepath.Dir(eKeyPath), 0700) if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil { - return fmt.Errorf("failed to write user encryption key: %w", err) + return fmt.Errorf("failed to write decoupled encryption key: %w", err) } - log("user encryption key generated and stored, public key: %s\n", color.CyanString(ePub.Hex())) + log("decoupled encryption key generated and stored, public key: %s\n", color.CyanString(ePub.Hex())) // publish kind:10044 - log("publishing user encryption public key (kind:10044)\n") + log("publishing decoupled encryption public key (kind:10044)\n") evt10044 := nostr.Event{ Kind: 10044, Content: "", @@ -164,10 +164,10 @@ var dekey = &cli.Command{ return fmt.Errorf("invalid main key: %w", err) } if eSec.Public() != ePub { - return fmt.Errorf("stored user encryption key is corrupted: %w", err) + return fmt.Errorf("stored decoupled encryption key is corrupted: %w", err) } } else { - log("- encryption key not found locally, attempting to fetch the key from other devices\n") + log("- decoupled encryption key not found locally, attempting to fetch the key from other devices\n") // check if our kind:4454 is already published log("- checking for existing device announcement (kind:4454)\n") @@ -242,14 +242,14 @@ var dekey = &cli.Command{ } // check if it matches mainPub if eSec.Public() == ePub { - log(color.GreenString("successfully decrypted encryption key from another device\n")) + log(color.GreenString("successfully received decoupled encryption key from another device\n")) // store it os.MkdirAll(filepath.Dir(eKeyPath), 0700) os.WriteFile(eKeyPath, []byte(eSecHex), 0600) // delete our 4454 if we had one, since we received the key if len(ourDeviceAnnouncementEvents) > 0 { - log("deleting our device announcement (kind:4454) since we received the encryption key\n") + log("deleting our device announcement (kind:4454) since we received the decoupled encryption key\n") deletion4454 := nostr.Event{ CreatedAt: nostr.Now(), Kind: 5, @@ -290,11 +290,11 @@ var dekey = &cli.Command{ } if eSec == [32]byte{} { - log("encryption secret key not available, must be sent from another device to %s first\n", + log("decoupled encryption secret key not available, must be sent from another device to %s first\n", color.YellowString(deviceName)) return nil } - log(color.GreenString("- encryption key ready\n")) + log(color.GreenString("- decoupled encryption key ready\n")) // now we have mainSec, check for other kind:4454 events newer than the 10044 log("- checking for other devices and key messages so we can send the key\n") @@ -359,7 +359,7 @@ var dekey = &cli.Command{ } else { var proceed bool if err := survey.AskOne(&survey.Confirm{ - Message: fmt.Sprintf("share encryption key with %s"+colors.bold("?"), + Message: fmt.Sprintf("share decoupled encryption key with %s"+colors.bold("?"), color.YellowString(deviceTag[1])), }, &proceed); err != nil { return err @@ -398,7 +398,7 @@ var dekey = &cli.Command{ } } - log("- sending encryption key to new device %s\n", color.YellowString(deviceTag[1])) + log("- sending decoupled encryption key to new device %s\n", color.YellowString(deviceTag[1])) ss, err := nip44.GenerateConversationKey(theirDevice, deviceSec) if err != nil { continue @@ -424,7 +424,7 @@ var dekey = &cli.Command{ if err := publishFlow(ctx, c, kr, evt4455, relayList); err != nil { log(color.RedString("failed to publish key message: %v\n"), err) } else { - log(" - encryption key sent to %s\n", color.GreenString(deviceTag[1])) + log(" - decoupled encryption key sent to %s\n", color.GreenString(deviceTag[1])) } } } diff --git a/gift.go b/gift.go index dbb6097..a007a0d 100644 --- a/gift.go +++ b/gift.go @@ -4,10 +4,14 @@ import ( "context" "fmt" "math/rand" + "os" + "path/filepath" "time" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/keyer" "fiatjaf.com/nostr/nip44" + "github.com/fatih/color" "github.com/mailru/easyjson" "github.com/urfave/cli/v3" ) @@ -16,19 +20,27 @@ var gift = &cli.Command{ Name: "gift", Usage: "gift-wraps (or unwraps) an event according to NIP-59", Description: `example: - nak event | nak gift wrap --sec -p | nak gift unwrap --sec --from `, + nak event | nak gift wrap --sec -p | nak gift unwrap --sec --from + +a decoupled key (if it has been created or received with "nak dekey" previously) will be used by default.`, DisableSliceFlagSeparator: true, + Flags: append( + defaultKeyFlags, + &cli.BoolFlag{ + Name: "use-direct", + Usage: "Use the key given to --sec directly even when a decoupled key exists.", + }, + ), Commands: []*cli.Command{ { Name: "wrap", - Flags: append( - defaultKeyFlags, + Flags: []cli.Flag{ &PubKeyFlag{ Name: "recipient-pubkey", Aliases: []string{"p", "tgt", "target", "pubkey", "to"}, Required: true, }, - ), + }, Usage: "turns an event into a rumor (unsigned) then gift-wraps it to the recipient", Description: `example: nak event -c 'hello' | nak gift wrap --sec -p `, @@ -38,14 +50,25 @@ var gift = &cli.Command{ return err } - recipient := getPubKey(c, "recipient-pubkey") - - // get sender pubkey + // get sender pubkey (ourselves) sender, err := kr.GetPublicKey(ctx) if err != nil { return fmt.Errorf("failed to get sender pubkey: %w", err) } + var cipher nostr.Cipher = kr + // use decoupled key if it exists + configPath := c.String("config-path") + eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, sender) + if has { + if err != nil { + return fmt.Errorf("decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --use-direct to bypass", err) + } + cipher = keyer.NewPlainKeySigner(eSec) + } + + recipient := getPubKey(c, "recipient-pubkey") + // read event from stdin for eventJSON := range getJsonsOrBlank() { if eventJSON == "{}" { @@ -65,7 +88,7 @@ var gift = &cli.Command{ // create seal rumorJSON, _ := easyjson.Marshal(rumor) - encryptedRumor, err := kr.Encrypt(ctx, string(rumorJSON), recipient) + encryptedRumor, err := cipher.Encrypt(ctx, string(rumorJSON), recipient) if err != nil { return fmt.Errorf("failed to encrypt rumor: %w", err) } @@ -115,20 +138,36 @@ var gift = &cli.Command{ Usage: "decrypts a gift-wrap event sent by the sender to us and exposes its internal rumor (unsigned event).", Description: `example: nak req -p -k 1059 dmrelay.com | nak gift unwrap --sec --from `, - Flags: append( - defaultKeyFlags, + Flags: []cli.Flag{ &PubKeyFlag{ Name: "sender-pubkey", Aliases: []string{"p", "src", "source", "pubkey", "from"}, Required: true, }, - ), + }, Action: func(ctx context.Context, c *cli.Command) error { kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } + // get receiver public key (ourselves) + receiver, err := kr.GetPublicKey(ctx) + if err != nil { + return err + } + + var cipher nostr.Cipher = kr + // use decoupled key if it exists + configPath := c.String("config-path") + eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, receiver) + if has { + if err != nil { + return fmt.Errorf("decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --use-direct to bypass", err) + } + cipher = keyer.NewPlainKeySigner(eSec) + } + sender := getPubKey(c, "sender-pubkey") // read gift-wrapped event from stdin @@ -149,7 +188,7 @@ var gift = &cli.Command{ ephemeralPubkey := wrap.PubKey // decrypt seal - sealJSON, err := kr.Decrypt(ctx, wrap.Content, ephemeralPubkey) + sealJSON, err := cipher.Decrypt(ctx, wrap.Content, ephemeralPubkey) if err != nil { return fmt.Errorf("failed to decrypt seal: %w", err) } @@ -164,7 +203,7 @@ var gift = &cli.Command{ } // decrypt rumor - rumorJSON, err := kr.Decrypt(ctx, seal.Content, sender) + rumorJSON, err := cipher.Decrypt(ctx, seal.Content, sender) if err != nil { return fmt.Errorf("failed to decrypt rumor: %w", err) } @@ -190,3 +229,45 @@ func randomNow() nostr.Timestamp { randomOffset := rand.Int63n(twoDays) return nostr.Timestamp(now - randomOffset) } + +func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey nostr.PubKey) (nostr.SecretKey, bool, error) { + relays := sys.FetchWriteRelays(ctx, pubkey) + + keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{10044}, + Authors: []nostr.PubKey{pubkey}, + }, nostr.SubscriptionOptions{Label: "nak-nip4e-gift"}) + var eSec nostr.SecretKey + var ePub nostr.PubKey + + keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""}) + if ok { + // get the pub from the tag + for _, tag := range keyAnnouncementEvent.Tags { + if len(tag) >= 2 && tag[0] == "n" { + ePub, _ = nostr.PubKeyFromHex(tag[1]) + break + } + } + if ePub == nostr.ZeroPK { + return [32]byte{}, true, fmt.Errorf("got invalid kind:10044 event, no 'n' tag") + } + + // check if we have the key + eKeyPath := filepath.Join(configPath, "dekey", "p", pubkey.Hex(), "e", ePub.Hex()) + if data, err := os.ReadFile(eKeyPath); err == nil { + log(color.GreenString("- and we have it locally already\n")) + eSec, err = nostr.SecretKeyFromHex(string(data)) + if err != nil { + return [32]byte{}, true, fmt.Errorf("invalid main key: %w", err) + } + if eSec.Public() != ePub { + return [32]byte{}, true, fmt.Errorf("stored decoupled encryption key is corrupted: %w", err) + } + + return eSec, true, nil + } + } + + return [32]byte{}, false, nil +}