diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/nymdrop-reader/gui.go | 135 | ||||
| -rw-r--r-- | cmd/nymdrop-reader/main.go | 476 | ||||
| -rw-r--r-- | cmd/nymdrop-reader/memguard_verify_test.go | 64 |
3 files changed, 181 insertions, 494 deletions
diff --git a/cmd/nymdrop-reader/gui.go b/cmd/nymdrop-reader/gui.go new file mode 100644 index 0000000..d8e5383 --- /dev/null +++ b/cmd/nymdrop-reader/gui.go @@ -0,0 +1,135 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + + "nymdrop/internal/reader" +) + +// submissionEntry is one row in the submissions list: enough to render the +// row (label) and to load the full body on demand (path) without keeping +// every decrypted body in memory at once. +type submissionEntry struct { + label string + path string +} + +// runGUI is the non-technical front-end for nymdrop-reader: a journalist +// running this from a USB stick should never need a terminal. It drives the +// same reader.Instance the CLI uses, through the same Hooks contract, so +// there is exactly one implementation of the receive/decrypt/save logic. +func runGUI(cfg reader.Config) { + a := app.NewWithID("art.virebent.nymdrop-reader") + w := a.NewWindow("NymDrop Reader") + w.Resize(fyne.NewSize(900, 560)) + + statusLabel := widget.NewLabel("Starting...") + statusLabel.Wrapping = fyne.TextWrapWord + + addrEntry := widget.NewEntry() + addrEntry.Disable() + addrEntry.SetPlaceHolder("Nym inbox address will appear here once connected") + + outDirLabel := widget.NewLabel("") + + var submissions []submissionEntry + + content := widget.NewMultiLineEntry() + content.Wrapping = fyne.TextWrapWord + content.Disable() + + list := widget.NewList( + func() int { return len(submissions) }, + func() fyne.CanvasObject { return widget.NewLabel("template") }, + func(id widget.ListItemID, obj fyne.CanvasObject) { + obj.(*widget.Label).SetText(submissions[id].label) + }, + ) + list.OnSelected = func(id widget.ListItemID) { + if id < 0 || id >= len(submissions) { + return + } + data, err := os.ReadFile(submissions[id].path) + if err != nil { + content.SetText(fmt.Sprintf("error reading %s: %v", submissions[id].path, err)) + return + } + content.SetText(string(data)) + } + + hooks := reader.Hooks{ + Log: func(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + fyne.Do(func() { statusLabel.SetText(msg) }) + }, + SelfAddress: func(addr string) { + fyne.Do(func() { + addrEntry.SetText(addr) + statusLabel.SetText("Connected -- waiting for submissions.") + }) + }, + Submission: func(path, preview string) { + ts := strings.TrimSuffix(filepath.Base(path), ".txt") + oneLine := strings.ReplaceAll(preview, "\n", " ") + if len(oneLine) > 80 { + oneLine = oneLine[:80] + "..." + } + entry := submissionEntry{label: ts + " " + oneLine, path: path} + fyne.Do(func() { + // Newest first: a journalist checking the box wants the + // latest tip on top, not scrolled to the bottom. + submissions = append([]submissionEntry{entry}, submissions...) + list.Refresh() + statusLabel.SetText(fmt.Sprintf("%d submission(s) received.", len(submissions))) + }) + }, + Attachment: func(path string, size int) { + fyne.Do(func() { + statusLabel.SetText(fmt.Sprintf("attachment saved: %s (%d bytes)", path, size)) + }) + }, + Error: func(err error) { + fyne.Do(func() { statusLabel.SetText("error: " + err.Error()) }) + }, + } + + in, err := reader.Start(cfg, hooks) + if err != nil { + statusLabel.SetText("failed to start: " + err.Error()) + } else { + outDirLabel.SetText("Submissions saved to: " + in.OutDir()) + go in.ReceiveLoop() + } + + top := container.NewVBox( + widget.NewLabelWithStyle("NymDrop Reader", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), + statusLabel, + widget.NewForm(widget.NewFormItem("Nym inbox address", addrEntry)), + outDirLabel, + ) + + split := container.NewHSplit( + container.NewBorder(widget.NewLabel("Submissions"), nil, nil, nil, list), + container.NewBorder(widget.NewLabel("Content"), nil, nil, nil, content), + ) + split.Offset = 0.35 + + w.SetContent(container.NewBorder(top, nil, nil, nil, split)) + + w.SetCloseIntercept(func() { + if in != nil { + in.Close() + } + w.Close() + }) + + w.ShowAndRun() +} diff --git a/cmd/nymdrop-reader/main.go b/cmd/nymdrop-reader/main.go index 8e235d9..4582768 100644 --- a/cmd/nymdrop-reader/main.go +++ b/cmd/nymdrop-reader/main.go @@ -1,464 +1,80 @@ package main import ( - "crypto/aes" - "crypto/cipher" - "crypto/ecdh" - "crypto/rand" - "crypto/sha256" - - "golang.org/x/crypto/hkdf" - "embed" - "encoding/base64" - "encoding/hex" - "encoding/json" "flag" "fmt" - "io" - "io/fs" - "nymdrop/internal/nym" "os" - "os/exec" "os/signal" "path/filepath" - "runtime" "strings" "syscall" - "time" - "github.com/awnumar/memguard" - "golang.org/x/net/websocket" + "nymdrop/internal/reader" ) -//go:embed bin/nym-client-linux-amd64 -var nymBin embed.FS - func main() { keyFile := flag.String("key", "", "path to X25519 private key file (hex, 32 bytes); generated if missing") inboxID := flag.String("id", "nymdrop-inbox", "nym-client identity id") - wsPort := flag.Int("ws-port", 1977, "nym-client websocket port") - outDir := flag.String("out", "", "directory to save submissions (default: ~/nymdrop-inbox)") + wsPort := flag.Int("ws-port", 1977, "nym-client websocket port") + outDir := flag.String("out", "", "directory to save submissions (default: ~/nymdrop-inbox, or <data-dir>/nymdrop-inbox)") + dataDir := flag.String("data-dir", "", "self-contained data directory (key, nym-client state, inbox all live here instead of $HOME — for portable/USB use, no trace left on the host)") + guiMode := flag.Bool("gui", false, "launch the graphical reader instead of the headless/CLI mode") flag.Parse() - homeDir, _ := os.UserHomeDir() - if *outDir == "" { - *outDir = filepath.Join(homeDir, "nymdrop-inbox") - } - if err := os.MkdirAll(*outDir, 0700); err != nil { - fatalf("mkdir outDir: %v", err) - } - - privKey := loadOrGenKey(*keyFile, homeDir) - - nymBinPath := extractNymClient() - defer os.RemoveAll(filepath.Dir(nymBinPath)) - - configDir := filepath.Join(homeDir, ".nymdrop-reader") - initNymClient(nymBinPath, *inboxID, configDir) - - nymCmd := startNymClient(nymBinPath, *inboxID, configDir, *wsPort) - defer nymCmd.Process.Kill() - - wsURL := fmt.Sprintf("ws://127.0.0.1:%d", *wsPort) - ws := dialWS(wsURL) - defer ws.Close() - - // Print own Nym address so operator knows where to point sources. - printSelfAddress(ws) - - fmt.Printf("nymdrop-reader listening — submissions saved to %s\n", *outDir) - fmt.Println("Press Ctrl+C to quit.") - - go receiveLoop(ws, privKey, *outDir) - - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - <-sig - fmt.Println("\nShutting down.") - // Safety net: wipes any locked buffer still alive (e.g. one in flight in - // receiveLoop when the signal arrived) on top of the deferred Destroy - // calls that cover the normal path. - memguard.Purge() -} - -// ── key management ─────────────────────────────────────────────────────────── - -func loadOrGenKey(keyFile, homeDir string) *ecdh.PrivateKey { - if keyFile == "" { - keyFile = filepath.Join(homeDir, ".nymdrop-reader", "privkey.hex") - } - data, err := os.ReadFile(keyFile) - if err == nil { - raw, err := hex.DecodeString(strings.TrimSpace(string(data))) - if err != nil { - fatalf("privkey decode: %v", err) - } - // raw is wiped as soon as it's copied into locked, non-swappable memory; - // ecdh.NewPrivateKey takes its own copy, so the guarded buffer can be - // destroyed right after construction. - keyBuf := memguard.NewBufferFromBytes(raw) - priv, err := ecdh.X25519().NewPrivateKey(keyBuf.Bytes()) - keyBuf.Destroy() - if err != nil { - fatalf("privkey load: %v", err) - } - fmt.Printf("Loaded private key from %s\n", keyFile) - pubHex := hex.EncodeToString(priv.PublicKey().Bytes()) - fmt.Printf("Public key (deploy in nymdrop-server): %s\n", pubHex) - return priv - } - - // Generate new key. - rawPriv := make([]byte, 32) - if _, err := rand.Read(rawPriv); err != nil { - fatalf("keygen rand: %v", err) - } - keyBuf := memguard.NewBufferFromBytes(rawPriv) // wipes rawPriv on copy - priv, err := ecdh.X25519().NewPrivateKey(keyBuf.Bytes()) - if err != nil { - keyBuf.Destroy() - fatalf("keygen: %v", err) - } - if err := os.MkdirAll(filepath.Dir(keyFile), 0700); err != nil { - keyBuf.Destroy() - fatalf("mkdir keydir: %v", err) - } - if err := os.WriteFile(keyFile, []byte(hex.EncodeToString(keyBuf.Bytes())+"\n"), 0600); err != nil { - keyBuf.Destroy() - fatalf("write privkey: %v", err) - } - keyBuf.Destroy() - pubHex := hex.EncodeToString(priv.PublicKey().Bytes()) - fmt.Printf("Generated new private key → %s\n", keyFile) - fmt.Printf("Public key (deploy in nymdrop-server): %s\n\n", pubHex) - fmt.Println(" Set NYMDROP_PUBKEY_PLACEHOLDER to the public key above in static/crypto.js") - fmt.Println(" and rebuild the server before accepting submissions.") - return priv -} - -// ── nym-client lifecycle ───────────────────────────────────────────────────── - -func extractNymClient() string { - tmpDir, err := os.MkdirTemp("", "nymdrop-reader-*") - if err != nil { - fatalf("mktemp: %v", err) - } - binName := "nym-client" - if runtime.GOOS == "windows" { - binName += ".exe" - } - binPath := filepath.Join(tmpDir, binName) - - srcName := "bin/nym-client-linux-amd64" - data, err := fs.ReadFile(nymBin, srcName) - if err != nil { - fatalf("embedded nym-client not found: %v", err) - } - if err := os.WriteFile(binPath, data, 0700); err != nil { - fatalf("extract nym-client: %v", err) + cfg := reader.Config{ + KeyFile: *keyFile, + InboxID: *inboxID, + WSPort: *wsPort, + OutDir: *outDir, + DataDir: *dataDir, + Debug: os.Getenv("NYMDROP_DEBUG") != "", } - return binPath -} - -func initNymClient(binPath, id, configDir string) { - // --home removed: nym-client v1.1.76+ uses ~/.nym/ fixed path - cmd := exec.Command(binPath, "init", "--id", id) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - _ = cmd.Run() -} -func startNymClient(binPath, id, configDir string, wsPort int) *exec.Cmd { - cmd := exec.Command(binPath, "run", - "--id", id, - "--port", fmt.Sprintf("%d", wsPort), - ) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - fatalf("nym-client start: %v", err) - } - return cmd -} - -// dialWS waits for the nym-client WebSocket to be ready, then returns the -// open connection. Reusing the probe connection avoids the duplicate-connection -// panic in nym-client (which rejects a second WS on the same port). -func dialWS(wsURL string) *websocket.Conn { - fmt.Print("Connecting to Nym mixnet") - for i := 0; i < 120; i++ { - ws, err := websocket.Dial(wsURL, "", "http://localhost/") - if err == nil { - fmt.Println(" ready.") - return ws - } - fmt.Print(".") - time.Sleep(500 * time.Millisecond) - } - fatalf("nym-client websocket did not start in time") - return nil -} - -// ── websocket message types ─────────────────────────────────────────────────── - -type wsReceived struct { - Type string `json:"type"` - Message json.RawMessage `json:"message"` - SenderTag string `json:"senderTag"` -} - -// printSelfAddress queries the inbox address using nym-client's binary -// protocol rather than JSON. This is deliberate, not cosmetic: nym-client -// picks text-vs-binary for every later push ("received" events) based on -// the format of the *last* request it saw on this connection (see -// clients/native/src/websocket/handler.rs, ReceivedResponseType). Submission -// payloads are raw binary crypto material with no base64 wrapping (see -// Client.Send in internal/nym) — if this connection stayed in JSON/text -// mode, nym-client would run String::from_utf8_lossy over that binary data -// before handing it to us, silently corrupting it. Querying the self -// address in binary form here locks the connection into binary mode for -// the lifetime of receiveLoop below. -func printSelfAddress(ws *websocket.Conn) { - if err := websocket.Message.Send(ws, nym.EncodeSelfAddressRequest()); err != nil { - fmt.Fprintf(os.Stderr, "selfAddress send: %v\n", err) - return - } - var frame []byte - if err := websocket.Message.Receive(ws, &frame); err != nil { - fmt.Fprintf(os.Stderr, "selfAddress recv: %v\n", err) + if *guiMode { + runGUI(cfg) return } - addr, err := nym.DecodeSelfAddressResponse(frame) - if err != nil { - fmt.Fprintf(os.Stderr, "selfAddress decode: %v\n", err) - return - } - fmt.Printf("Nym inbox address: %s\n\n", addr) -} - -// ── receive loop ────────────────────────────────────────────────────────────── - -func receiveLoop(ws *websocket.Conn, privKey *ecdh.PrivateKey, outDir string) { - debug := os.Getenv("NYMDROP_DEBUG") != "" - for { - // Read the raw frame regardless of opcode. websocket.Message.Receive - // copies the payload of both text and binary frames into the []byte, - // whereas websocket.JSON.Receive silently drops anything that is not a - // well-formed JSON text frame — which is exactly how earlier received - // messages were being lost. - var frame []byte - if err := websocket.Message.Receive(ws, &frame); err != nil { - if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") { - return - } - fmt.Fprintf(os.Stderr, "ws recv: %v\n", err) - time.Sleep(500 * time.Millisecond) - continue - } - if len(frame) == 0 { - continue - } - if debug { - preview := frame - if len(preview) > 200 { - preview = preview[:200] - } - fmt.Fprintf(os.Stderr, "[debug] frame: %d bytes, lead=0x%02x: %q\n", - len(frame), frame[0], preview) - } - - raw, ok := extractMessage(frame) - if !ok { - continue - } - - plaintext, err := decryptMessage(raw, privKey) - if err != nil { - fmt.Fprintf(os.Stderr, "decrypt failed: %v\n", err) - continue - } - - saveSubmission(outDir, plaintext) - } -} - -// extractMessage pulls the raw submission payload out of a nym-client -// "received" frame, tolerating both encodings nym-client can use: binary -// (what this connection actually runs in, see printSelfAddress) and -// text/JSON (kept as a defensive fallback only — should never fire here). -func extractMessage(frame []byte) ([]byte, bool) { - // Binary frame (nym native binary protocol), the expected/only path: - // 0x01 (Received tag) | has_sender_tag(1) | [sender_tag 16B] | msg_len(8, BE) | msg - // nym.DecodeReceived returns the message bytes exactly as the sender - // transmitted them — for nymdrop that is the raw, unencoded packet - // (ephemeral pubkey || iv || AES-GCM ciphertext), see internal/nym.Client.Send. - if frame[0] == nym.RespReceived { - return nym.DecodeReceived(frame) - } - - // Text/JSON frame: {"type":"received","message":"<base64>","senderTag":...} - // Only reachable if this connection somehow reverted to text mode; the - // message field is base64 in that protocol (JSON strings can't carry - // arbitrary binary), so it must be decoded here to match the binary path. - if frame[0] == '{' { - var msg wsReceived - if err := json.Unmarshal(frame, &msg); err != nil || msg.Type != "received" { - return nil, false - } - var s string - if err := json.Unmarshal(msg.Message, &s); err != nil { - return nil, false - } - raw, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return nil, false - } - return raw, true - } - - return nil, false + runCLI(cfg) } -// ── decryption ──────────────────────────────────────────────────────────────── - -// decryptMessage decrypts a raw packet from the relay. -// -// Wire format from relay: -// nonce_hex(32 ASCII chars) + ":" + ephemeral_x25519_pub(32) + iv(12) + ciphertext+tag -// -// The nonce prefix is stripped; it exists only to make identical submissions -// look different on the Nym wire. It is not used in decryption. -func decryptMessage(raw []byte, privKey *ecdh.PrivateKey) (string, error) { - // Strip relay nonce prefix: 32 hex chars + ':' - const noncePrefix = 32 + 1 // "deadbeef...:" = 33 bytes - if len(raw) < noncePrefix+32+12+1 { - return "", fmt.Errorf("packet too short (%d bytes)", len(raw)) - } - raw = raw[noncePrefix:] - - // Extract ephemeral public key (32 bytes X25519) - ephPubBytes := raw[:32] - iv := raw[32:44] - ciphertext := raw[44:] - - ephPub, err := ecdh.X25519().NewPublicKey(ephPubBytes) - if err != nil { - return "", fmt.Errorf("ephemeral pubkey: %w", err) - } - - // ECDH - sharedSecret, err := privKey.ECDH(ephPub) +func runCLI(cfg reader.Config) { + hooks := reader.Hooks{ + Log: func(format string, args ...any) { + fmt.Printf(format+"\n", args...) + }, + SelfAddress: func(addr string) { + fmt.Printf("Nym inbox address: %s\n\n", addr) + }, + Submission: func(path, preview string) { + ts := strings.TrimSuffix(filepath.Base(path), ".txt") + fmt.Printf("\n[%s] New submission -> %s\n", ts, path) + fmt.Printf(" %s\n", strings.ReplaceAll(preview, "\n", "\n ")) + }, + Attachment: func(path string, size int) { + fmt.Printf(" attachment -> %s (%d bytes)\n", path, size) + }, + Error: func(err error) { + fmt.Fprintln(os.Stderr, err) + }, + } + + in, err := reader.Start(cfg, hooks) if err != nil { - return "", fmt.Errorf("ecdh: %w", err) + fatalf("%v", err) } - // Locked, non-swappable memory for the two secrets derived per submission; - // Destroy zeroes and unlocks instead of relying on a plain overwrite loop. - sharedBuf := memguard.NewBufferFromBytes(sharedSecret) - defer sharedBuf.Destroy() + defer in.Close() - // HKDF-SHA-256 - aesKeyRaw := make([]byte, 32) - hkdfR := hkdf.New(sha256.New, sharedBuf.Bytes(), make([]byte, 32), []byte("nymdrop-v1")) - if _, err := io.ReadFull(hkdfR, aesKeyRaw); err != nil { - return "", fmt.Errorf("hkdf: %w", err) - } - aesBuf := memguard.NewBufferFromBytes(aesKeyRaw) - defer aesBuf.Destroy() - - // AES-GCM-256 decrypt - block, err := aes.NewCipher(aesBuf.Bytes()) - if err != nil { - return "", fmt.Errorf("aes: %w", err) - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return "", fmt.Errorf("gcm: %w", err) - } - plaintext, err := gcm.Open(nil, iv, ciphertext, nil) - if err != nil { - return "", fmt.Errorf("gcm open: %w", err) - } - - return string(plaintext), nil -} - -// ── storage ─────────────────────────────────────────────────────────────────── - -func saveSubmission(outDir, plaintext string) { - ts := time.Now().UTC().Format("20060102-150405") - path := filepath.Join(outDir, ts+".txt") - - // Avoid collision on rapid submissions. - for i := 1; ; i++ { - if _, err := os.Stat(path); os.IsNotExist(err) { - break - } - path = filepath.Join(outDir, fmt.Sprintf("%s-%d.txt", ts, i)) - } - - // Separate embedded files from text body. - body := plaintext - fileSection := "" - if idx := strings.Index(plaintext, "\n---FILE:"); idx != -1 { - body = plaintext[:idx] - fileSection = plaintext[idx+1:] - } - - if err := os.WriteFile(path, []byte(body), 0600); err != nil { - fmt.Fprintf(os.Stderr, "save submission: %v\n", err) - return - } - fmt.Printf("\n[%s] New submission → %s\n", ts, path) - preview := body - if len(preview) > 200 { - preview = preview[:200] + "..." - } - fmt.Printf(" %s\n", strings.ReplaceAll(strings.TrimSpace(preview), "\n", "\n ")) - - // Save attached files. - if fileSection != "" { - saveAttachments(outDir, ts, fileSection) - } -} - -func saveAttachments(outDir, ts, fileSection string) { - lines := strings.Split(fileSection, "\n") - var currentName string - var currentData strings.Builder + fmt.Printf("nymdrop-reader listening -- submissions saved to %s\n", in.OutDir()) + fmt.Println("Press Ctrl+C to quit.") - flush := func() { - if currentName == "" { - return - } - raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(currentData.String())) - if err != nil { - fmt.Fprintf(os.Stderr, " attachment decode %q: %v\n", currentName, err) - return - } - safeName := filepath.Base(filepath.Clean(currentName)) - attPath := filepath.Join(outDir, ts+"-"+safeName) - if err := os.WriteFile(attPath, raw, 0600); err != nil { - fmt.Fprintf(os.Stderr, " save attachment %q: %v\n", currentName, err) - return - } - fmt.Printf(" attachment → %s (%d bytes)\n", attPath, len(raw)) - currentName = "" - currentData.Reset() - } + go in.ReceiveLoop() - for _, line := range lines { - if strings.HasPrefix(line, "---FILE:") && strings.HasSuffix(line, "---") { - flush() - currentName = strings.TrimSuffix(strings.TrimPrefix(line, "---FILE:"), "---") - } else if currentName != "" { - currentData.WriteString(line) - } - } - flush() + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + fmt.Println("\nShutting down.") } -// ── helpers ─────────────────────────────────────────────────────────────────── - func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "nymdrop-reader: "+format+"\n", args...) os.Exit(1) diff --git a/cmd/nymdrop-reader/memguard_verify_test.go b/cmd/nymdrop-reader/memguard_verify_test.go deleted file mode 100644 index 7b240e6..0000000 --- a/cmd/nymdrop-reader/memguard_verify_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/ecdh" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "io" - "testing" - - "golang.org/x/crypto/hkdf" -) - -// Verifies the decryptMessage path still produces the correct plaintext after -// wrapping the ECDH/HKDF secrets in memguard.LockedBuffer, using the exact -// wire format the browser client (static/crypto.js) and relay produce. -func TestDecryptMessageAfterMemguard(t *testing.T) { - readerPriv, err := ecdh.X25519().GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - ephPriv, err := ecdh.X25519().GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - - shared, err := ephPriv.ECDH(readerPriv.PublicKey()) - if err != nil { - t.Fatal(err) - } - aesKey := make([]byte, 32) - hkdfR := hkdf.New(sha256.New, shared, make([]byte, 32), []byte("nymdrop-v1")) - if _, err := io.ReadFull(hkdfR, aesKey); err != nil { - t.Fatal(err) - } - block, err := aes.NewCipher(aesKey) - if err != nil { - t.Fatal(err) - } - gcm, err := cipher.NewGCM(block) - if err != nil { - t.Fatal(err) - } - iv := make([]byte, 12) - if _, err := rand.Read(iv); err != nil { - t.Fatal(err) - } - want := "E2E memguard verification message" - ciphertext := gcm.Seal(nil, iv, []byte(want), nil) - - packet := append(append(append([]byte{}, ephPriv.PublicKey().Bytes()...), iv...), ciphertext...) - noncePrefix := hex.EncodeToString(make([]byte, 16)) + ":" - raw := append([]byte(noncePrefix), packet...) - - got, err := decryptMessage(raw, readerPriv) - if err != nil { - t.Fatalf("decryptMessage: %v", err) - } - if got != want { - t.Fatalf("plaintext mismatch: got %q want %q", got, want) - } -} |
