>From 3f777c98b55a230a18533e39bb481b902aeba128 Mon Sep 17 00:00:00 2001 From: "J.P. Neverwas" Date: Thu, 4 Jan 2024 06:04:06 -0800 Subject: [PATCH 1/3] [POC] Use erc-pre-send-functions API ;; Note that these changes exist for demonstration purposes and are intended ;; to be viewed as a series. For the sake of simplicity, they ignore ;; in-channel functionality and focus exclusively on queries (direct ;; messages). These changes only work with ERC 5.6-git and the patches from bug#68265 applied atop HEAD. The primary innovation here is the newly decoupled show-send pairing for processing prompt input via `erc-pre-send-functions'. This allows the module to refrain from disabling `erc-fill', which would otherwise mangle its payload. However, notice there's still a bit of awkwardness with `erc-crypt--insert' in terms of abstraction leakage. Third parties shouldn't be troubled with the particulars of message text properties. In much the same fashion, the function `erc-crypt-get-last-message-nick', relies on heuristics to find the speaker. --- erc-crypt.el | 251 +++++++++++++++++++++++---------------------------- 1 file changed, 111 insertions(+), 140 deletions(-) diff --git a/erc-crypt.el b/erc-crypt.el index 8528be0..fdc2c8c 100644 --- a/erc-crypt.el +++ b/erc-crypt.el @@ -7,7 +7,7 @@ ;; Version: 2.1 ;; Author: xristos ;; URL: https://github.com/atomontage/erc-crypt -;; Package-Requires: ((cl-lib "0.5")) +;; Package-Requires: ((erc "5.6") (cl-lib "0.5")) ;; Keywords: comm ;; Redistribution and use in source and binary forms, with or without @@ -86,17 +86,9 @@ ;; to preserve your current history. That way, it's much easier for outsiders ;; to gauge how they differ. Just a suggestion. -;; FIXME include minimum supported version of ERC in the Package-Requires -;; header so that people who install this via package.el will automatically -;; get the right ERC as a dependency. (require 'erc) (require 'sha1) (require 'cl-lib) -(require 'erc-fill) - -;; erc-fill doesn't play nice with erc-crypt.el -(defvar-local erc-crypt-fill-function nil) -;; (make-variable-buffer-local 'erc-fill-function) ; <- pls don't do this! (defvar erc-crypt-openssl-path "openssl" "Path to openssl binary.") @@ -119,16 +111,9 @@ If input message exceeds it, message is broken up using `erc-crypt-split-message'. This is used to work around IRC protocol message limits.") -(defvar-local erc-crypt-message nil - "Last message sent (before encryption).") - (defvar-local erc-crypt-key-file nil "Path to erc-crypt keyfile. It's buffer local.") -(defvar-local erc-crypt--left-over nil - "List that contains message fragments. -Processed by `erc-crypt-post-send' inside `erc-send-completed-hook'.") - (defvar-local erc-crypt--insert-queue nil "List that contains message fragments, before insertion. Managed by `erc-crypt-maybe-insert'.") @@ -179,51 +164,54 @@ Must be string.") (defvar erc-crypt-dir-userdef "~/.emacs.d/irc/erc-crypt" "User defined directory for erc-crypt.") -;; Consider initializing at runtime so users can set `erc-crypt-dir-userdef' -;; after loading this file. -(defvar erc-crypt--dir (expand-file-name - (file-name-as-directory erc-crypt-dir-userdef)) - "Erc crypt directory contained public and private keys.") +;; You could make this `buffer-local' so that users can use different +;; directories for different ERC sessions in the same Emacs session, but you'd +;; need to update all the `erc-crypt-dh-*' functions, etc. +(defvar erc-crypt--dir nil + "Directory ending in a slash, containing public and private keys.") -(add-hook 'erc-insert-pre-hook #'erc-crypt-on-off-check) - ;; enable if '----CRYPT ON----' string found - -(define-minor-mode erc-crypt-mode +(define-erc-module crypt nil "Per buffer encryption for ERC." - :lighter " CRYPT" - (if erc-crypt-mode - ;; Enabled - (progn - ;; FIXME use new API (`erc-pre-send-functions'). - (with-suppressed-warnings ((obsolete erc-send-pre-hook)) - (add-hook 'erc-send-pre-hook #'erc-crypt-maybe-send nil t)) + ;; Enabled + ((unless erc-crypt--dir + ;; enable if '----CRYPT ON----' string found + (add-hook 'erc-insert-pre-hook #'erc-crypt-on-off-check) + (setq erc-crypt--dir + (expand-file-name (file-name-as-directory erc-crypt-dir-userdef)))) + (if (or (eql erc--module-toggle-prefix-arg 4) (erc-crypt-find-key)) + (progn + (add-hook 'erc-pre-send-functions #'erc-crypt-maybe-send nil t) (add-hook 'erc-send-modify-hook #'erc-crypt-maybe-send-fixup nil t) - (add-hook 'erc-send-completed-hook #'erc-crypt-post-send nil t) (add-hook 'erc-insert-pre-hook #'erc-crypt-pre-insert nil t) (add-hook 'erc-insert-modify-hook #'erc-crypt-maybe-insert nil t) (add-hook 'erc-insert-post-hook #'erc-crypt-dh-save nil t) ;; Reset buffer locals - (setq-local erc-fill-function nil) - (setq erc-crypt--left-over nil - erc-crypt--insert-queue nil - erc-crypt-fill-function erc-fill-function)) + (setq erc-crypt--insert-queue nil) + ;; Don't bother splitting lines, since the sub protocol already does + ;; that for transmission purposes + (setq-local erc-split-line-length 0)) + (erc-crypt-mode -1))) ;; Disabled - (progn - ;; FIXME use new API (`erc-pre-send-functions'). - (with-suppressed-warnings ((obsolete erc-send-pre-hook)) - (remove-hook 'erc-send-pre-hook #'erc-crypt-maybe-send t)) + ( (remove-hook 'erc-pre-send-functions #'erc-crypt-maybe-send t) (remove-hook 'erc-send-modify-hook #'erc-crypt-maybe-send-fixup t) - (remove-hook 'erc-send-completed-hook #'erc-crypt-post-send t) (remove-hook 'erc-insert-pre-hook #'erc-crypt-pre-insert t) (remove-hook 'erc-insert-modify-hook #'erc-crypt-maybe-insert t) (remove-hook 'erc-insert-post-hook #'erc-crypt-dh-save t) - (unless erc-fill-function (kill-local-variable 'erc-fill-function)) - (setq erc-crypt-fill-function nil)))) - -;; FIXME move here so `erc-crypt-mode' itself is already defined. -(define-globalized-minor-mode erc-crypt-on-off erc-crypt-mode - erc-crypt-find-key :group 'erc-crypt) ;; enable if key found + (mapc #'kill-local-variable '(erc-crypt-key-file + erc-crypt--insert-queue + erc-crypt--post-insert + erc-split-line-length))) + 'local) + +(unless (assq 'erc-crypt-mode minor-mode-alist) + (push '(erc-crypt-mode + (erc-crypt--insert-queue + (" CRYPT⇄" (:propertize (:eval (number-to-string + (length erc-crypt--insert-queue))) + face mode-line-emphasis)) + " CRYPT")) + minor-mode-alist)) ;;; ;;; Internals @@ -274,20 +262,19 @@ See `erc-send-modify-hook' and `erc-insert-modify-hook'." (let ((start (cl-gensym))) `(when erc-crypt-mode (goto-char (point-min)) - ;; FIXME These two functions can return nil when, e.g., - ;; `erc-crypt-msg-type' is "plain-text", which means this search will - ;; always match, and QUIT messages etc. will be replaced with an - ;; indicator. - (let* ((prefix (erc-crypt-prefix-check)) - (postfix (erc-crypt-postfix-check)) - (,start nil)) - (when (re-search-forward (concat prefix ".+" postfix) nil t) + (let ((,start nil)) + (when-let ((prefix (erc-crypt-prefix-check)) + (postfix (erc-crypt-postfix-check)) + ((re-search-forward + (rx-to-string `(: ,prefix (+ nonl) ,postfix)) nil t))) (let ((,message (buffer-substring (+ (match-beginning 0) (length prefix)) (- (match-end 0) (length postfix)))) (,start (match-beginning 0))) (delete-region (match-beginning 0) (match-end 0)) + ;; FIXME probably don't need `start' at all. + (cl-assert (= (point) ,start)) (goto-char ,start) ,@body) (erc-restore-text-properties)))))) @@ -417,9 +404,9 @@ and the CIPHERTEXT, which must be BASE64 encoded as well." (error-message-string ex)) nil))) -;; FIXME use new API expecting an `erc-input' object instead of a STRING. -(defun erc-crypt-maybe-send (string) - "Encrypt STRING and send to receiver. Run as a hook in `erc-send-pre-hook'. +(defun erc-crypt-maybe-send (input) + "Encrypt `string' slot of `erc-input' object INPUT. +;; FIXME string ~~> input ... STRING should contain user input. In order to get around IRC protocol message size limits, STRING is split into fragments and padded to a constant size, `erc-crypt-max-length', by calling `erc-crypt-split-message'. @@ -430,22 +417,21 @@ formatting preserved intact. On errors, do not send STRING to the server." (when (and erc-crypt-mode erc-crypt-key-file ;; Skip ERC commands - (not (string= "/" (substring string 0 1)))) - (let* ((split (erc-crypt-split-message string)) - (encrypted (mapcar #'erc-crypt-encrypt split))) - (cond ((cl-some #'null encrypted) - (erc-crypt--message "Message will not be sent") - (with-suppressed-warnings ((obsolete erc-send-this)) - (setq erc-send-this nil))) - (t - ;; str is dynamically bound - (with-suppressed-warnings ((lexical str)) (defvar str)) - (setq erc-crypt-message str - str (concat erc-crypt-prefix - (cl-first encrypted) - erc-crypt-postfix) - erc-crypt--left-over - (cl-rest encrypted))))))) + (not (string-prefix-p "/" (erc-input-string input)))) + (if-let ((clear-str (erc-input-string input)) + (encrypted (mapcar #'erc-crypt-encrypt + (erc-crypt-split-message clear-str))) + ;; No need to invert with `not' if `encrypted' is non-nil. + ((not (cl-some #'null encrypted))) + (bookended (mapcar (lambda (encrypted-msg-body) + (concat erc-crypt-prefix + encrypted-msg-body + erc-crypt-postfix)) + encrypted))) + (setf (erc-input-substxt input) clear-str + (erc-input-string input) (string-join bookended "\n")) + (erc-crypt--message "Message will not be sent") + (setf (erc-input-sendp input) nil)))) (defun erc-crypt-find-key () @@ -463,7 +449,7 @@ On errors, do not send STRING to the server." channel;<--- channel or friend directory - 4rd %s channel));<- channel or friend content (key-exists (file-exists-p key-path)));<- if key found - (when key-exists (progn (erc-crypt-enable);<-- then enable erc-crypt + (when key-exists (progn (unless erc-crypt-mode (erc-crypt-enable)) (setq erc-crypt-key-file key-path))))));<- set ;; path of key @@ -474,18 +460,17 @@ Needed for receiving public keys and signature." (unless erc-crypt-mode (when (eq major-mode 'erc-mode) (when (string-match "----CRYPT ON----" string) - (erc-crypt-enable))))) + (erc-crypt-mode +4))))) (defun erc-crypt-maybe-send-fixup () "Restore encrypted message back to its plaintext form. This happens inside `erc-send-modify-hook'." - (erc-crypt--with-message (_) - (insert erc-crypt-message) - (goto-char (point-min)) - (insert (concat (propertize erc-crypt-indicator 'face - (list :foreground erc-crypt-success-color)) - " ")))) + (when erc-crypt-mode + ;; HACK bind `erc-crypt--insert-queue' to avoid interfering with ongoing + ;; receipt. FIXME don't do ^ + (let (erc-crypt--insert-queue) + (erc-crypt--insert "")))) (cl-defun erc-crypt-string-check (string) @@ -546,20 +531,31 @@ Does not display message and does not trigger `erc-insert-modify-hook'." ;; Error, erc-insert-this will be set to t so it's not possible ;; for multiple error-indicating conses to be inserted in the ;; queue. - (push (cons :error nil) erc-crypt--insert-queue))))) + (push (cons :error nil) erc-crypt--insert-queue)))) + (when erc-crypt--insert-queue + (force-mode-line-update))) + +;; Maybe optionize this or similar. +(defvar erc-crypt-indicator-style 'after-speaker) (defun erc-crypt--insert (msg &optional error) + "Insert (ERROR) MSG with `erc-crypt-indicator'." (insert (concat (if error "(decrypt error) " "") (decode-coding-string msg 'utf-8 :nocopy))) (goto-char (point-min)) - (insert (concat - (propertize - erc-crypt-indicator 'face - (list :foreground - (if error - erc-crypt-failure-color erc-crypt-success-color))) - " ")) + (when-let ((beg (text-property-not-all (point) (point-max) + 'erc--speaker nil))) + (goto-char (if (eq erc-crypt-indicator-style 'after-speaker) + (next-single-property-change beg 'erc--speaker) + beg))) + ;; FIXME cache this and/or use defined faces instead of anonymous ones. + (insert (propertize "‍" + 'display erc-crypt-indicator + 'font-lock-face (list :foreground + (if error + erc-crypt-failure-color + erc-crypt-success-color)))) (setq erc-crypt--insert-queue nil)) ;; FIXME integrate the following into (around) `erc-crypt-maybe-insert' body @@ -663,6 +659,8 @@ This happens inside `erc-insert-modify-hook'." (erc-crypt-move-keys tempdir dir) + ;; FIXME don't kill buffers in `erc-insert-post-hook'; if you + ;; must, use `erc-insert-done-hook' instead (set-buffer ;; buffer with received keys is unneeded now (car (erc-buffer-list-with-nick nick @@ -691,6 +689,7 @@ This happens inside `erc-insert-modify-hook'." "Verify received ed25519 signature, then check verified status in TEMPDIR. If positive copy to DIR / NICK directory and delete after original ~/.emasc.d/irc/erc-crypt/temp/verify-status after all." + ;; FIXME `tempdir' ends in a slash. so this results in //nick. (let* ((keyname (concat tempdir "/" nick)) (status (call-process erc-crypt-openssl-path @@ -714,6 +713,17 @@ Then move to KEYFILE with filename NICK." "--peerkey" (concat tempdir nick "-x25519_pub.pem") "-out" keyfile)) +;; XXX if this is meant to find the speaker of the previous chat message, +;; you'll have to `save-restriction' and `widen' before searching backward, +;; because this runs in a narrowed buffer. If, OTOH, this is meant to find +;; the speaker of the *current* message, you can instead do: +;; +;; (erc-get-parsed-vector-nick (erc-get-parsed-vector)) +;; +;; However, there's no real need to re-parse the original sender because ERC +;; already knows the speaker's nick but doesn't provide a way for third +;; parties to access it. Please file a bug report with M-x erc-bug RET if +;; you feel such functionality should be exposed. (defun erc-crypt-get-last-message-nick () "Get the nickname of the last message in the ERC chat buffer." (interactive) @@ -728,19 +738,6 @@ Then move to KEYFILE with filename NICK." (nick (match-string 0 nick-line))) ;; bind nick to variable (goto-char (point-max)) nick)))) -;; return nick - -(defun erc-crypt-post-send (_) - "Send message fragments placed in `erc-crypt--left-over' to remote end. -STRING is unused, but required." - (unwind-protect - (cl-loop for m in erc-crypt--left-over do - (erc-message "PRIVMSG" - (concat (erc-default-target) " " - (concat erc-crypt-prefix m erc-crypt-postfix)) - )) - (setq erc-crypt--left-over nil))) - (defun erc-crypt-split-message (string) "Split STRING and pad to maximum size if needed." @@ -797,17 +794,15 @@ It is used for verify x25519_pub.pem key." ed-b64))) (defun erc-crypt-dh-ed-sig-read () - "Read ed25519_sig.bin signature file found in `erc-crypt--dir'/secret/ dir. -It is used for verify x25519_pub.pem key." - (with-temp-buffer - (insert-file-contents (concat - erc-crypt--dir "secret/ed25519_sig.bin")) - (let* ((sig-b64 (base64-encode-string (buffer-string))) - (sig-split (erc-crypt-split-dh sig-b64)) - (sig-b64 (mapcar #'(lambda (sig-split) - (base64-encode-string sig-split t)) - sig-split))) - sig-b64))) + "Read ed25519_sig.bin signature file from `erc-crypt--dir'/secret/. +Use it to verify x25519_pub.pem in the same directory." + (with-temp-buffer + (set-buffer-multibyte nil) + (insert-file-contents-literally + (concat erc-crypt--dir "secret/ed25519_sig.bin")) + (let* ((sig-b64 (base64-encode-string (buffer-string))) + (sig-split (erc-crypt-split-dh sig-b64))) + (mapcar (lambda (split) (base64-encode-string split t)) sig-split)))) (defun erc-crypt-dh-ex (nick) "Read and public keys and signature and send to NICK." @@ -922,7 +917,7 @@ x25519 public key." (erc-crypt--message "Keypairs generated and signed succesfully.") (erc-crypt--message "Generate keypairs or signing failed.")))) - +;; FIXME autoload this. (defun erc-cmd-CRYPT (option &optional &rest args) ; FIXME 2x &foo (progn (cond ((string= option "genkeys") @@ -957,29 +952,5 @@ x25519 public key." (erc-send-input (substring (format "%s" args) 1 (- len 1)))) (erc-crypt-enable))))) -;;; -;;; Interactive -;;; - -;; FIXME consider using `define-erc-module' with a non-nil `local', instead of -;; `define-minor-mode' above. It gives you these enable/disable helpers for -;; free and a bunch of symbol properties and other niceties that ERC relies on -;; for Customize integrations and so forth. May have to condition the enable -;; body on `erc-crypt-find-key' passing, and disable the mode if it fails. - -;;;###autoload -(defun erc-crypt-enable () - "Enable PSK encryption for the current buffer." - (interactive) - (when (eq major-mode 'erc-mode) t) - (erc-crypt-mode 1)) - -;;;###autoload -(defun erc-crypt-disable () - "Disable PSK encryption for the current buffer." - (interactive) - (when (eq major-mode 'erc-mode) t) - (erc-crypt-mode -1)) - (provide 'erc-crypt) ;;; erc-crypt.el ends here -- 2.42.0