mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
2 Commits
1dab81f77c
...
4b8b6bb3de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b8b6bb3de | ||
|
|
df491be232 |
282
dekey.go
Normal file
282
dekey.go
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip44"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dekey = &cli.Command{
|
||||||
|
Name: "dekey",
|
||||||
|
Usage: "handles NIP-4E decoupled encryption keys",
|
||||||
|
Description: "maybe this picture will explain better than I can do here for now: https://cdn.azzamo.net/89c543d261ad0d665c1dea78f91e527c2e39e7fe503b440265a3c47e63c9139f.png",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(defaultKeyFlags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "device-name",
|
||||||
|
Usage: "name of this device that will be published and displayed on other clients",
|
||||||
|
Value: func() string {
|
||||||
|
if hostname, err := os.Hostname(); err == nil {
|
||||||
|
return "nak@" + hostname
|
||||||
|
}
|
||||||
|
return "nak@unknown"
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userPub, err := kr.GetPublicKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := c.String("config-path")
|
||||||
|
deviceName := c.String("device-name")
|
||||||
|
|
||||||
|
// check if we already have a local-device secret key
|
||||||
|
deviceKeyPath := filepath.Join(configPath, "dekey", "device-key")
|
||||||
|
var deviceSec nostr.SecretKey
|
||||||
|
if data, err := os.ReadFile(deviceKeyPath); err == nil {
|
||||||
|
deviceSec, err = nostr.SecretKeyFromHex(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid device key in %s: %w", deviceKeyPath, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// create one
|
||||||
|
deviceSec = nostr.Generate()
|
||||||
|
os.MkdirAll(filepath.Dir(deviceKeyPath), 0700)
|
||||||
|
if err := os.WriteFile(deviceKeyPath, []byte(deviceSec.Hex()), 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write device key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
devicePub := deviceSec.Public()
|
||||||
|
|
||||||
|
// get relays for the user
|
||||||
|
relays := sys.FetchWriteRelays(ctx, userPub)
|
||||||
|
relayList := connectToAllRelays(ctx, c, relays, nil, nostr.PoolOptions{})
|
||||||
|
if len(relayList) == 0 {
|
||||||
|
return fmt.Errorf("no relays to use")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if kind:4454 is already published
|
||||||
|
events := sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{4454},
|
||||||
|
Authors: []nostr.PubKey{userPub},
|
||||||
|
Tags: nostr.TagMap{
|
||||||
|
"pubkey": []string{devicePub.Hex()},
|
||||||
|
},
|
||||||
|
}, nostr.SubscriptionOptions{Label: "nak-nip4e"})
|
||||||
|
if len(events) == 0 {
|
||||||
|
// publish kind:4454
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 4454,
|
||||||
|
Content: "",
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"client", deviceName},
|
||||||
|
{"pubkey", devicePub.Hex()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign with main key
|
||||||
|
if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||||
|
return fmt.Errorf("failed to sign device event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish
|
||||||
|
if err := publishFlow(ctx, c, kr, evt, relayList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for kind:10044
|
||||||
|
userKeyEventDate := nostr.Now()
|
||||||
|
userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{10044},
|
||||||
|
Authors: []nostr.PubKey{userPub},
|
||||||
|
}, nostr.SubscriptionOptions{Label: "nak-nip4e"})
|
||||||
|
var eSec nostr.SecretKey
|
||||||
|
var ePub nostr.PubKey
|
||||||
|
if userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}); !ok {
|
||||||
|
// generate main secret key
|
||||||
|
eSec = nostr.Generate()
|
||||||
|
ePub := eSec.Public()
|
||||||
|
|
||||||
|
// store it
|
||||||
|
eKeyPath := filepath.Join(configPath, "dekey", "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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish kind:10044
|
||||||
|
evt10044 := nostr.Event{
|
||||||
|
Kind: 10044,
|
||||||
|
Content: "",
|
||||||
|
CreatedAt: userKeyEventDate,
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"n", ePub.Hex()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := kr.SignEvent(ctx, &evt10044); err != nil {
|
||||||
|
return fmt.Errorf("failed to sign kind:10044: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := publishFlow(ctx, c, kr, evt10044, relayList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userKeyEventDate = userKeyEvent.CreatedAt
|
||||||
|
|
||||||
|
// get the pub from the tag
|
||||||
|
for _, tag := range userKeyEvent.Tags {
|
||||||
|
if len(tag) >= 2 && tag[0] == "n" {
|
||||||
|
ePub, _ = nostr.PubKeyFromHex(tag[1])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ePub == nostr.ZeroPK {
|
||||||
|
return fmt.Errorf("invalid kind:10044 event, no 'n' tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have the key
|
||||||
|
eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex())
|
||||||
|
if data, err := os.ReadFile(eKeyPath); err == nil {
|
||||||
|
eSec, err = nostr.SecretKeyFromHex(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid main key: %w", err)
|
||||||
|
}
|
||||||
|
if eSec.Public() != ePub {
|
||||||
|
return fmt.Errorf("stored user encryption key is corrupted: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// try to decrypt from kind:4455
|
||||||
|
for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{4455},
|
||||||
|
Tags: nostr.TagMap{
|
||||||
|
"p": []string{devicePub.Hex()},
|
||||||
|
},
|
||||||
|
}, nostr.SubscriptionOptions{Label: "nak-nip4e"}) {
|
||||||
|
var senderPub nostr.PubKey
|
||||||
|
for _, tag := range eKeyMsg.Tags {
|
||||||
|
if len(tag) >= 2 && tag[0] == "P" {
|
||||||
|
senderPub, _ = nostr.PubKeyFromHex(tag[1])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if senderPub == nostr.ZeroPK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ss, err := nip44.GenerateConversationKey(senderPub, deviceSec)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eSecHex, err := nip44.Decrypt(eKeyMsg.Content, ss)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eSec, err = nostr.SecretKeyFromHex(eSecHex)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// check if it matches mainPub
|
||||||
|
if eSec.Public() == ePub {
|
||||||
|
// store it
|
||||||
|
os.MkdirAll(filepath.Dir(eKeyPath), 0700)
|
||||||
|
os.WriteFile(eKeyPath, []byte(eSecHex), 0600)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if eSec == [32]byte{} {
|
||||||
|
log("main secret key not available, must authorize on another device\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we have mainSec, check for other kind:4454 events newer than the 10044
|
||||||
|
keyMsgs := make([]string, 0, 5)
|
||||||
|
for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{4454, 4455},
|
||||||
|
Authors: []nostr.PubKey{userPub},
|
||||||
|
Since: userKeyEventDate,
|
||||||
|
}, nostr.SubscriptionOptions{Label: "nak-nip4e"}) {
|
||||||
|
if keyOrDeviceEvt.Kind == 4455 {
|
||||||
|
// key event
|
||||||
|
|
||||||
|
// skip ourselves
|
||||||
|
if keyOrDeviceEvt.Tags.FindWithValue("p", devicePub.Hex()) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// assume a key msg will always come before its associated devicemsg
|
||||||
|
// so just store them here:
|
||||||
|
pubkeyTag := keyOrDeviceEvt.Tags.Find("p")
|
||||||
|
if pubkeyTag == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyMsgs = append(keyMsgs, pubkeyTag[1])
|
||||||
|
} else if keyOrDeviceEvt.Kind == 4454 {
|
||||||
|
// device event
|
||||||
|
|
||||||
|
// skip ourselves
|
||||||
|
if keyOrDeviceEvt.Tags.FindWithValue("pubkey", devicePub.Hex()) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this already has a corresponding keyMsg then skip it
|
||||||
|
pubkeyTag := keyOrDeviceEvt.Tags.Find("pubkey")
|
||||||
|
if pubkeyTag == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slices.Contains(keyMsgs, pubkeyTag[1]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we know we're dealing with a deviceMsg without a corresponding keyMsg
|
||||||
|
// so we have to build a keyMsg for them
|
||||||
|
theirDevice, err := nostr.PubKeyFromHex(pubkeyTag[1])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ss, err := nip44.GenerateConversationKey(theirDevice, deviceSec)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ciphertext, err := nip44.Encrypt(eSec.Hex(), ss)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
evt4455 := nostr.Event{
|
||||||
|
Kind: 4455,
|
||||||
|
Content: ciphertext,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"p", theirDevice.Hex()},
|
||||||
|
{"P", devicePub.Hex()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := kr.SignEvent(ctx, &evt4455); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
publishFlow(ctx, c, kr, evt4455, relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
1
main.go
1
main.go
@@ -40,6 +40,7 @@ var app = &cli.Command{
|
|||||||
bunker,
|
bunker,
|
||||||
serve,
|
serve,
|
||||||
blossomCmd,
|
blossomCmd,
|
||||||
|
dekey,
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
gift,
|
gift,
|
||||||
|
|||||||
17
serve.go
17
serve.go
@@ -51,6 +51,12 @@ var serve = &cli.Command{
|
|||||||
Name: "grasp",
|
Name: "grasp",
|
||||||
Usage: "enable grasp server",
|
Usage: "enable grasp server",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "grasp-path",
|
||||||
|
Usage: "where to store the repositories",
|
||||||
|
TakesFile: true,
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "blossom",
|
Name: "blossom",
|
||||||
Usage: "enable blossom server",
|
Usage: "enable blossom server",
|
||||||
@@ -135,10 +141,13 @@ var serve = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.Bool("grasp") {
|
if c.Bool("grasp") {
|
||||||
var err error
|
repoDir = c.String("grasp-path")
|
||||||
repoDir, err = os.MkdirTemp("", "nak-serve-grasp-repos-")
|
if repoDir == "" {
|
||||||
if err != nil {
|
var err error
|
||||||
return fmt.Errorf("failed to create grasp repos directory: %w", err)
|
repoDir, err = os.MkdirTemp("", "nak-serve-grasp-repos-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create grasp repos directory: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
g := grasp.New(rl, repoDir)
|
g := grasp.New(rl, repoDir)
|
||||||
g.OnRead = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) {
|
g.OnRead = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user