summaryrefslogtreecommitdiffstats
path: root/internal/ui
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-08-23 00:26:27 +0200
committerGab Virebent <gabriel1@virebent.art>2026-08-23 00:26:27 +0200
commitbdb8a02bbb3dc0d7c39f5b3ec451abb68e9d093d (patch)
tree796d246f9874641717b5e81a72989961e65a9b9f /internal/ui
parentc1decadb590c4d79d92bcc4df7772119a5c91244 (diff)
downloadaegis-main.tar.gz
aegis-main.tar.xz
aegis-main.zip
Simplify Usenet posting to signing onlyHEADmain
Diffstat (limited to 'internal/ui')
-rw-r--r--internal/ui/app.go227
-rw-r--r--internal/ui/app_test.go46
2 files changed, 84 insertions, 189 deletions
diff --git a/internal/ui/app.go b/internal/ui/app.go
index d193ce1..f7e7cac 100644
--- a/internal/ui/app.go
+++ b/internal/ui/app.go
@@ -10,7 +10,6 @@ import (
"io"
"mime"
"net/mail"
- "net/url"
"sort"
"strconv"
"strings"
@@ -86,16 +85,14 @@ type application struct {
composeGroups *widget.Entry
composeDelivery *widget.Select
- composeMail2News *widget.Entry
+ composeTo *widget.Entry
composeFrom *widget.Entry
composeSubject *widget.Entry
composeReferences *widget.Entry
composeFollowupTo *widget.Entry
composeBody *widget.Entry
composeCryptoMode *widget.Select
- composeCryptoKey *widget.Entry
composeCryptoSigningKey *widget.Entry
- composeCryptoKeyURL *widget.Entry
cryptoAlgorithm *widget.Select
cryptoOperation *widget.Select
@@ -144,6 +141,8 @@ type application struct {
vfaceStatus *widget.Label
vfaceImage *canvas.Image
vfaceHash *widget.Label
+ vfacePublicKey *widget.Label
+ vfaceKeyStatus *widget.Label
}
func Run() {
@@ -311,8 +310,8 @@ func (a *application) buildComposer() fyne.CanvasObject {
a.composeGroups.SetPlaceHolder("comp.lang.go,example.group")
a.composeDelivery = widget.NewSelect([]string{"NNTP direct posting", "SMTP mail2news"}, nil)
a.composeDelivery.SetSelected("NNTP direct posting")
- a.composeMail2News = widget.NewEntry()
- a.composeMail2News.SetText(a.settings.SMTPRecipient)
+ a.composeTo = widget.NewEntry()
+ a.composeTo.SetText(a.settings.SMTPRecipient)
a.composeFrom = widget.NewEntry()
a.composeSubject = widget.NewEntry()
a.composeReferences = widget.NewEntry()
@@ -322,35 +321,27 @@ func (a *application) buildComposer() fyne.CanvasObject {
a.composeBody = widget.NewMultiLineEntry()
a.composeBody.SetPlaceHolder("Article body...")
a.composeFrom.SetText(formatFrom(a.settings.DisplayName, a.settings.Email))
- a.composeFrom.Disable()
+ // Keep the identity visible in the normal foreground color. VFace, when
+ // unlocked, still replaces this value before posting.
a.composeCryptoMode = widget.NewSelect([]string{
"Plain",
"Sign with Ed25519",
- "Encrypt with age",
- "Encrypt with age and sign",
}, nil)
a.composeCryptoMode.SetSelected("Plain")
- a.composeCryptoKey = widget.NewMultiLineEntry()
- a.composeCryptoKey.SetPlaceHolder("age recipient (native X25519 or SSH Ed25519/RSA)")
- a.composeCryptoKey.Wrapping = fyne.TextWrapOff
a.composeCryptoSigningKey = widget.NewMultiLineEntry()
- a.composeCryptoSigningKey.SetPlaceHolder("Ed25519 private key, base64 or hexadecimal")
+ a.composeCryptoSigningKey.SetPlaceHolder("Optional Ed25519 private key; VFace supplies it automatically")
a.composeCryptoSigningKey.Wrapping = fyne.TextWrapOff
- a.composeCryptoKeyURL = widget.NewEntry()
- a.composeCryptoKeyURL.SetPlaceHolder("Optional HTTPS URL for the public key")
post := widget.NewButtonWithIcon("Post article", theme.MailSendIcon(), a.postArticle)
form := widget.NewForm(
widget.NewFormItem("Newsgroups", a.composeGroups),
widget.NewFormItem("Delivery", a.composeDelivery),
- widget.NewFormItem("mail2news recipient", a.composeMail2News),
+ widget.NewFormItem("To", a.composeTo),
widget.NewFormItem("From", a.composeFrom),
widget.NewFormItem("Subject", a.composeSubject),
widget.NewFormItem("References", a.composeReferences),
widget.NewFormItem("Followup-To", a.composeFollowupTo),
- widget.NewFormItem("Crypto mode", a.composeCryptoMode),
- widget.NewFormItem("Age recipient", a.composeCryptoKey),
+ widget.NewFormItem("Mode", a.composeCryptoMode),
widget.NewFormItem("Ed25519 signing key", a.composeCryptoSigningKey),
- widget.NewFormItem("Public-key URL", a.composeCryptoKeyURL),
)
return container.NewBorder(form, post, nil, nil, a.composeBody)
}
@@ -368,6 +359,11 @@ func (a *application) buildProfile() fyne.CanvasObject {
a.vfaceStatus.Wrapping = fyne.TextWrapWord
a.vfaceHash = widget.NewLabel("")
a.vfaceHash.Wrapping = fyne.TextWrapWord
+ a.vfacePublicKey = widget.NewLabel("")
+ a.vfacePublicKey.Wrapping = fyne.TextWrapBreak
+ a.vfacePublicKey.Selectable = true
+ a.vfaceKeyStatus = widget.NewLabel("")
+ a.vfaceKeyStatus.Wrapping = fyne.TextWrapWord
a.vfaceImage = canvas.NewImageFromImage(image.NewRGBA(image.Rect(0, 0, 48, 48)))
a.vfaceImage.FillMode = canvas.ImageFillContain
a.vfaceImage.SetMinSize(fyne.NewSize(96, 96))
@@ -387,7 +383,7 @@ func (a *application) buildProfile() fyne.CanvasObject {
widget.NewFormItem("Vault password", a.vfacePasswordEntry),
widget.NewFormItem("Confirm password", a.vfaceConfirmEntry),
)
- provider := widget.NewLabel("Crypto providers: age for software encryption. OpenPGP is available only through a YubiKey integration and is not exposed as a standalone format.")
+ provider := widget.NewLabel("VFace creates an Ed25519 key pair. The public key is part of the identity; the private key remains encrypted in the local vault and is used for signing. Message encryption is intentionally not part of the Usenet client.")
provider.Wrapping = fyne.TextWrapWord
return container.NewVScroll(container.NewVBox(
widget.NewLabelWithStyle("Optional pseudonymous identity", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
@@ -397,6 +393,8 @@ func (a *application) buildProfile() fyne.CanvasObject {
a.vfaceStatus,
a.vfaceImage,
a.vfaceHash,
+ a.vfacePublicKey,
+ a.vfaceKeyStatus,
provider,
))
}
@@ -429,6 +427,7 @@ func (a *application) createVFaceProfile() {
a.vfaceConfirmEntry.SetText("")
a.renderVFaceProfile(value)
a.updateComposeIdentity()
+ a.composeCryptoMode.SetSelected("Sign with Ed25519")
a.vfaceStatus.SetText("VFace identity created and encrypted on disk.")
})
}()
@@ -454,6 +453,7 @@ func (a *application) loadVFaceProfile() {
a.vfaceEmailEntry.SetText(value.Email)
a.renderVFaceProfile(value)
a.updateComposeIdentity()
+ a.composeCryptoMode.SetSelected("Sign with Ed25519")
a.vfaceStatus.SetText("VFace identity loaded from encrypted disk vault.")
})
}()
@@ -464,6 +464,15 @@ func (a *application) lockVFaceProfile() {
if a.vfaceImage != nil {
a.vfaceImage.Hide()
}
+ if a.vfacePublicKey != nil {
+ a.vfacePublicKey.SetText("")
+ }
+ if a.vfaceKeyStatus != nil {
+ a.vfaceKeyStatus.SetText("")
+ }
+ if a.composeCryptoMode != nil && a.composeCryptoMode.Selected == "Sign with Ed25519" {
+ a.composeCryptoMode.SetSelected("Plain")
+ }
if a.vfaceStatus != nil {
a.vfaceStatus.SetText("VFace identity locked. VFace is optional.")
}
@@ -483,6 +492,8 @@ func (a *application) renderVFaceProfile(value vfaceprofile.Profile) {
a.vfaceImage.Refresh()
}
a.vfaceHash.SetText("Identity SHA-256: " + profile.IdentityHash + "\nPNG SHA-256: " + profile.PNGHash)
+ a.vfacePublicKey.SetText("Ed25519 public key (selectable):\n" + value.PublicKey)
+ a.vfaceKeyStatus.SetText("Ed25519 key pair ready. Private key is encrypted in the local vault and available for signing.")
}
func (a *application) updateComposeIdentity() {
@@ -497,12 +508,12 @@ func (a *application) updateComposeIdentity() {
}
func (a *application) buildCrypto() fyne.CanvasObject {
- a.cryptoAlgorithm = widget.NewSelect([]string{"OpenPGP", "age", "Ed25519", "YubiCrypt"}, nil)
- a.cryptoAlgorithm.SetSelected("OpenPGP")
- a.cryptoOperation = widget.NewSelect([]string{"Sign", "Verify", "Encrypt", "Decrypt"}, nil)
+ a.cryptoAlgorithm = widget.NewSelect([]string{"Ed25519", "YubiCrypt"}, nil)
+ a.cryptoAlgorithm.SetSelected("Ed25519")
+ a.cryptoOperation = widget.NewSelect([]string{"Sign", "Verify"}, nil)
a.cryptoOperation.SetSelected("Sign")
a.cryptoMessage = widget.NewMultiLineEntry()
- a.cryptoMessage.SetPlaceHolder("Message or ciphertext")
+ a.cryptoMessage.SetPlaceHolder("Message")
a.cryptoMessage.Wrapping = fyne.TextWrapOff
a.cryptoPrimary = widget.NewMultiLineEntry()
a.cryptoPrimary.SetPlaceHolder("Key material supplied by you")
@@ -529,13 +540,13 @@ func (a *application) buildCrypto() fyne.CanvasObject {
a.cryptoSecondary.SetText("")
a.cryptoOutput.SetText("No result yet.")
})
- note := widget.NewLabel("Aegis does not generate or save keys. Key fields are used only for this session. YubiCrypt requires the optional yubicrypt executable, a YubiKey, pcscd and the PIV PIN.")
+ note := widget.NewLabel("This panel is limited to signing and verification. YubiCrypt requires the optional yubicrypt executable, a YubiKey, pcscd and the PIV PIN.")
note.Wrapping = fyne.TextWrapWord
form := widget.NewForm(
widget.NewFormItem("Format", a.cryptoAlgorithm),
widget.NewFormItem("Operation", a.cryptoOperation),
)
- messageBox := container.NewVBox(widget.NewLabel("Message / ciphertext"), a.cryptoMessage)
+ messageBox := container.NewVBox(widget.NewLabel("Message"), a.cryptoMessage)
a.cryptoPrimaryBox = container.NewVBox(a.cryptoPrimaryLabel, a.cryptoPrimary)
a.cryptoSecretBox = container.NewVBox(a.cryptoSecretLabel, a.cryptoSecret)
a.cryptoSecondaryBox = container.NewVBox(a.cryptoSecondaryLabel, a.cryptoSecondary)
@@ -567,27 +578,6 @@ func (a *application) refreshCryptoFields() {
a.cryptoPrimaryLabel.SetText("Signature")
a.cryptoSecondaryLabel.SetText("Public key")
}
- case "Encrypt":
- if algorithm == "OpenPGP" {
- a.cryptoPrimaryLabel.SetText("Recipient public key")
- a.cryptoSecondaryLabel.SetText("Optional signer private key")
- } else if algorithm == "YubiCrypt" {
- a.cryptoPrimaryLabel.SetText("RSA recipient certificate/key (PEM)")
- a.cryptoSecondaryBox.Hide()
- } else {
- a.cryptoPrimaryLabel.SetText("Recipient key")
- a.cryptoSecondaryLabel.SetText("Not used")
- }
- case "Decrypt":
- if algorithm == "YubiCrypt" {
- a.cryptoPrimaryBox.Hide()
- a.cryptoSecretBox.Show()
- a.cryptoSecretLabel.SetText("YubiKey PIV PIN")
- a.cryptoSecondaryBox.Hide()
- } else {
- a.cryptoPrimaryLabel.SetText("Private key / identity")
- a.cryptoSecondaryLabel.SetText("Not used")
- }
default:
if algorithm == "YubiCrypt" {
a.cryptoPrimaryBox.Hide()
@@ -600,7 +590,7 @@ func (a *application) refreshCryptoFields() {
}
}
a.cryptoSecondary.Disable()
- if operation == "Verify" || (operation == "Encrypt" && algorithm == "OpenPGP") {
+ if operation == "Verify" {
a.cryptoSecondary.Enable()
}
a.cryptoPrimaryBox.Refresh()
@@ -616,17 +606,13 @@ func (a *application) runCryptoOperation() {
secret := a.cryptoSecret.Text
secondary := a.cryptoSecondary.Text
if len(strings.TrimSpace(string(message))) == 0 {
- dialog.ShowError(errors.New("message or ciphertext is required"), a.window)
+ dialog.ShowError(errors.New("message is required"), a.window)
return
}
- if algorithm == "YubiCrypt" && operation != "Verify" && operation != "Encrypt" && strings.TrimSpace(secret) == "" {
+ if algorithm == "YubiCrypt" && operation == "Sign" && strings.TrimSpace(secret) == "" {
dialog.ShowError(errors.New("YubiKey PIV PIN is required"), a.window)
return
}
- if algorithm == "YubiCrypt" && operation == "Encrypt" && strings.TrimSpace(primary) == "" {
- dialog.ShowError(errors.New("RSA recipient certificate/key is required"), a.window)
- return
- }
if algorithm != "YubiCrypt" && strings.TrimSpace(primary) == "" {
dialog.ShowError(errors.New("primary key material is required"), a.window)
return
@@ -636,33 +622,6 @@ func (a *application) runCryptoOperation() {
var result string
var err error
switch algorithm {
- case "OpenPGP":
- switch operation {
- case "Sign":
- result, err = cryptokit.SignOpenPGPDetached(message, primary)
- case "Verify":
- err = cryptokit.VerifyOpenPGPDetached(message, primary, secondary)
- result = "OpenPGP signature verified."
- case "Encrypt":
- result, err = cryptokit.EncryptOpenPGP(message, primary, secondary)
- case "Decrypt":
- var plaintext []byte
- plaintext, err = cryptokit.DecryptOpenPGP(message, primary)
- result = string(plaintext)
- }
- case "age":
- switch operation {
- case "Encrypt":
- var ciphertext []byte
- ciphertext, err = cryptokit.EncryptAge(message, primary)
- result = string(ciphertext)
- case "Decrypt":
- var plaintext []byte
- plaintext, err = cryptokit.DecryptAge(message, primary)
- result = string(plaintext)
- default:
- err = errors.New("age supports Encrypt and Decrypt")
- }
case "Ed25519":
switch operation {
case "Sign":
@@ -670,8 +629,6 @@ func (a *application) runCryptoOperation() {
case "Verify":
err = cryptokit.VerifyEd25519(message, primary, secondary)
result = "Ed25519 signature verified."
- default:
- err = errors.New("raw Ed25519 supports Sign and Verify; use age SSH keys for encryption")
}
case "YubiCrypt":
switch operation {
@@ -683,18 +640,10 @@ func (a *application) runCryptoOperation() {
var verified []byte
verified, err = cryptokit.VerifyYubiCrypt(message)
result = string(verified)
- case "Encrypt":
- var ciphertext []byte
- ciphertext, err = cryptokit.EncryptYubiCrypt(message, primary)
- result = string(ciphertext)
- case "Decrypt":
- var plaintext []byte
- plaintext, err = cryptokit.DecryptYubiCrypt(message, secret)
- result = string(plaintext)
}
}
fyne.Do(func() {
- a.setBusy(false, "Cryptography operation completed.")
+ a.setBusy(false, "Signing operation completed.")
if err != nil {
dialog.ShowError(err, a.window)
return
@@ -780,7 +729,7 @@ func (a *application) buildSettings() fyne.CanvasObject {
widget.NewFormItem("SMTP transport", a.smtpModeSelect),
widget.NewFormItem("SMTP username", a.smtpUserEntry),
widget.NewFormItem("SMTP email", a.smtpEmailEntry),
- widget.NewFormItem("mail2news recipient", a.smtpRecipientEntry),
+ widget.NewFormItem("Default To", a.smtpRecipientEntry),
widget.NewFormItem("SMTP password", a.smtpPasswordEntry),
widget.NewFormItem("SMTP TLS", a.smtpSkipVerify),
widget.NewFormItem("NNTP display name", a.displayEntry),
@@ -1192,11 +1141,10 @@ func formatArticleHeaders(article string, showAll bool) string {
return raw
}
important := []string{
- "From", "Date", "Newsgroups", "Subject", "Message-ID", "References", "Followup-To",
+ "From", "To", "Date", "Newsgroups", "Subject", "Message-ID", "References", "Followup-To",
"Reply-To", "Organization", "User-Agent", "MIME-Version", "Content-Type",
- "Content-Transfer-Encoding", "Face", "OpenPGP", "X-OpenPGP", "X-Signature",
- "X-Aegis-Crypto-Version", "X-Aegis-Encryption", "X-Aegis-Signature",
- "X-Aegis-Public-Key", "X-Aegis-Key-Fingerprint", "X-Aegis-Public-Key-URL",
+ "Content-Transfer-Encoding", "Face", "X-Signature",
+ "X-Aegis-Signature", "X-Aegis-Public-Key", "X-Aegis-Key-Fingerprint",
"X-VFace-Version", "X-Ed25519-Pub", "X-Ed25519-Sig", "Identity-Hash",
"X-VFace-Hash", "X-VFace-PNG-SHA256", "X-VFace-Verify",
}
@@ -1268,26 +1216,30 @@ func (a *application) postArticle() {
return
}
var recipients []string
+ to := strings.TrimSpace(a.composeTo.Text)
if delivery == "SMTP mail2news" {
- recipient := strings.TrimSpace(a.composeMail2News.Text)
- if recipient == "" {
- recipient = strings.TrimSpace(settings.SMTPRecipient)
+ if to == "" {
+ to = strings.TrimSpace(settings.SMTPRecipient)
}
- if recipient == "" {
- dialog.ShowError(errors.New("mail2news recipient is required for SMTP delivery"), a.window)
+ if to == "" {
+ dialog.ShowError(errors.New("To address is required for SMTP delivery"), a.window)
return
}
- if _, err := mail.ParseAddress(recipient); err != nil {
- dialog.ShowError(fmt.Errorf("invalid mail2news recipient: %w", err), a.window)
+ if _, err := mail.ParseAddress(to); err != nil {
+ dialog.ShowError(fmt.Errorf("invalid To address: %w", err), a.window)
+ return
+ }
+ recipients = []string{to}
+ } else if to != "" {
+ if _, err := mail.ParseAddress(to); err != nil {
+ dialog.ShowError(fmt.Errorf("invalid To address: %w", err), a.window)
return
}
- recipients = []string{recipient}
}
article, err := buildArticleWithIdentityHeaders(groups, from, subject, a.composeBody.Text, identityHeaders, articleCryptoOptions{
Mode: a.composeCryptoMode.Selected,
- EncryptionKey: a.composeCryptoKey.Text,
+ To: to,
SigningKey: signingKey,
- PublicKeyURL: a.composeCryptoKeyURL.Text,
ExpectedPublicKey: expectedPublicKey,
References: strings.TrimSpace(a.composeReferences.Text),
FollowupTo: strings.TrimSpace(a.composeFollowupTo.Text),
@@ -1322,10 +1274,8 @@ func (a *application) postArticle() {
a.composeBody.SetText("")
a.composeReferences.SetText("")
a.composeFollowupTo.SetText("")
- a.composeMail2News.SetText(settings.SMTPRecipient)
- a.composeCryptoKey.SetText("")
+ a.composeTo.SetText(settings.SMTPRecipient)
a.composeCryptoSigningKey.SetText("")
- a.composeCryptoKeyURL.SetText("")
a.composeCryptoMode.SetSelected("Plain")
a.setBusy(false, "Article accepted by the NNTP server.")
dialog.ShowInformation("Article posted", "The NNTP server accepted the article.", a.window)
@@ -1339,9 +1289,8 @@ func buildTextArticle(groups []string, from, subject, body string) (string, erro
type articleCryptoOptions struct {
Mode string
- EncryptionKey string
+ To string
SigningKey string
- PublicKeyURL string
ExpectedPublicKey string
References string
FollowupTo string
@@ -1399,6 +1348,15 @@ func buildArticleWithIdentityHeaders(groups []string, from, subject, body string
"Content-Type: " + contentType,
"Content-Transfer-Encoding: " + contentTransferEncoding,
}
+ if to := strings.TrimSpace(options.To); to != "" {
+ if strings.ContainsAny(to, "\r\n") {
+ return "", errors.New("To must not contain line breaks")
+ }
+ if _, err := mail.ParseAddress(to); err != nil {
+ return "", fmt.Errorf("invalid To address: %w", err)
+ }
+ headers = append(headers, foldHeader("To", to))
+ }
if options.References != "" {
if strings.ContainsAny(options.References, "\r\n") {
return "", errors.New("References must not contain line breaks")
@@ -1445,51 +1403,18 @@ func prepareArticleCrypto(body, mode string, options articleCryptoOptions) (head
contentType = "text/plain; charset=UTF-8"
transferEncoding = "8bit"
signingKey := strings.TrimSpace(options.SigningKey)
- encryptionKey := strings.TrimSpace(options.EncryptionKey)
- publicKeyURL := strings.TrimSpace(options.PublicKeyURL)
- if publicKeyURL != "" {
- parsed, parseErr := url.Parse(publicKeyURL)
- if parseErr != nil || !strings.EqualFold(parsed.Scheme, "https") || parsed.Host == "" || strings.ContainsAny(publicKeyURL, "\r\n") {
- return nil, "", "", "", errors.New("public-key URL must be a valid HTTPS URL without line breaks")
- }
- }
switch mode {
case "Plain":
- if signingKey != "" || encryptionKey != "" || publicKeyURL != "" {
- return nil, "", "", "", errors.New("plain mode does not accept cryptographic key fields")
- }
case "Sign with Ed25519":
if signingKey == "" {
return nil, "", "", "", errors.New("an Ed25519 private key is required for signing")
}
- case "Encrypt with age":
- if encryptionKey == "" {
- return nil, "", "", "", errors.New("an age recipient is required for encryption")
- }
- ciphertext, encryptErr := cryptokit.EncryptAge([]byte(body), encryptionKey)
- if encryptErr != nil {
- return nil, "", "", "", fmt.Errorf("encrypt article body: %w", encryptErr)
- }
- wireBody = normalizeCRLF(string(ciphertext))
- contentType = "application/vnd.aegis.age"
- transferEncoding = "7bit"
- case "Encrypt with age and sign":
- if encryptionKey == "" || signingKey == "" {
- return nil, "", "", "", errors.New("an age recipient and an Ed25519 private key are required")
- }
- ciphertext, encryptErr := cryptokit.EncryptAge([]byte(body), encryptionKey)
- if encryptErr != nil {
- return nil, "", "", "", fmt.Errorf("encrypt article body: %w", encryptErr)
- }
- wireBody = normalizeCRLF(string(ciphertext))
- contentType = "application/vnd.aegis.age"
- transferEncoding = "7bit"
default:
- return nil, "", "", "", fmt.Errorf("unsupported article crypto mode %q", mode)
+ return nil, "", "", "", fmt.Errorf("unsupported article signing mode %q", mode)
}
- if mode == "Sign with Ed25519" || mode == "Encrypt with age and sign" {
+ if mode == "Sign with Ed25519" {
if signingKey == "" {
return nil, "", "", "", errors.New("an Ed25519 private key is required for signing")
}
@@ -1521,14 +1446,6 @@ func prepareArticleCrypto(body, mode string, options articleCryptoOptions) (head
"X-Ed25519-Sig: "+signature,
"X-Aegis-Key-Fingerprint: "+fingerprint,
)
- if publicKeyURL != "" {
- headers = append(headers, foldHeader("X-Aegis-Public-Key-URL", publicKeyURL))
- }
- } else if publicKeyURL != "" {
- return nil, "", "", "", errors.New("public-key URL requires an Ed25519 signature mode")
- }
- if mode == "Encrypt with age" || mode == "Encrypt with age and sign" {
- headers = append([]string{"X-Aegis-Crypto-Version: 1", "X-Aegis-Encryption: age"}, headers...)
}
return headers, contentType, transferEncoding, wireBody, nil
}
diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go
index 0483bdb..6e9dc2a 100644
--- a/internal/ui/app_test.go
+++ b/internal/ui/app_test.go
@@ -10,8 +10,6 @@ import (
"aegis/internal/cryptokit"
"aegis/internal/identity"
-
- "filippo.io/age"
)
func TestNormalizeGroups(t *testing.T) {
@@ -100,7 +98,7 @@ func TestPrepareArticleCryptoSignsCanonicalBody(t *testing.T) {
headers, contentType, transferEncoding, wireBody, err := prepareArticleCrypto(
"hello\r\nworld",
"Sign with Ed25519",
- articleCryptoOptions{SigningKey: privateSeed, PublicKeyURL: "https://keys.example.invalid/aegis.pub"},
+ articleCryptoOptions{SigningKey: privateSeed},
)
if err != nil {
t.Fatal(err)
@@ -108,10 +106,6 @@ func TestPrepareArticleCryptoSignsCanonicalBody(t *testing.T) {
if contentType != "text/plain; charset=UTF-8" || transferEncoding != "8bit" {
t.Fatalf("unexpected MIME metadata: %q, %q", contentType, transferEncoding)
}
- joined := strings.Join(headers, "\r\n")
- if !strings.Contains(joined, "X-Aegis-Public-Key-URL: https://keys.example.invalid/aegis.pub") {
- t.Fatalf("missing public-key URL: %s", joined)
- }
const signaturePrefix = "X-Aegis-Signature: ed25519; "
var signature string
for _, header := range headers {
@@ -127,38 +121,22 @@ func TestPrepareArticleCryptoSignsCanonicalBody(t *testing.T) {
}
}
-func TestPrepareArticleCryptoEncryptsAndSigns(t *testing.T) {
- identity, err := age.GenerateX25519Identity()
- if err != nil {
- t.Fatal(err)
+func TestPrepareArticleCryptoRejectsMessageEncryption(t *testing.T) {
+ if _, _, _, _, err := prepareArticleCrypto("secret body", "Encrypt with age", articleCryptoOptions{}); err == nil {
+ t.Fatal("message encryption mode was accepted")
}
- _, private, err := ed25519.GenerateKey(rand.Reader)
- if err != nil {
- t.Fatal(err)
- }
- headers, contentType, transferEncoding, wireBody, err := prepareArticleCrypto(
- "secret body",
- "Encrypt with age and sign",
- articleCryptoOptions{
- EncryptionKey: identity.Recipient().String(),
- SigningKey: base64.StdEncoding.EncodeToString(private.Seed()),
- },
+}
+
+func TestBuildArticleIncludesToHeader(t *testing.T) {
+ article, err := buildArticleWithIdentityHeaders(
+ []string{"alt.test"}, "reader@example.org", "To header", "body", nil,
+ articleCryptoOptions{Mode: "Plain", To: "mail2news@example.org"},
)
if err != nil {
t.Fatal(err)
}
- if contentType != "application/vnd.aegis.age" || transferEncoding != "7bit" {
- t.Fatalf("unexpected encrypted MIME metadata: %q, %q", contentType, transferEncoding)
- }
- if !strings.Contains(strings.Join(headers, "\r\n"), "X-Aegis-Encryption: age") {
- t.Fatal("missing age encryption header")
- }
- plaintext, err := cryptokit.DecryptAge([]byte(strings.ReplaceAll(wireBody, "\r\n", "\n")), identity.String())
- if err != nil {
- t.Fatal(err)
- }
- if string(plaintext) != "secret body" {
- t.Fatalf("decrypted body = %q", plaintext)
+ if !strings.Contains(article, "To: mail2news@example.org\r\n") {
+ t.Fatalf("article missing To header:\n%s", article)
}
}