branch: elpa/age
commit f312d1a6693aa2355661474d7326dfc7928a1e81
Author: Bas Alberts <[email protected]>
Commit: Bas Alberts <[email protected]>

    Initial commit
---
 age.el | 1189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 1189 insertions(+)

diff --git a/age.el b/age.el
new file mode 100644
index 00000000000..9b3d13bdf51
--- /dev/null
+++ b/age.el
@@ -0,0 +1,1189 @@
+;;; age.el --- the Age Library -*- lexical-binding: t -*-
+
+;; EPG/EPA modified to work with Age*
+;;
+;; * https://github.com/FiloSottile/age
+
+;; Maintainer: Bas Alberts <[email protected]>
+;; Keywords: emacs
+;; Version: 0.1
+
+;; This is intended to provide transparent Age based file encryption
+;; and decryption in Emacs. As such age.el does not support all
+;; Age CLI based use cases. Rather age.el assumes you have configured
+;; a default identity and a default recipient, e.g. based off your
+;; ssh private key and ssh public key in ~/.ssh/id_rsa[.pub], which
+;; is the default setting.
+
+;; The main use case is for folks who like to e.g. encrypt their org
+;; notes and things of that nature. Since age.el provides is a direct
+;; port from EPG/EPA it can support all roles that .gpg files can
+;; support in Emacs, e.g. ~/.authinfo.age should work fine as well.
+
+;; Usage:
+;;
+;; Put age.el somewhere in your load-path and:
+;;
+;; (require 'age)
+;; (age-file-enable)
+;;
+;; age.el also supports creating new .age files through find-file and
+;; they will be encrypted to your default recipient on first save.
+
+;; Known issues:
+;;
+;; The Age CLI does not support pinentry by design. Users are encouraged
+;; to use identity (private) keys and recipient (public) keys, and manage
+;; those secrets outside of Emacs accordingly. As such age.el does not
+;; currently support passphrase based Age Encryption/Decryption as we
+;; do not have a tty available to provide a passphrase to Age (I think).
+
+;; Original copyright notice:
+
+;; Copyright (C) 1999-2000, 2002-2022 Free Software Foundation, Inc.
+
+;; Author: Daiki Ueno <[email protected]>
+;; Keywords: emacs
+;; Version: 1.0.0
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is just a reworked version of epg.el, so original copyright applies.
+
+;;; Code:
+
+(require 'rfc6068)
+(eval-when-compile (require 'cl-lib))
+(eval-when-compile (require 'subr-x))
+
+;;; Configuration
+
+(defconst age-package-name "age"
+  "Name of this package.")
+
+(defconst age-version-number "0.1"
+  "Version number of this package.")
+
+;;; Options
+
+(defgroup age ()
+  "Interface to Age."
+  :tag "Age"
+  :version "27.1"
+  :group 'data
+  :group 'external)
+
+(defcustom age-default-recipient (expand-file-name "~/.ssh/id_rsa.pub")
+  "Default recipient to use for age (public key).
+
+This file can contain multiple recipients, one per line."
+  :type 'file)
+
+(defcustom age-default-identity (expand-file-name "~/.ssh/id_rsa")
+  "Default identity to use for age (private key).
+
+This file can contain multiple identities, one per line."
+  :type 'file)
+
+(defcustom age-always-use-default-keys t
+  "If non-nil, use default identities and recipients without nagging."
+  :type 'boolean)
+
+(defcustom age-program (executable-find "age")
+  "Say what age program to prefer."
+  :version "27.1"
+  :type 'string)
+
+(defcustom age-passphrase-coding-system nil
+  "Coding system to use with messages from `age-program'."
+  :type 'symbol)
+
+;; In the doc string below, we say "symbol `error'" to avoid producing
+;; a hyperlink for `error' the function.
+(defcustom age-pinentry-mode nil ;; XXX: leaving this in for now
+  "The pinentry mode."
+  :type '(choice (const nil)
+                (const ask)
+                (const cancel)
+                (const error))
+  :version "27.1")
+
+(defcustom age-debug nil
+  "If non-nil, debug output goes to the \"*age-debug*\" buffer."
+  :type 'boolean)
+
+;;; Constants
+
+(defconst age-minimum-version "1.0.0")
+
+(defconst age-config--program-alist
+  `((Age
+     age-program
+     ("age" . ,age-minimum-version)))
+  "Alist used to obtain the usable configuration of executables.
+The first element of each entry is protocol symbol, which is
+`Age'.  The second element is a symbol where the executable name
+is remembered.  The rest of the entry is an alist mapping executable
+names to the minimum required version suitable for the use with Emacs.")
+
+(defconst age-config--configuration-constructor-alist
+  '((Age . age-config--make-age-configuration))
+  "Alist used to obtain the usable configuration of executables.
+The first element of each entry is protocol symbol, which is
+either `Age'.  The second element is a function which constructs
+a configuration object (actually a plist).")
+
+;;; "Configuration"
+
+(defvar age--configurations nil)
+
+;;;###autoload
+(defun age-find-configuration (protocol &optional no-cache program-alist)
+  "Find or create a usable configuration to handle PROTOCOL.
+This function first looks at the existing configuration found by
+the previous invocation of this function, unless NO-CACHE is non-nil.
+
+Then it walks through PROGRAM-ALIST or
+`age-config--program-alist'.  If `age-program' is already set
+with custom, use it.
+
+Otherwise, it tries the programs listed in the entry until the
+version requirement is met."
+  (unless program-alist
+    (setq program-alist age-config--program-alist))
+  (let ((entry (assq protocol program-alist)))
+    (unless entry
+      (error "Unknown protocol `%S'" protocol))
+    (cl-destructuring-bind (symbol . alist)
+        (cdr entry)
+      (let ((constructor
+             (alist-get protocol age-config--configuration-constructor-alist)))
+        (or (and (not no-cache) (alist-get protocol age--configurations))
+            ;; If the executable value is already set with M-x
+            ;; customize, use it without checking.
+            (if (and symbol (or (get symbol 'saved-value)
+                                (get symbol 'customized-value)))
+                (let ((configuration
+                       (funcall constructor (symbol-value symbol))))
+                  (push (cons protocol configuration) age--configurations)
+                  configuration)
+              (catch 'found
+                (dolist (program-version alist)
+                  (let ((executable (executable-find (car program-version))))
+                    (when executable
+                      (let ((configuration
+                             (funcall constructor executable)))
+                        (when (ignore-errors
+                                (age-check-configuration configuration
+                                                         (cdr program-version))
+                                t)
+                          (unless no-cache
+                            (push (cons protocol configuration)
+                                  age--configurations))
+                          (throw 'found configuration)))))))))))))
+
+;; Create an `age-configuration' object for `age', using PROGRAM.
+(defun age-config--make-age-configuration (program)
+  (list (cons 'program program)
+        (cons 'version
+              ;; XXX: clean this up
+              (substring
+               (shell-command-to-string
+                (format "%s --version" program))
+               0 -1))))
+
+;;;###autoload
+(defun age-configuration ()
+  "Return a list of internal configuration parameters of `age-program'."
+  (age-config--make-age-configuration age-program))
+
+;;;###autoload
+(defun age-check-configuration (config &optional req-versions)
+  "Verify that a sufficient version of Age is installed.
+CONFIG should be a `age-configuration' object (a plist).
+REQ-VERSIONS should be a list with elements of the form (MIN
+. MAX) where MIN and MAX are version strings indicating a
+semi-open range of acceptable versions.  REQ-VERSIONS may also be
+a single minimum version string."
+  (let ((version (alist-get 'version config)))
+    (unless (stringp version)
+      (error "Undetermined version: %S" version))
+    (catch 'version-ok
+      (pcase-dolist ((or `(,min . ,max)
+                         (and min (let max nil)))
+                     (if (listp req-versions) req-versions
+                       (list req-versions)))
+        (when (and (version<= (or min age-minimum-version)
+                              version)
+                   (or (null max)
+                       (version< version max)))
+          (throw 'version-ok t)))
+      (error "Unsupported version: %s" version))))
+
+(defun age-required-version-p (protocol required-version)
+  "Verify a sufficient version of Age for specific protocol.
+PROTOCOL is `Age' or `CMS'.  REQUIRED-VERSION is a string
+containing the required version number.  Return non-nil if
+that version or higher is installed."
+  (let ((version (cdr (assq 'version (age-find-configuration protocol)))))
+    (and (stringp version)
+         (version<= required-version version))))
+
+(define-error 'age-error "Age error")
+
+;;; Variables
+
+(defvar age-read-point nil)
+(defvar age-process-filter-running nil)
+(defvar age-context nil)
+(defvar age-debug-buffer nil)
+
+;;; Enums
+
+(defconst age-invalid-recipients-reason-alist
+  '((0 . "unknown recipient type")))
+
+(defconst age-no-data-reason-alist
+  '((1 . "did you mean to use -a/--armor")))
+
+(defconst age-unexpected-reason-alist nil)
+
+(defvar age-prompt-alist nil)
+
+;;; Structs
+
+;;;; Data Struct
+
+(cl-defstruct (age-data
+               (:constructor nil)
+               (:constructor age-make-data-from-file (file))
+               (:constructor age-make-data-from-string (string))
+               (:copier nil)
+               (:predicate nil))
+  (file nil :read-only t)
+  (string nil :read-only t))
+
+;;;; Context Struct
+(declare-function age-passphrase-callback-function "age-mode.el")
+
+(cl-defstruct (age-context
+               (:constructor nil)
+               (:constructor age-context--make
+                             (protocol &optional armor
+                                       &aux
+                                       (program
+                                        (let ((configuration 
(age-find-configuration protocol)))
+                                          (unless configuration
+                                            (signal 'age-error
+                                                    (list "no usable 
configuration" protocol)))
+                                          (alist-get 'program 
configuration)))))
+               (:copier nil)
+               (:predicate nil))
+  protocol
+  program
+  armor
+  (passphrase-callback (list #'age-passphrase-callback-function))
+  edit-callback
+  process
+  output-file
+  result
+  operation
+  ;; XXX: no pinentry mode on Age, waiting for a plugin
+  (pinentry-mode age-pinentry-mode)
+  (error-output "")
+  error-buffer)
+
+;;;; Context Methods
+
+;; This is not an alias, just so we can mark it as autoloaded.
+;;;###autoload
+(defun age-make-context (&optional protocol armor)
+  "Return a context object."
+  (age-context--make (or protocol 'Age) armor))
+
+(defun age-context-set-armor (context armor)
+  "Specify if the output should be ASCII armored in CONTEXT."
+  (declare (obsolete setf "25.1"))
+  (setf (age-context-armor context) armor))
+
+;; XXX: unused currently, so... untested.
+(defun age-context-set-passphrase-callback (context
+                                           passphrase-callback)
+  "Set the function used to query passphrase.
+
+PASSPHRASE-CALLBACK is either a function, or a cons-cell whose
+car is a function and cdr is a callback data.
+
+The function gets three arguments: the context, the key-id in
+question, and the callback data (if any)."
+  ;; (declare (obsolete setf "25.1"))
+  (setf (age-context-passphrase-callback context)
+        (if (functionp passphrase-callback)
+           (list passphrase-callback)
+         passphrase-callback)))
+
+;;; Functions
+
+(defun age-context-result-for (context name)
+  "Return the result of CONTEXT associated with NAME."
+  (cdr (assq name (age-context-result context))))
+
+(defun age-context-set-result-for (context name value)
+  "Set the result of CONTEXT associated with NAME to VALUE."
+  (let* ((result (age-context-result context))
+        (entry (assq name result)))
+    (if entry
+       (setcdr entry value)
+      (setf (age-context-result context) (cons (cons name value) result)))))
+
+(defun age-error-to-string (error)
+  (cond
+   ;; general age-error
+   ((eq (car error) 'age-error)
+    (cadr error))
+   ;; XXX: give me a heads up if I'm not handling something yet
+   (t (message "XXX Translate this error: %s" error))))
+
+(defun age-errors-to-string (errors)
+  (mapconcat #'age-error-to-string errors "; "))
+
+(defun age--start (context args)
+  "Start `age-program' in a subprocess with given ARGS."
+  (if (and (age-context-process context)
+          (eq (process-status (age-context-process context)) 'run))
+      (error "%s is already running in this context"
+            (age-context-program context)))
+  (let* ((args (append
+               (if (age-context-armor context) '("--armor"))
+               (if (age-context-output-file context)
+                   (list "--output" (age-context-output-file context)))
+               args))
+        (process-environment process-environment)
+        (buffer (generate-new-buffer " *age*"))
+        error-process
+        process)
+    ;; XXX: don't need this, but probably will come in handy at some point
+    (setq process-environment
+         (cons (format "INSIDE_EMACS=%s,age" emacs-version)
+               process-environment))
+    (if age-debug
+       (save-excursion
+         (unless age-debug-buffer
+           (setq age-debug-buffer (generate-new-buffer "*age-debug*")))
+         (set-buffer age-debug-buffer)
+         (goto-char (point-max))))
+    (with-current-buffer buffer
+      (if (fboundp 'set-buffer-multibyte)
+         (set-buffer-multibyte nil))
+      (setq-local age-read-point (point-min))
+      (setq-local age-process-filter-running nil)
+      (setq-local age-context context))
+    ;; make sure our error buffer has access to buffer local context as well
+    (let ((error-buffer (generate-new-buffer "*age-error")))
+      (with-current-buffer error-buffer
+        (setq-local age-context context))
+      (setq error-process
+           (make-pipe-process :name "age-error"
+                              :buffer error-buffer
+                              ;; Suppress "XXX finished" line.
+                              :sentinel #'ignore
+                               :filter #'age--process-stderr-filter
+                              :noquery t))
+      (setf (age-context-error-buffer context) error-buffer))
+    (with-existing-directory
+      (with-file-modes 448
+        (setq process (make-process :name "age"
+                                   :buffer buffer
+                                   :command (cons (age-context-program context)
+                                                  args)
+                                   :connection-type 'pipe
+                                   :coding 'raw-text
+                                   :filter #'age--process-stdout-filter
+                                   :stderr error-process
+                                   :noquery t))))
+    (setf (age-context-process context) process)))
+
+(defun age--process-stdout-filter (process input)
+  (message "debug: age stdout: %s" input))
+
+(defun age--process-stderr-filter (process input)
+  (when age-debug
+    (with-current-buffer
+        (or age-debug-buffer
+            (setq age-debug-buffer (generate-new-buffer "*age-debug*")))
+      (goto-char (point-max))
+      (insert input)))
+  (when (buffer-live-p (process-buffer process))
+    (with-current-buffer (process-buffer process)
+      (unless age-process-filter-running
+        (let ((age-process-filter-running t)))
+        (string-match "age: error: \\(.*\\)" input)
+        (let ((error-msg (match-string 1 input)))
+          (when error-msg
+            ;; age-context is buffer local
+            (age-context-set-result-for age-context 'error `((age-error 
,error-msg)))
+            (age--status-AGE_FAILED age-context error-msg)))))))
+
+(defun age-read-output (context)
+  "Read the output file CONTEXT and return the content as a string."
+  (with-temp-buffer
+    (if (fboundp 'set-buffer-multibyte)
+       (set-buffer-multibyte nil))
+    (if (file-exists-p (age-context-output-file context))
+       (let ((coding-system-for-read 'binary))
+         (insert-file-contents (age-context-output-file context))
+         (buffer-string)))))
+
+(defun age-wait-for-completion (context)
+  "Wait until the `age-program' process completes."
+  (while (eq (process-status (age-context-process context)) 'run)
+    (accept-process-output (age-context-process context) 1))
+  ;; This line is needed to run the process-filter right now.
+  (sleep-for 0.1)
+  (age-context-set-result-for
+   context 'error
+   (nreverse (age-context-result-for context 'error)))
+  (setf (age-context-error-output context)
+       (with-current-buffer (age-context-error-buffer context)
+         (buffer-string))))
+
+(defun age-reset (context)
+  "Reset the CONTEXT."
+  (if (and (age-context-process context)
+          (buffer-live-p (process-buffer (age-context-process context))))
+      (kill-buffer (process-buffer (age-context-process context))))
+  (if (buffer-live-p (age-context-error-buffer context))
+      (kill-buffer (age-context-error-buffer context)))
+  (setf (age-context-process context) nil)
+  (setf (age-context-edit-callback context) nil))
+
+(defun age-delete-output-file (context)
+  "Delete the output file of CONTEXT."
+  (if (and (age-context-output-file context)
+          (file-exists-p (age-context-output-file context)))
+      (delete-file (age-context-output-file context))))
+
+;; XXX: completely untested, artifact from EPA's status handling
+;; XXX: rework this when we get a pinentry solution available
+(defun age--status-GET_PASSPHRASE (context string)
+  (when (string-match "\\`passphrase\\." string)
+    (unless (age-context-passphrase-callback context)
+      (error "Variable `passphrase-callback' not set"))
+    (let (inhibit-quit
+         passphrase
+         passphrase-with-new-line
+         encoded-passphrase-with-new-line)
+      (unwind-protect
+         (condition-case nil
+             (progn
+               (setq passphrase
+                     (funcall
+                      (car (age-context-passphrase-callback context))
+                      context
+                      (cdr (age-context-passphrase-callback context))))
+               (when passphrase
+                 (setq passphrase-with-new-line (concat passphrase "\n"))
+                 (clear-string passphrase)
+                 (setq passphrase nil)
+                 (if age-passphrase-coding-system
+                     (progn
+                       (setq encoded-passphrase-with-new-line
+                             (encode-coding-string
+                              passphrase-with-new-line
+                              (coding-system-change-eol-conversion
+                               age-passphrase-coding-system 'unix)))
+                       (clear-string passphrase-with-new-line)
+                       (setq passphrase-with-new-line nil))
+                   (setq encoded-passphrase-with-new-line
+                         passphrase-with-new-line
+                         passphrase-with-new-line nil))
+                 (process-send-string (age-context-process context)
+                                      encoded-passphrase-with-new-line)))
+           (quit
+            (age-context-set-result-for
+             context 'error
+             (cons '(quit)
+                   (age-context-result-for context 'error)))
+            (delete-process (age-context-process context))))
+       (if passphrase
+           (clear-string passphrase))
+       (if passphrase-with-new-line
+           (clear-string passphrase-with-new-line))
+       (if encoded-passphrase-with-new-line
+           (clear-string encoded-passphrase-with-new-line))))))
+
+;;; Status Functions
+
+(defun age--status-AGE_FAILED (context _string)
+  (age-context-set-result-for context 'age-failed t))
+
+;;; Public Functions
+
+(defun age-cancel (context)
+  (if (buffer-live-p (process-buffer (age-context-process context)))
+      (with-current-buffer (process-buffer (age-context-process context))
+       (age-context-set-result-for
+        age-context 'error
+        (cons '(quit)
+              (age-context-result-for age-context 'error)))))
+  (if (eq (process-status (age-context-process context)) 'run)
+      (delete-process (age-context-process context))))
+
+(defun age-start-decrypt (context cipher)
+  "Initiate a decrypt operation on CIPHER.
+CIPHER must be a file data object.
+
+If you use this function, you will need to wait for the completion of
+`age-program' by using `age-wait-for-completion' and call
+`age-reset' to clear a temporary output file.
+If you are unsure, use synchronous version of this function
+`age-decrypt-file' or `age-decrypt-string' instead."
+  (unless (age-data-file cipher)
+    (error "Not a file"))
+  (setf (age-context-operation context) 'decrypt)
+  (setf (age-context-result context) nil)
+  (let ((identity
+         (if (or age-always-use-default-keys
+                 (y-or-n-p "Use default identity? "))
+             age-default-identity
+           (read-file-name "Path to identity: " (expand-file-name "~/")))))
+    (age--start context (list "--decrypt" "--identity" identity "--" 
(age-data-file cipher)))))
+
+(defun age--check-error-for-decrypt (context)
+  (let ((errors (age-context-result-for context 'error)))
+    (if (age-context-result-for context 'age-failed)
+       (signal 'age-error
+               (list "Age failed with error" (age-errors-to-string errors))))))
+
+(defun age-decrypt-file (context cipher plain)
+  "Decrypt a file CIPHER and store the result to a file PLAIN.
+If PLAIN is nil, it returns the result as a string."
+  (unwind-protect
+      (progn
+       (setf (age-context-output-file context)
+              (or plain (make-temp-file "age-output")))
+        ;;(message "XXX: context: %s" context)
+        (age-start-decrypt context (age-make-data-from-file cipher))
+        (age-wait-for-completion context)
+        ;; XXX: replace this with a simpler error passing thing
+       (age--check-error-for-decrypt context)
+        (unless plain
+         (age-read-output context)))
+    (unless plain
+      (age-delete-output-file context))
+    (age-reset context)))
+
+(defun age-decrypt-string (context cipher)
+  "Decrypt a string CIPHER and return the plain text."
+  (let ((input-file (make-temp-file "age-input"))
+       (coding-system-for-write 'binary))
+    (unwind-protect
+       (progn
+         (write-region cipher nil input-file nil 'quiet)
+         (setf (age-context-output-file context)
+                (make-temp-file "age-output"))
+         (age-start-decrypt context (age-make-data-from-file input-file))
+         (age-wait-for-completion context)
+         (age--check-error-for-decrypt context)
+         (age-read-output context))
+      (age-delete-output-file context)
+      (if (file-exists-p input-file)
+         (delete-file input-file))
+      (age-reset context))))
+
+(defun age-start-encrypt (context plain recipients)
+  "Initiate an encrypt operation on PLAIN.
+PLAIN is a data object.
+If RECIPIENTS is nil, it performs symmetric encryption.
+
+If you use this function, you will need to wait for the completion of
+`age-program' by using `age-wait-for-completion' and call
+`age-reset' to clear a temporary output file.
+If you are unsure, use synchronous version of this function
+`age-encrypt-file' or `age-encrypt-string' instead."
+  (setf (age-context-operation context) 'encrypt)
+  (setf (age-context-result context) nil)
+  ;; XXX: fixme ... we _ALWAYS_ need recipients
+  (let ((recipients (or recipients
+                        (age-select-keys
+                         context
+                         "Select recipients for encryption."))))
+    (age--start context
+                ;; if recipients is nil, we go to the default identity
+               (append '("--encrypt")
+                       (apply #'nconc
+                              (mapcar
+                               (lambda (recipient)
+                                  ;; recipients is a list of age public keys
+                                  (when age-debug
+                                    (message "Adding recipient: %s" recipient))
+                                 (list "-r" recipient))
+                               recipients))
+                       (if (age-data-file plain)
+                           (list "--" (age-data-file plain))))))
+  (when (age-data-string plain)
+    (if (eq (process-status (age-context-process context)) 'run)
+       (process-send-string (age-context-process context)
+                            (age-data-string plain)))
+    (if (eq (process-status (age-context-process context)) 'run)
+       (process-send-eof (age-context-process context)))))
+
+(defun age-encrypt-file (context plain recipients cipher)
+  "Encrypt a file PLAIN and store the result to a file CIPHER.
+If CIPHER is nil, it returns the result as a string.
+If RECIPIENTS is nil, it performs symmetric encryption."
+  (unwind-protect
+      (progn
+        (setf (age-context-output-file context)
+              (or cipher (make-temp-file "age-output")))
+       (age-start-encrypt context (age-make-data-from-file plain) recipients)
+       (age-wait-for-completion context)
+       (let ((errors (age-context-result-for context 'error)))
+         (if errors
+             (signal 'age-error
+                     (list "Encrypt failed" (age-errors-to-string errors)))))
+       (unless cipher
+         (age-read-output context)))
+    (unless cipher
+      (age-delete-output-file context))
+    (age-reset context)))
+
+(defun age-encrypt-string (context plain recipients)
+  "Encrypt a string PLAIN.
+If RECIPIENTS is nil, it performs symmetric encryption."
+  (let ((input-file
+         ;; XXX: this is always true, but keep the protocol flexibility for now
+        (when (eq (age-context-protocol context) 'Age)
+          (make-temp-file "age-input")))
+       (coding-system-for-write 'binary))
+    (unwind-protect
+       (progn
+         (setf (age-context-output-file context)
+                (make-temp-file "age-output"))
+         (if input-file
+             (write-region plain nil input-file nil 'quiet))
+         (age-start-encrypt context
+                            (if input-file
+                                (age-make-data-from-file input-file)
+                              (age-make-data-from-string plain))
+                            recipients)
+         (age-wait-for-completion context)
+         (let ((errors (age-context-result-for context 'error)))
+           (if errors
+               (signal 'age-error
+                       (list "Encrypt failed" (age-errors-to-string errors)))))
+         (age-read-output context))
+      (age-delete-output-file context)
+      (if input-file
+         (delete-file input-file))
+      (age-reset context))))
+
+;;; Decode Functions
+
+(defun age--decode-percent-escape (string)
+  (setq string (encode-coding-string string 'raw-text))
+  (let ((index 0))
+    (while (string-match "%\\(\\(%\\)\\|\\([[:xdigit:]][[:xdigit:]]\\)\\)"
+                        string index)
+      (if (match-beginning 2)
+         (setq string (replace-match "%" t t string)
+               index (1- (match-end 0)))
+       (setq string (replace-match
+                     (byte-to-string
+                       (string-to-number (match-string 3 string) 16))
+                     t t string)
+             index (- (match-end 0) 2))))
+    string))
+
+(defun age--decode-percent-escape-as-utf-8 (string)
+  (declare (obsolete rfc6068-unhexify-string "28.1"))
+  (decode-coding-string (age--decode-percent-escape string) 'utf-8))
+
+(defun age--decode-hexstring (string)
+  (declare (obsolete rfc6068-unhexify-string "28.1"))
+  (let ((index 0))
+    (while (eq index (string-match "[[:xdigit:]][[:xdigit:]]" string index))
+      (setq string (replace-match (string (string-to-number
+                                          (match-string 0 string) 16))
+                                 t t string)
+           index (1- (match-end 0))))
+    string))
+
+(defun age--decode-quotedstring (string)
+  (let ((index 0))
+    (while (string-match "\\\\\\(\\([,=+<>#;\\\"]\\)\\|\
+\\([[:xdigit:]][[:xdigit:]]\\)\\)"
+                        string index)
+      (if (match-beginning 2)
+         (setq string (replace-match "\\2" t nil string)
+               index (1- (match-end 0)))
+       (if (match-beginning 3)
+           (setq string (replace-match (string (string-to-number
+                                                (match-string 0 string) 16))
+                                       t t string)
+                 index (- (match-end 0) 2)))))
+    string))
+
+;;; File mode hooks
+
+(defcustom age-file-name-regexp "\\.age\\'"
+  "Age file name regexp."
+  :type 'regexp
+  :group 'age-file)
+
+(defcustom age-file-inhibit-auto-save t
+  "If non-nil, disable auto-saving when opening an encrypted file."
+  :type 'boolean
+  :group 'age-file)
+
+(defvar age-file-encrypt-to nil
+  "Recipient(s) used for encrypting files.
+May either be a string or a list of strings.")
+
+(put 'age-file-encrypt-to 'safe-local-variable
+     #'(lambda (val)
+        (or (stringp val)
+            (and (listp val)
+                 (catch 'safe
+                   (mapc (lambda (elt)
+                           (unless (stringp elt)
+                             (throw 'safe nil)))
+                         val)
+                   t)))))
+
+(put 'age-file-encrypt-to 'permanent-local t)
+
+(defvar age-file-handler
+  (cons age-file-name-regexp 'age-file-handler))
+
+(defvar age-file-auto-mode-alist-entry
+  (list age-file-name-regexp nil 'age-file))
+
+(defun age-file-find-file-hook ()
+  (if (and buffer-file-name
+          (string-match age-file-name-regexp buffer-file-name)
+          age-file-inhibit-auto-save)
+      (auto-save-mode 0)))
+
+(define-minor-mode age-encryption-mode
+  "Toggle automatic Age file encryption/decryption (Age Encryption mode)."
+  :global t :init-value t :group 'age-file :version "0.1"
+  ;;:initialize 'custom-initialize-delay
+  (setq file-name-handler-alist (delq age-file-handler 
file-name-handler-alist))
+  (remove-hook 'find-file-hook #'age-file-find-file-hook)
+  (setq auto-mode-alist (delq age-file-auto-mode-alist-entry))
+  (when age-encryption-mode
+    (setq file-name-handler-alist (cons age-file-handler 
file-name-handler-alist))
+    (add-hook 'find-file-hook 'age-file-find-file-hook)
+    (setq auto-mode-alist (cons age-file-auto-mode-alist-entry auto))))
+
+(put 'age-file-handler 'safe-magic t)
+(put 'age-file-handler 'operations '(write-region insert-file-contents))
+
+;;; age-file
+
+;;; Options
+
+(defcustom age-file-cache-passphrase-for-symmetric-encryption nil
+  "If non-nil, cache passphrase for symmetric encryption."
+  :type 'boolean
+  :group 'age-file)
+
+(defcustom age-file-select-keys nil
+  "Control whether or not to pop up the key selection dialog.
+
+If t, always ask user to select recipients.
+If nil, query user only when `age-file-encrypt-to' is not set.
+If neither t nor nil, don't ask user.  In this case, symmetric
+encryption is used."
+  :type '(choice (const :tag "Ask always" t)
+                (const :tag "Ask when recipients are not set" nil)
+                (const :tag "Don't ask" silent))
+  :group 'age-file)
+
+;;; Other
+
+(defvar age-file-passphrase-alist nil)
+
+;; XXX: fixme when we have a pinentry available
+(defun age-passphrase-callback-function (context handback)
+  (read-passwd
+   (format "Passphrase%s: "
+          ;; Add the file name to the prompt, if any.
+          (if (stringp handback)
+              (format " for %s" handback)
+            ""))
+   (eq (age-context-operation context) 'encrypt)))
+
+;; XXX: fixme when we have a pinentry available
+(defun age-file-passphrase-callback-function (context key-id file)
+  (if age-file-cache-passphrase-for-symmetric-encryption
+      (progn
+        (setq file (file-truename file))
+        (let ((entry (assoc file age-file-passphrase-alist))
+             passphrase)
+         (or (copy-sequence (cdr entry))
+             (progn
+               (unless entry
+                 (setq entry (list file))
+                 (setq age-file-passphrase-alist
+                       (cons entry
+                             age-file-passphrase-alist)))
+               (setq passphrase (age-passphrase-callback-function context
+                                                                  file))
+               (setcdr entry (copy-sequence passphrase))
+               passphrase))))
+    (age-passphrase-callback-function context file)))
+
+;;; Utilities
+
+(defvar age-error-buffer nil)
+(defvar age-suppress-error-buffer nil)
+
+(defun age-display-error (context)
+  (unless (or (equal (age-context-error-output context) "")
+              age-suppress-error-buffer)
+    (let ((buffer (get-buffer-create "*Error*")))
+      (save-selected-window
+       (unless (and age-error-buffer (buffer-live-p age-error-buffer))
+         (setq age-error-buffer (generate-new-buffer "*Error*")))
+       (if (get-buffer-window age-error-buffer)
+           (delete-window (get-buffer-window age-error-buffer)))
+       (with-current-buffer buffer
+         (let ((inhibit-read-only t)
+               buffer-read-only)
+           (erase-buffer)
+           (insert (format
+                    (pcase (age-context-operation context)
+                      ('decrypt "Error while decrypting with \"%s\":")
+                      ('encrypt "Error while encrypting with \"%s\":")
+                      (_ "Error while executing \"%s\":\n\n"))
+                    (age-context-program context))
+                   "\n\n"
+                   (age-context-error-output context)))
+          (goto-char (point-min)))
+       (display-buffer buffer)))))
+
+;;; File Handler
+
+(defvar age-inhibit nil
+  "Non-nil means don't try to decrypt .age files when operating on them.")
+
+;;;###autoload
+(defun age-file-handler (operation &rest args)
+  (save-match-data
+    (let ((op (get operation 'age-file)))
+      (if (and op (not age-inhibit))
+          (apply op args)
+       (age-file-run-real-handler operation args)))))
+
+(defun age-file-run-real-handler (operation args)
+  (let ((inhibit-file-name-handlers
+        (cons 'age-file-handler
+              (and (eq inhibit-file-name-operation operation)
+                   inhibit-file-name-handlers)))
+       (inhibit-file-name-operation operation))
+    (apply operation args)))
+
+(defun age-file-decode-and-insert (string file visit beg end replace)
+  (save-restriction
+    (narrow-to-region (point) (point))
+    (insert string)
+    (decode-coding-inserted-region
+     (point-min) (point-max)
+     (substring file 0 (string-match age-file-name-regexp file))
+     visit beg end replace)
+    (goto-char (point-max))
+    (- (point-max) (point-min))))
+
+(defvar age-file-error nil)
+(defun age-file--find-file-not-found-function ()
+  (let ((error age-file-error))
+    (save-window-excursion
+      (kill-buffer))
+    (if (nth 3 error)
+        (user-error "Wrong passphrase: %s" (nth 3 error))
+      (signal 'file-missing
+             (cons "Opening input file" (cdr error))))))
+
+(defun age--wrong-password-p (context)
+  (let ((error-string (age-context-error-output context)))
+    (and (string-match "\\(incorrect passphrase\\)"
+                       error-string)
+         (match-string 1 error-string))))
+
+(defvar last-coding-system-used)
+(defun age-file-insert-file-contents (file &optional visit beg end replace)
+  (barf-if-buffer-read-only)
+  (if (and visit (or beg end))
+      (error "Attempt to visit less than an entire file"))
+  (setq file (expand-file-name file))
+  (let* ((local-copy
+         (condition-case nil
+             (age-file-run-real-handler #'file-local-copy (list file))
+           (error)))
+        (local-file (or local-copy file))
+        (context (age-make-context))
+         (buf (current-buffer))
+        string length entry)
+    (if visit
+       (setq buffer-file-name file))
+    (age-context-set-passphrase-callback
+     context
+     (cons #'age-file-passphrase-callback-function
+          local-file))
+    (unwind-protect
+       (progn
+         (condition-case error
+             (setq string (age-decrypt-file context local-file nil))
+           (error
+             (if (setq entry (assoc file age-file-passphrase-alist))
+                (setcdr entry nil))
+            ;; If the decryption program can't be found,
+            ;; signal that as a non-file error
+            ;; so that find-file-noselect-1 won't handle it.
+            ;; Borrowed from jka-compr.el.
+            (if (and (memq 'file-error (get (car error) 'error-conditions))
+                     (equal (cadr error) "Searching for program"))
+                (error "Decryption program `%s' not found"
+                       (nth 3 error)))
+            (let ((exists (file-exists-p local-file)))
+              (when exists
+                 (if-let ((wrong-password (age--wrong-password-p context)))
+                     ;; Don't display the *error* buffer if we just
+                     ;; have a wrong password; let the later error
+                     ;; handler notify the user.
+                     (setq error (append error (list wrong-password)))
+                  (age-display-error context))
+                 (if (equal (caddr error) "Unexpected; Exit")
+                     (setq string (with-temp-buffer
+                                    (insert-file-contents-literally local-file)
+                                    (buffer-string)))
+                  ;; Hack to prevent find-file from opening empty buffer
+                  ;; when decryption failed (bug#6568).  See the place
+                  ;; where `find-file-not-found-functions' are called in
+                  ;; `find-file-noselect-1'.
+                  (setq-local age-file-error error)
+                  (add-hook 'find-file-not-found-functions
+                            'age-file--find-file-not-found-function
+                            nil t)))
+              (signal (if exists 'file-error 'file-missing)
+                      (cons "Opening input file" (cdr error))))))
+          (set-buffer buf) ;In case timer/filter changed/killed it (bug#16029)!
+         (setq-local age-file-encrypt-to
+                      (mapcar #'car (age-context-result-for
+                                     context 'encrypted-to)))
+         (if (or beg end)
+              (setq string (substring string
+                                      (or beg 0)
+                                      (and end (min end (length string))))))
+         (save-excursion
+           ;; If visiting, bind off buffer-file-name so that
+           ;; file-locking will not ask whether we should
+           ;; really edit the buffer.
+           (let ((buffer-file-name
+                  (if visit nil buffer-file-name)))
+              (setq length
+                    (if replace
+                        (age-file--replace-text string file visit beg end)
+                     (age-file-decode-and-insert
+                       string file visit beg end replace))))
+           (if visit
+               (set-visited-file-modtime))))
+      (if (and local-copy
+              (file-exists-p local-copy))
+         (delete-file local-copy)))
+    (list file length)))
+
+(put 'insert-file-contents 'age-file 'age-file-insert-file-contents)
+
+(defun age-file--replace-text (string file visit beg end)
+  ;; The idea here is that we want to replace the text in the buffer
+  ;; (for instance, for a `revert-buffer'), but we want to touch as
+  ;; little of the text as possible.  So we compare the new and the
+  ;; old text and only starts replacing when the text changes.
+  (let ((orig-point (point))
+        new-start length)
+    (goto-char (point-max))
+    (setq new-start (point))
+    (setq length
+         (age-file-decode-and-insert
+           string file visit beg end t))
+    (if (equal (buffer-substring (point-min) new-start)
+               (buffer-substring new-start (point-max)))
+        ;; The new text is equal to the old, so just keep the old.
+        (delete-region new-start (point-max))
+      ;; Compute the region the hard way.
+      (let ((p1 (point-min))
+            (p2 new-start))
+        (while (and (< p1 new-start)
+                    (< p2 (point-max))
+                    (eql (char-after p1) (char-after p2)))
+          (cl-incf p1)
+          (cl-incf p2))
+        (delete-region new-start p2)
+        (delete-region p1 new-start)))
+    ;; Restore point, if possible.
+    (if (< orig-point (point-max))
+        (goto-char orig-point)
+      (goto-char (point-max)))
+    length))
+
+;; XXX: always armor for now, let bind this otherwise
+(defvar age-armor t
+  "Controls whether or not Age encrypted files will be ASCII armored.")
+
+;; XXX lazy hacks abound, clean this up
+(defun age-select-keys (context msg &optional recipients)
+  ;; file mode
+  (let* ((recipients-file
+          (if (or age-always-use-default-keys
+                  (y-or-n-p "Use default recipient? "))
+              age-default-recipient
+            (read-file-name "Path to recipient: " (expand-file-name "~/"))))
+         ;; XXX: kludge, fixme
+         (recipients
+          (or recipients
+              (cl-loop
+               for recipient in
+               (remove ""
+                       (split-string
+                        (with-temp-buffer
+                          (insert-file-contents
+                           (expand-file-name recipients-file))
+                          (buffer-string))
+                        "\n"))
+               unless (string-match-p "^#" recipient)
+               collect recipient))))
+    ;; make sure this is buffer-local
+    (setq-local age-file-encrypt-to recipients)))
+
+(defun age-file-write-region (start end file &optional append visit lockname 
mustbenew)
+  (if append
+      (error "Can't append to the file"))
+  (setq file (expand-file-name file))
+  (let* ((coding-system (or coding-system-for-write
+                           (if (fboundp 'select-safe-coding-system)
+                               (let ((buffer-file-name file))
+                                 (select-safe-coding-system
+                                  (point-min) (point-max)))
+                             buffer-file-coding-system)))
+        (context (age-make-context))
+        (coding-system-for-write 'binary)
+        string entry
+        (recipients
+         (cond
+          ((listp age-file-encrypt-to) age-file-encrypt-to)
+          ((stringp age-file-encrypt-to) (list age-file-encrypt-to))))
+        buffer)
+    (age-context-set-passphrase-callback
+     context
+     (cons #'age-file-passphrase-callback-function
+          file))
+    (setf (age-context-armor context) age-armor)
+    (condition-case error
+       (setq string
+             (age-encrypt-string
+              context
+              (if (stringp start)
+                  (encode-coding-string start coding-system)
+                (unless start
+                  (setq start (point-min)
+                        end (point-max)))
+                (setq buffer (current-buffer))
+                (with-temp-buffer
+                  (insert-buffer-substring buffer start end)
+                  ;; Translate the region according to
+                  ;; `buffer-file-format', as `write-region' would.
+                  ;; We can't simply do `write-region' (into a
+                  ;; temporary file) here, since it writes out
+                  ;; decrypted contents.
+                  (format-encode-buffer (with-current-buffer buffer
+                                          buffer-file-format))
+                  (encode-coding-string (buffer-string)
+                                        coding-system)))
+              (if (or (eq age-file-select-keys t)
+                      (and (null age-file-select-keys)
+                           (not (local-variable-p 'age-file-encrypt-to
+                                                  (current-buffer)))))
+                  (age-select-keys
+                   context
+                   "Select recipients for encryption."
+                   recipients))))
+      (error
+       (age-display-error context)
+       (if (setq entry (assoc file age-file-passphrase-alist))
+          (setcdr entry nil))
+       (signal 'file-error (cons "Opening output file" (cdr error)))))
+    (age-file-run-real-handler
+     #'write-region
+     (list string nil file append visit lockname mustbenew))
+    (if (boundp 'last-coding-system-used)
+       (setq last-coding-system-used coding-system))
+    (if (eq visit t)
+       (progn
+         (setq buffer-file-name file)
+         (set-visited-file-modtime))
+      (if (stringp visit)
+         (progn
+           (set-visited-file-modtime)
+           (setq buffer-file-name visit))))
+    (if (or (eq visit t)
+           (eq visit nil)
+           (stringp visit))
+       (message "Wrote %s" buffer-file-name))))
+
+(put 'write-region 'age-file 'age-file-write-region)
+
+;;; Commands
+
+(defun age-file-select-keys ()
+  "Select recipients for encryption."
+  (interactive)
+  (setq-local age-file-encrypt-to
+              (age-select-keys
+               (age-make-context)
+               "Select recipients for encryption.")))
+
+;;;###autoload
+(defun age-file-enable ()
+  (interactive)
+  (if (memq age-file-handler file-name-handler-alist)
+      (message "`age-file' already enabled")
+    (setq file-name-handler-alist
+         (cons age-file-handler file-name-handler-alist))
+    (add-hook 'find-file-hook 'age-file-find-file-hook)
+    (setq auto-mode-alist (cons age-file-auto-mode-alist-entry 
auto-mode-alist))
+    (message "`age-file' enabled")))
+
+;;;###autoload
+(defun age-file-disable ()
+  (interactive)
+  (if (memq age-file-handler file-name-handler-alist)
+      (progn
+       (setq file-name-handler-alist
+             (delq age-file-handler file-name-handler-alist))
+       (remove-hook 'find-file-hook 'age-file-find-file-hook)
+       (setq auto-mode-alist (delq age-file-auto-mode-alist-entry
+                                   auto-mode-alist))
+       (message "`age-file' disabled"))
+    (message "`age-file' already disabled")))
+
+(provide 'age)
+
+;;; age.el ends here


Reply via email to