Skip to content

Commit 81524de

Browse files
committed
gift: unwrap tries both decoupled and identity keys, wrap defaults to decoupled but accepts flags to change that.
1 parent 8334474 commit 81524de

3 files changed

Lines changed: 137 additions & 51 deletions

File tree

gift.go

Lines changed: 134 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,7 @@ var gift = &cli.Command{
2424
2525
a decoupled key (if it has been created or received with "nak dekey" previously) will be used by default.`,
2626
DisableSliceFlagSeparator: true,
27-
Flags: append(
28-
defaultKeyFlags,
29-
&cli.BoolFlag{
30-
Name: "use-direct",
31-
Usage: "Use the key given to --sec directly even when a decoupled key exists.",
32-
},
33-
),
27+
Flags: defaultKeyFlags,
3428
Commands: []*cli.Command{
3529
{
3630
Name: "wrap",
@@ -40,6 +34,14 @@ a decoupled key (if it has been created or received with "nak dekey" previously)
4034
Aliases: []string{"p", "tgt", "target", "pubkey", "to"},
4135
Required: true,
4236
},
37+
&cli.BoolFlag{
38+
Name: "use-our-identity-key",
39+
Usage: "Encrypt with the key given to --sec directly even when a decoupled key exists for the sender.",
40+
},
41+
&cli.BoolFlag{
42+
Name: "use-their-identity-key",
43+
Usage: "Encrypt to the public key given as --recipient-pubkey directly even when a decoupled key exists for the receiver.",
44+
},
4345
},
4446
Usage: "turns an event into a rumor (unsigned) then gift-wraps it to the recipient",
4547
Description: `example:
@@ -56,18 +58,39 @@ a decoupled key (if it has been created or received with "nak dekey" previously)
5658
return fmt.Errorf("failed to get sender pubkey: %w", err)
5759
}
5860

61+
var using bool
62+
5963
var cipher nostr.Cipher = kr
6064
// use decoupled key if it exists
61-
configPath := c.String("config-path")
62-
eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, sender)
63-
if has {
64-
if err != nil {
65-
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)
65+
using = false
66+
if !c.Bool("use-our-identity-key") {
67+
configPath := c.String("config-path")
68+
eSec, has, err := getDecoupledEncryptionSecretKey(ctx, configPath, sender)
69+
if has {
70+
if err != nil {
71+
return fmt.Errorf("our decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --encrypt-with-our-identity-key to bypass", err)
72+
}
73+
cipher = keyer.NewPlainKeySigner(eSec)
74+
log("- using our decoupled encryption key %s\n", color.CyanString(eSec.Public().Hex()))
75+
using = true
6676
}
67-
cipher = keyer.NewPlainKeySigner(eSec)
77+
}
78+
if !using {
79+
log("- using our identity key %s\n", color.CyanString(sender.Hex()))
6880
}
6981

7082
recipient := getPubKey(c, "recipient-pubkey")
83+
using = false
84+
if !c.Bool("use-their-identity-key") {
85+
if theirEPub, exists := getDecoupledEncryptionPublicKey(ctx, recipient); exists {
86+
recipient = theirEPub
87+
using = true
88+
log("- using their decoupled encryption public key %s\n", color.CyanString(theirEPub.Hex()))
89+
}
90+
}
91+
if !using {
92+
log("- using their identity public key %s\n", color.CyanString(recipient.Hex()))
93+
}
7194

7295
// read event from stdin
7396
for eventJSON := range getJsonsOrBlank() {
@@ -137,14 +160,7 @@ a decoupled key (if it has been created or received with "nak dekey" previously)
137160
Name: "unwrap",
138161
Usage: "decrypts a gift-wrap event sent by the sender to us and exposes its internal rumor (unsigned event).",
139162
Description: `example:
140-
nak req -p <my-public-key> -k 1059 dmrelay.com | nak gift unwrap --sec <my-secret-key> --from <sender-public-key>`,
141-
Flags: []cli.Flag{
142-
&PubKeyFlag{
143-
Name: "sender-pubkey",
144-
Aliases: []string{"p", "src", "source", "pubkey", "from"},
145-
Required: true,
146-
},
147-
},
163+
nak req -p <my-public-key> -k 1059 dmrelay.com | nak gift unwrap --sec <my-secret-key>`,
148164
Action: func(ctx context.Context, c *cli.Command) error {
149165
kr, _, err := gatherKeyerFromArguments(ctx, c)
150166
if err != nil {
@@ -157,19 +173,18 @@ a decoupled key (if it has been created or received with "nak dekey" previously)
157173
return err
158174
}
159175

160-
var cipher nostr.Cipher = kr
176+
ciphers := []nostr.Cipher{kr}
161177
// use decoupled key if it exists
162178
configPath := c.String("config-path")
163-
eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, receiver)
179+
eSec, has, err := getDecoupledEncryptionSecretKey(ctx, configPath, receiver)
164180
if has {
165181
if err != nil {
166-
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)
182+
return fmt.Errorf("our 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)
167183
}
168-
cipher = keyer.NewPlainKeySigner(eSec)
184+
ciphers = append(ciphers, kr)
185+
ciphers[0] = keyer.NewPlainKeySigner(eSec) // pub decoupled key first
169186
}
170187

171-
sender := getPubKey(c, "sender-pubkey")
172-
173188
// read gift-wrapped event from stdin
174189
for wrapJSON := range getJsonsOrBlank() {
175190
if wrapJSON == "{}" {
@@ -185,36 +200,79 @@ a decoupled key (if it has been created or received with "nak dekey" previously)
185200
return fmt.Errorf("not a gift wrap event (kind %d)", wrap.Kind)
186201
}
187202

188-
ephemeralPubkey := wrap.PubKey
203+
// decrypt seal (in the process also find out if they encrypted it to our identity key or to our decoupled key)
204+
var cipher nostr.Cipher
205+
var seal nostr.Event
189206

190-
// decrypt seal
191-
sealJSON, err := cipher.Decrypt(ctx, wrap.Content, ephemeralPubkey)
192-
if err != nil {
193-
return fmt.Errorf("failed to decrypt seal: %w", err)
207+
// try both the receiver identity key and decoupled key
208+
err = nil
209+
for c, potentialCipher := range ciphers {
210+
switch c {
211+
case 0:
212+
log("- trying the receiver's decoupled encryption key %s\n", color.CyanString(eSec.Public().Hex()))
213+
case 1:
214+
log("- trying the receiver's identity key %s\n", color.CyanString(receiver.Hex()))
215+
}
216+
217+
sealj, thisErr := potentialCipher.Decrypt(ctx, wrap.Content, wrap.PubKey)
218+
if thisErr != nil {
219+
err = thisErr
220+
continue
221+
}
222+
if thisErr := easyjson.Unmarshal([]byte(sealj), &seal); thisErr != nil {
223+
err = fmt.Errorf("invalid seal JSON: %w", thisErr)
224+
continue
225+
}
226+
227+
cipher = potentialCipher
228+
break
194229
}
195-
196-
var seal nostr.Event
197-
if err := easyjson.Unmarshal([]byte(sealJSON), &seal); err != nil {
198-
return fmt.Errorf("invalid seal JSON: %w", err)
230+
if seal.ID == nostr.ZeroID {
231+
// if both ciphers failed above we'll reach here
232+
return fmt.Errorf("failed to decrypt seal: %w", err)
199233
}
200234

201235
if seal.Kind != 13 {
202236
return fmt.Errorf("not a seal event (kind %d)", seal.Kind)
203237
}
204238

205-
// decrypt rumor
206-
rumorJSON, err := cipher.Decrypt(ctx, seal.Content, sender)
207-
if err != nil {
208-
return fmt.Errorf("failed to decrypt rumor: %w", err)
239+
senderEncryptionPublicKeys := []nostr.PubKey{seal.PubKey}
240+
if theirEPub, exists := getDecoupledEncryptionPublicKey(ctx, seal.PubKey); exists {
241+
senderEncryptionPublicKeys = append(senderEncryptionPublicKeys, seal.PubKey)
242+
senderEncryptionPublicKeys[0] = theirEPub // put decoupled key first
209243
}
210244

245+
// decrypt rumor (at this point we know what cipher is the one they encrypted to)
246+
// (but we don't know if they have encrypted with their identity key or their decoupled key, so try both)
211247
var rumor nostr.Event
212-
if err := easyjson.Unmarshal([]byte(rumorJSON), &rumor); err != nil {
213-
return fmt.Errorf("invalid rumor JSON: %w", err)
248+
err = nil
249+
for s, senderEncryptionPublicKey := range senderEncryptionPublicKeys {
250+
switch s {
251+
case 0:
252+
log("- trying the sender's decoupled encryption public key %s\n", color.CyanString(senderEncryptionPublicKey.Hex()))
253+
case 1:
254+
log("- trying the sender's identity public key %s\n", color.CyanString(senderEncryptionPublicKey.Hex()))
255+
}
256+
257+
rumorj, thisErr := cipher.Decrypt(ctx, seal.Content, senderEncryptionPublicKey)
258+
if thisErr != nil {
259+
err = fmt.Errorf("failed to decrypt rumor: %w", thisErr)
260+
continue
261+
}
262+
if thisErr := easyjson.Unmarshal([]byte(rumorj), &rumor); thisErr != nil {
263+
err = fmt.Errorf("invalid rumor JSON: %w", thisErr)
264+
continue
265+
}
266+
267+
break
268+
}
269+
270+
if rumor.ID == nostr.ZeroID {
271+
return fmt.Errorf("failed to decrypt rumor: %w", err)
214272
}
215273

216274
// output the unwrapped event (rumor)
217-
stdout(rumorJSON)
275+
stdout(rumor.String())
218276
}
219277

220278
return nil
@@ -230,18 +288,18 @@ func randomNow() nostr.Timestamp {
230288
return nostr.Timestamp(now - randomOffset)
231289
}
232290

233-
func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey nostr.PubKey) (nostr.SecretKey, bool, error) {
291+
func getDecoupledEncryptionSecretKey(ctx context.Context, configPath string, pubkey nostr.PubKey) (nostr.SecretKey, bool, error) {
234292
relays := sys.FetchWriteRelays(ctx, pubkey)
235293

236294
keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
237295
Kinds: []nostr.Kind{10044},
238296
Authors: []nostr.PubKey{pubkey},
239297
}, nostr.SubscriptionOptions{Label: "nak-nip4e-gift"})
240-
var eSec nostr.SecretKey
241-
var ePub nostr.PubKey
242298

243299
keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""})
244300
if ok {
301+
var ePub nostr.PubKey
302+
245303
// get the pub from the tag
246304
for _, tag := range keyAnnouncementEvent.Tags {
247305
if len(tag) >= 2 && tag[0] == "n" {
@@ -256,8 +314,7 @@ func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey no
256314
// check if we have the key
257315
eKeyPath := filepath.Join(configPath, "dekey", "p", pubkey.Hex(), "e", ePub.Hex())
258316
if data, err := os.ReadFile(eKeyPath); err == nil {
259-
log(color.GreenString("- and we have it locally already\n"))
260-
eSec, err = nostr.SecretKeyFromHex(string(data))
317+
eSec, err := nostr.SecretKeyFromHex(string(data))
261318
if err != nil {
262319
return [32]byte{}, true, fmt.Errorf("invalid main key: %w", err)
263320
}
@@ -271,3 +328,32 @@ func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey no
271328

272329
return [32]byte{}, false, nil
273330
}
331+
332+
func getDecoupledEncryptionPublicKey(ctx context.Context, pubkey nostr.PubKey) (nostr.PubKey, bool) {
333+
relays := sys.FetchWriteRelays(ctx, pubkey)
334+
335+
keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
336+
Kinds: []nostr.Kind{10044},
337+
Authors: []nostr.PubKey{pubkey},
338+
}, nostr.SubscriptionOptions{Label: "nak-nip4e-gift"})
339+
340+
keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""})
341+
if ok {
342+
var ePub nostr.PubKey
343+
344+
// get the pub from the tag
345+
for _, tag := range keyAnnouncementEvent.Tags {
346+
if len(tag) >= 2 && tag[0] == "n" {
347+
ePub, _ = nostr.PubKeyFromHex(tag[1])
348+
break
349+
}
350+
}
351+
if ePub == nostr.ZeroPK {
352+
return nostr.ZeroPK, false
353+
}
354+
355+
return ePub, true
356+
}
357+
358+
return nostr.ZeroPK, false
359+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25
44

55
require (
66
fiatjaf.com/lib v0.3.1
7-
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb
7+
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6
88
github.com/AlecAivazis/survey/v2 v2.3.7
99
github.com/bep/debounce v1.2.1
1010
github.com/btcsuite/btcd/btcec/v2 v2.3.6

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
22
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
3-
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb h1:GuqPn1g0JRD/dGxFRxEwEFxvbcT3vyvMjP3OoeLIIh0=
4-
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
3+
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6 h1:yH+cU9ZNgUdMCRa5eS3pmqTPP/QdZtSmQAIrN/U5nEc=
4+
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
55
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
66
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
77
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=

0 commit comments

Comments
 (0)