mirror of
https://github.com/fiatjaf/nak.git
synced 2026-02-14 03:44:33 +00:00
Compare commits
4 Commits
a19a179548
...
v0.17.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81524de04f | ||
|
|
8334474f96 | ||
|
|
87f27e214e | ||
|
|
32999917b4 |
89
.github/workflows/release-cli.yml
vendored
89
.github/workflows/release-cli.yml
vendored
@@ -47,3 +47,92 @@ jobs:
|
||||
md5sum: false
|
||||
sha256sum: false
|
||||
compress_assets: false
|
||||
smoke-test-linux-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-all-for-all
|
||||
steps:
|
||||
- name: download and smoke test latest binary
|
||||
run: |
|
||||
set -eo pipefail # exit on error, and on pipe failures
|
||||
|
||||
echo "downloading nak binary from releases"
|
||||
RELEASE_URL="https://api.github.com/repos/fiatjaf/nak/releases/latest"
|
||||
wget $(wget -q -O - ${RELEASE_URL} | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url') -O nak -nv
|
||||
chmod +x nak
|
||||
|
||||
echo "printing version..."
|
||||
./nak --version
|
||||
|
||||
# generate and manipulate keys
|
||||
echo "testing key operations..."
|
||||
SECRET_KEY=$(./nak key generate)
|
||||
PUBLIC_KEY=$(echo $SECRET_KEY | ./nak key public)
|
||||
echo "generated key pair: $SECRET_KEY => $PUBLIC_KEY"
|
||||
|
||||
# create events
|
||||
echo "testing event creation..."
|
||||
./nak event -c "hello world"
|
||||
HELLOWORLD=$(./nak event -c "hello world")
|
||||
echo " hello world again: $HELLOWORLD"
|
||||
./nak event --ts "2 days ago" -c "event with timestamp"
|
||||
./nak event -k 1 -t "t=test" -c "event with tag"
|
||||
|
||||
# test NIP-19 encoding/decoding
|
||||
echo "testing NIP-19 encoding/decoding..."
|
||||
NSEC=$(echo $SECRET_KEY | ./nak encode nsec)
|
||||
echo "encoded nsec: $NSEC"
|
||||
./nak encode npub 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
|
||||
EVENT_ID="5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59"
|
||||
NEVENT1=$(./nak encode nevent $EVENT_ID)
|
||||
echo "encoded nevent1: $NEVENT1"
|
||||
./nak decode $NEVENT1
|
||||
./nak decode npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
|
||||
|
||||
# test event verification
|
||||
echo "testing event verification..."
|
||||
# create an event and verify it
|
||||
VERIFY_EVENT=$(./nak event -c "verify me")
|
||||
echo $VERIFY_EVENT | ./nak verify
|
||||
|
||||
# test PoW
|
||||
echo "testing pow..."
|
||||
./nak event -c "testing pow" --pow 8
|
||||
|
||||
# test NIP-49 key encryption/decryption
|
||||
echo "testing NIP-49 key encryption/decryption..."
|
||||
ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword")
|
||||
echo "encrypted key: ${ENCRYPTED_KEY:0:20}..."
|
||||
DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword")
|
||||
if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then
|
||||
echo "nip-49 encryption/decryption test failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# test multi-value tags
|
||||
echo "testing multi-value tags..."
|
||||
./nak event --ts "yesterday" -t "e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.example.com;root" -c "testing multi-value tags"
|
||||
|
||||
# test relay operations (with a public relay)
|
||||
echo "testing publishing..."
|
||||
# publish a simple event to a public relay
|
||||
EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "test from nak smoke test" nos.lol)
|
||||
EVENT_ID=$(echo $EVENT_JSON | jq -r .id)
|
||||
echo "published event ID: $EVENT_ID"
|
||||
|
||||
# wait a moment for propagation
|
||||
sleep 2
|
||||
|
||||
# fetch the event we just published
|
||||
./nak req -i $EVENT_ID nos.lol
|
||||
|
||||
# test serving (just start and immediately kill)
|
||||
echo "testing serve command..."
|
||||
timeout 2s ./nak serve || true
|
||||
|
||||
# test filesystem mount (just start and immediately kill)
|
||||
echo "testing fs mount command..."
|
||||
mkdir -p /tmp/nostr-mount
|
||||
timeout 2s ./nak fs --sec $SECRET_KEY /tmp/nostr-mount || true
|
||||
|
||||
echo "all tests passed"
|
||||
|
||||
97
.github/workflows/smoke-test-release.yml
vendored
97
.github/workflows/smoke-test-release.yml
vendored
@@ -1,97 +0,0 @@
|
||||
name: Smoke test the binary
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["build cli for all platforms"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
smoke-test-linux-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download and smoke test latest binary
|
||||
run: |
|
||||
set -eo pipefail # Exit on error, and on pipe failures
|
||||
|
||||
echo "Downloading nak binary from releases"
|
||||
RELEASE_URL="https://api.github.com/repos/fiatjaf/nak/releases/latest"
|
||||
wget $(wget -q -O - ${RELEASE_URL} | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url') -O nak -nv
|
||||
chmod +x nak
|
||||
|
||||
echo "Running basic tests..."
|
||||
./nak --version
|
||||
|
||||
# Generate and manipulate keys
|
||||
echo "Testing key operations..."
|
||||
SECRET_KEY=$(./nak key generate)
|
||||
PUBLIC_KEY=$(echo $SECRET_KEY | ./nak key public)
|
||||
echo "Generated key pair: $PUBLIC_KEY"
|
||||
|
||||
# Create events
|
||||
echo "Testing event creation..."
|
||||
./nak event -c "hello world"
|
||||
./nak event --ts "2 days ago" -c "event with timestamp"
|
||||
./nak event -k 1 -t "t=test" -c "event with tag"
|
||||
|
||||
# Test NIP-19 encoding/decoding
|
||||
echo "Testing NIP-19 encoding/decoding..."
|
||||
NSEC=$(echo $SECRET_KEY | ./nak encode nsec)
|
||||
echo "Encoded nsec: $NSEC"
|
||||
./nak encode npub 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
|
||||
NOTE_ID="5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59"
|
||||
NOTE1=$(./nak encode note $NOTE_ID)
|
||||
echo "Encoded note1: $NOTE1"
|
||||
./nak decode $NOTE1
|
||||
./nak decode npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
|
||||
|
||||
# Test event verification
|
||||
echo "Testing event verification..."
|
||||
# Create an event and verify it
|
||||
VERIFY_EVENT=$(./nak event -c "verify me")
|
||||
echo $VERIFY_EVENT | ./nak verify
|
||||
|
||||
# Test PoW
|
||||
echo "Testing PoW..."
|
||||
./nak event -c "testing pow" --pow 8
|
||||
|
||||
# Test NIP-49 key encryption/decryption
|
||||
echo "Testing NIP-49 key encryption/decryption..."
|
||||
ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword")
|
||||
echo "Encrypted key: ${ENCRYPTED_KEY:0:20}..."
|
||||
DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword")
|
||||
if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then
|
||||
echo "NIP-49 encryption/decryption test failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test multi-value tags
|
||||
echo "Testing multi-value tags..."
|
||||
./nak event --ts "yesterday" -t "e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.example.com;root" -c "Testing multi-value tags"
|
||||
|
||||
# Test relay operations (with a public relay)
|
||||
echo "Testing relay operations..."
|
||||
# Publish a simple event to a public relay
|
||||
EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "Test from nak smoke test" nos.lol)
|
||||
EVENT_ID=$(echo $EVENT_JSON | jq -r .id)
|
||||
echo "Published event ID: $EVENT_ID"
|
||||
|
||||
# Wait a moment for propagation
|
||||
sleep 2
|
||||
|
||||
# Fetch the event we just published
|
||||
./nak req -i $EVENT_ID nos.lol
|
||||
|
||||
# Test serving (just start and immediately kill)
|
||||
echo "Testing serve command..."
|
||||
timeout 2s ./nak serve || true
|
||||
|
||||
# Test filesystem mount (just start and immediately kill)
|
||||
echo "Testing fs mount command..."
|
||||
mkdir -p /tmp/nostr-mount
|
||||
timeout 2s ./nak fs --sec $SECRET_KEY /tmp/nostr-mount || true
|
||||
|
||||
echo "All tests passed"
|
||||
36
dekey.go
36
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]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
241
gift.go
241
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,29 @@ 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 <sec-a> -p <sec-b> | nak gift unwrap --sec <sec-b> --from <pub-a>`,
|
||||
nak event | nak gift wrap --sec <sec-a> -p <sec-b> | nak gift unwrap --sec <sec-b> --from <pub-a>
|
||||
|
||||
a decoupled key (if it has been created or received with "nak dekey" previously) will be used by default.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: defaultKeyFlags,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "wrap",
|
||||
Flags: append(
|
||||
defaultKeyFlags,
|
||||
Flags: []cli.Flag{
|
||||
&PubKeyFlag{
|
||||
Name: "recipient-pubkey",
|
||||
Aliases: []string{"p", "tgt", "target", "pubkey", "to"},
|
||||
Required: true,
|
||||
},
|
||||
),
|
||||
&cli.BoolFlag{
|
||||
Name: "use-our-identity-key",
|
||||
Usage: "Encrypt with the key given to --sec directly even when a decoupled key exists for the sender.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "use-their-identity-key",
|
||||
Usage: "Encrypt to the public key given as --recipient-pubkey directly even when a decoupled key exists for the receiver.",
|
||||
},
|
||||
},
|
||||
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 <my-secret-key> -p <target-public-key>`,
|
||||
@@ -38,14 +52,46 @@ 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 using bool
|
||||
|
||||
var cipher nostr.Cipher = kr
|
||||
// use decoupled key if it exists
|
||||
using = false
|
||||
if !c.Bool("use-our-identity-key") {
|
||||
configPath := c.String("config-path")
|
||||
eSec, has, err := getDecoupledEncryptionSecretKey(ctx, configPath, sender)
|
||||
if has {
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
cipher = keyer.NewPlainKeySigner(eSec)
|
||||
log("- using our decoupled encryption key %s\n", color.CyanString(eSec.Public().Hex()))
|
||||
using = true
|
||||
}
|
||||
}
|
||||
if !using {
|
||||
log("- using our identity key %s\n", color.CyanString(sender.Hex()))
|
||||
}
|
||||
|
||||
recipient := getPubKey(c, "recipient-pubkey")
|
||||
using = false
|
||||
if !c.Bool("use-their-identity-key") {
|
||||
if theirEPub, exists := getDecoupledEncryptionPublicKey(ctx, recipient); exists {
|
||||
recipient = theirEPub
|
||||
using = true
|
||||
log("- using their decoupled encryption public key %s\n", color.CyanString(theirEPub.Hex()))
|
||||
}
|
||||
}
|
||||
if !using {
|
||||
log("- using their identity public key %s\n", color.CyanString(recipient.Hex()))
|
||||
}
|
||||
|
||||
// read event from stdin
|
||||
for eventJSON := range getJsonsOrBlank() {
|
||||
if eventJSON == "{}" {
|
||||
@@ -65,7 +111,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)
|
||||
}
|
||||
@@ -114,22 +160,30 @@ var gift = &cli.Command{
|
||||
Name: "unwrap",
|
||||
Usage: "decrypts a gift-wrap event sent by the sender to us and exposes its internal rumor (unsigned event).",
|
||||
Description: `example:
|
||||
nak req -p <my-public-key> -k 1059 dmrelay.com | nak gift unwrap --sec <my-secret-key> --from <sender-public-key>`,
|
||||
Flags: append(
|
||||
defaultKeyFlags,
|
||||
&PubKeyFlag{
|
||||
Name: "sender-pubkey",
|
||||
Aliases: []string{"p", "src", "source", "pubkey", "from"},
|
||||
Required: true,
|
||||
},
|
||||
),
|
||||
nak req -p <my-public-key> -k 1059 dmrelay.com | nak gift unwrap --sec <my-secret-key>`,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sender := getPubKey(c, "sender-pubkey")
|
||||
// get receiver public key (ourselves)
|
||||
receiver, err := kr.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ciphers := []nostr.Cipher{kr}
|
||||
// use decoupled key if it exists
|
||||
configPath := c.String("config-path")
|
||||
eSec, has, err := getDecoupledEncryptionSecretKey(ctx, configPath, receiver)
|
||||
if has {
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
ciphers = append(ciphers, kr)
|
||||
ciphers[0] = keyer.NewPlainKeySigner(eSec) // pub decoupled key first
|
||||
}
|
||||
|
||||
// read gift-wrapped event from stdin
|
||||
for wrapJSON := range getJsonsOrBlank() {
|
||||
@@ -146,36 +200,79 @@ var gift = &cli.Command{
|
||||
return fmt.Errorf("not a gift wrap event (kind %d)", wrap.Kind)
|
||||
}
|
||||
|
||||
ephemeralPubkey := wrap.PubKey
|
||||
|
||||
// decrypt seal
|
||||
sealJSON, err := kr.Decrypt(ctx, wrap.Content, ephemeralPubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt seal: %w", err)
|
||||
}
|
||||
|
||||
// decrypt seal (in the process also find out if they encrypted it to our identity key or to our decoupled key)
|
||||
var cipher nostr.Cipher
|
||||
var seal nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(sealJSON), &seal); err != nil {
|
||||
return fmt.Errorf("invalid seal JSON: %w", err)
|
||||
|
||||
// try both the receiver identity key and decoupled key
|
||||
err = nil
|
||||
for c, potentialCipher := range ciphers {
|
||||
switch c {
|
||||
case 0:
|
||||
log("- trying the receiver's decoupled encryption key %s\n", color.CyanString(eSec.Public().Hex()))
|
||||
case 1:
|
||||
log("- trying the receiver's identity key %s\n", color.CyanString(receiver.Hex()))
|
||||
}
|
||||
|
||||
sealj, thisErr := potentialCipher.Decrypt(ctx, wrap.Content, wrap.PubKey)
|
||||
if thisErr != nil {
|
||||
err = thisErr
|
||||
continue
|
||||
}
|
||||
if thisErr := easyjson.Unmarshal([]byte(sealj), &seal); thisErr != nil {
|
||||
err = fmt.Errorf("invalid seal JSON: %w", thisErr)
|
||||
continue
|
||||
}
|
||||
|
||||
cipher = potentialCipher
|
||||
break
|
||||
}
|
||||
if seal.ID == nostr.ZeroID {
|
||||
// if both ciphers failed above we'll reach here
|
||||
return fmt.Errorf("failed to decrypt seal: %w", err)
|
||||
}
|
||||
|
||||
if seal.Kind != 13 {
|
||||
return fmt.Errorf("not a seal event (kind %d)", seal.Kind)
|
||||
}
|
||||
|
||||
// decrypt rumor
|
||||
rumorJSON, err := kr.Decrypt(ctx, seal.Content, sender)
|
||||
if err != nil {
|
||||
senderEncryptionPublicKeys := []nostr.PubKey{seal.PubKey}
|
||||
if theirEPub, exists := getDecoupledEncryptionPublicKey(ctx, seal.PubKey); exists {
|
||||
senderEncryptionPublicKeys = append(senderEncryptionPublicKeys, seal.PubKey)
|
||||
senderEncryptionPublicKeys[0] = theirEPub // put decoupled key first
|
||||
}
|
||||
|
||||
// decrypt rumor (at this point we know what cipher is the one they encrypted to)
|
||||
// (but we don't know if they have encrypted with their identity key or their decoupled key, so try both)
|
||||
var rumor nostr.Event
|
||||
err = nil
|
||||
for s, senderEncryptionPublicKey := range senderEncryptionPublicKeys {
|
||||
switch s {
|
||||
case 0:
|
||||
log("- trying the sender's decoupled encryption public key %s\n", color.CyanString(senderEncryptionPublicKey.Hex()))
|
||||
case 1:
|
||||
log("- trying the sender's identity public key %s\n", color.CyanString(senderEncryptionPublicKey.Hex()))
|
||||
}
|
||||
|
||||
rumorj, thisErr := cipher.Decrypt(ctx, seal.Content, senderEncryptionPublicKey)
|
||||
if thisErr != nil {
|
||||
err = fmt.Errorf("failed to decrypt rumor: %w", thisErr)
|
||||
continue
|
||||
}
|
||||
if thisErr := easyjson.Unmarshal([]byte(rumorj), &rumor); thisErr != nil {
|
||||
err = fmt.Errorf("invalid rumor JSON: %w", thisErr)
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if rumor.ID == nostr.ZeroID {
|
||||
return fmt.Errorf("failed to decrypt rumor: %w", err)
|
||||
}
|
||||
|
||||
var rumor nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(rumorJSON), &rumor); err != nil {
|
||||
return fmt.Errorf("invalid rumor JSON: %w", err)
|
||||
}
|
||||
|
||||
// output the unwrapped event (rumor)
|
||||
stdout(rumorJSON)
|
||||
stdout(rumor.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -190,3 +287,73 @@ func randomNow() nostr.Timestamp {
|
||||
randomOffset := rand.Int63n(twoDays)
|
||||
return nostr.Timestamp(now - randomOffset)
|
||||
}
|
||||
|
||||
func getDecoupledEncryptionSecretKey(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"})
|
||||
|
||||
keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""})
|
||||
if ok {
|
||||
var ePub nostr.PubKey
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
func getDecoupledEncryptionPublicKey(ctx context.Context, pubkey nostr.PubKey) (nostr.PubKey, bool) {
|
||||
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"})
|
||||
|
||||
keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""})
|
||||
if ok {
|
||||
var ePub nostr.PubKey
|
||||
|
||||
// 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 nostr.ZeroPK, false
|
||||
}
|
||||
|
||||
return ePub, true
|
||||
}
|
||||
|
||||
return nostr.ZeroPK, false
|
||||
}
|
||||
|
||||
4
git.go
4
git.go
@@ -277,6 +277,10 @@ aside from those, there is also:
|
||||
|
||||
// prompt for web URLs
|
||||
webURLs, err := promptForStringList("web URLs", config.Web, []string{
|
||||
fmt.Sprintf("https://viewsource.win/%s/%s",
|
||||
nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)),
|
||||
config.Identifier,
|
||||
),
|
||||
fmt.Sprintf("https://gitworkshop.dev/%s/%s",
|
||||
nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)),
|
||||
config.Identifier,
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.25
|
||||
|
||||
require (
|
||||
fiatjaf.com/lib v0.3.1
|
||||
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb
|
||||
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,7 +1,7 @@
|
||||
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
||||
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb h1:GuqPn1g0JRD/dGxFRxEwEFxvbcT3vyvMjP3OoeLIIh0=
|
||||
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6 h1:yH+cU9ZNgUdMCRa5eS3pmqTPP/QdZtSmQAIrN/U5nEc=
|
||||
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||
|
||||
Reference in New Issue
Block a user