├── README.md └── password-mode.el /README.md: -------------------------------------------------------------------------------- 1 | password-mode 2 | ============= 3 | 4 | An Emacs minor mode to hide sensitive information in buffers (passwords) 5 | using overlays. 6 | 7 | Warning 8 | ------- 9 | 10 | This is not a replacement for a real password manager. Emacs already provides 11 | an implementation of the Secret Service API (Gnome Keyring client): secrets.el 12 | 13 | But there may be situation when you have to deal with passwords in plain text files. 14 | You can further increase security by using GPG: http://blog.bogosity.se/2011/01/12/managing-passwords-using-gnupg-git-and-emacs/ 15 | 16 | 17 | 18 | Example Usage 19 | ------------- 20 | 21 | ```lisp 22 | (require 'password-mode) 23 | (add-hook 'text-mode-hook 'password-mode) 24 | ``` 25 | 26 | How it works 27 | ----------------------------- 28 | 29 | There is a prefix regexp used to find the text before the password: 30 | 31 | ```lisp 32 | (defcustom password-mode-password-prefix-regexs 33 | '("Password:\s+" "Passwort:\s+")) 34 | ``` 35 | 36 | There is also a regexp used to find the actual password: 37 | 38 | ```lisp 39 | (defcustom password-mode-password-regex 40 | "\\([[:graph:]]*\\)" 41 | ``` 42 | 43 | Change passwords 44 | ---------------- 45 | 46 | When you try to changes a password (hidden by the overlay) an Emacs 47 | password prompt is invoked to read the actual password. 48 | 49 | Copy to `kill-ring` 50 | ------------------- 51 | 52 | Use `(password-mode-copy-as-kill)` to copy the next password to the `kill-ring`. 53 | 54 | -------------------------------------------------------------------------------- /password-mode.el: -------------------------------------------------------------------------------- 1 | ;;; password-mode.el --- Hide password text using overlays 2 | 3 | ;; Copyright (C) 2012-2021 Jürgen Hötzel 4 | 5 | ;; Author: Jürgen Hötzel 6 | ;; URL: https://github.com/juergenhoetzel/password-mode 7 | ;; Version: 1.0-snapshot 8 | ;; Package-Requires: ((emacs "25.1")) 9 | ;; Keywords: docs password passphrase 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; example usage: (add-hook 'text-mode-hook 'password-mode) 27 | ;; Using with GPG: 28 | ;; http://blog.bogosity.se/2011/01/12/managing-passwords-using-gnupg-git-and-emacs/ 29 | 30 | ;;; Code: 31 | 32 | (require 'seq) 33 | (require 'cl-lib) 34 | 35 | ;;--------------------------------------------------------------------------- 36 | ;; user-configurable variables 37 | 38 | (defgroup password-mode nil 39 | "Minor mode for hiding passwords.") 40 | 41 | (defcustom password-mode-hook nil 42 | "*Hook called when password minor mode is activated or deactivated." 43 | :type 'hook 44 | :group 'password-mode) 45 | 46 | (defcustom password-mode-password-prefix-regexs 47 | '("[Pp]assword:?[[:space:]]+" "[Pp]asswort:?[[:space:]]+") 48 | "Regexps recognized as password prefix. 49 | 50 | Regexps must not contain parentheses for grouping, otherwise your 51 | match wouldn't work. Shy groups are OK." 52 | :type '(repeat (regexp :tag "Password Regex")) 53 | :group 'password-mode) 54 | 55 | (defconst password-mode-shown-text 56 | (propertize 57 | (apply 'concat (make-list 5 "*")) 58 | 'face 'font-lock-debug-face) 59 | "Always show the same text for passwords, so the length is not known.") 60 | 61 | (defcustom password-mode-password-regex 62 | "\\([[:graph:]]*\\)" 63 | "Regex to match Passwords." 64 | :type 'regexp 65 | :group 'password-mode) 66 | 67 | (defun password-mode-make-overlay (b e) 68 | "Return a new overlay in region defined by B and E." 69 | (let ((ov (make-overlay b e))) 70 | (overlay-put ov 'display 71 | password-mode-shown-text) 72 | (overlay-put ov 'password-mode-length (- e b)) 73 | ov)) 74 | 75 | (defun password-mode-prompt-password (ov after start end &optional len) 76 | "Prompt for new password." 77 | (when after 78 | (cl-assert (zerop len)) ;when doing insertion, len is always 0 79 | (let* ((inhibit-modification-hooks t) 80 | (insert-length (- (overlay-end ov) (overlay-start ov) (overlay-get ov 'password-mode-length))) 81 | (istr (buffer-substring (overlay-start ov) (+ (overlay-start ov) insert-length))) 82 | (new-password (password-mode-read-passwd "Password: " t istr))) 83 | (delete-region (overlay-start ov) (overlay-end ov)) 84 | (delete-overlay ov) 85 | (password-mode-insert-password new-password) 86 | (clear-string new-password)))) 87 | 88 | (defun password-mode-insert-password (new-password) 89 | "Insert NEW-PASSWORD with hidden password overlay." 90 | ;; timing issue, first insert a dummy string, so the password is never visible 91 | (insert (apply 'concat (make-list (length new-password) "*"))) 92 | (let ((ov (password-mode-make-overlay (- (point) (length new-password)) (point)))) 93 | (goto-char (- (point) (length new-password))) 94 | (insert new-password) 95 | (delete-region (point) (+ (point) (length new-password))) 96 | (overlay-put ov 'insert-in-front-hooks '(password-mode-prompt-password)))) 97 | 98 | ;;; reimplementation, of read-passwd (which das not support initial value) 99 | (defun password-mode-read-passwd (prompt &optional confirm initial) 100 | "Read a password, prompting with PROMPT. 101 | If optional CONFIRM is non-nil, read the password twice to make sure. 102 | Optional INITIAL is a default password to use instead of empty input. 103 | 104 | This function echoes `*' for each character that the user types. 105 | 106 | The user ends with RET, LFD, or ESC. DEL or C-h rubs out. 107 | C-y yanks the current kill. C-u kills line. 108 | C-g quits; if `inhibit-quit' was non-nil around this function, 109 | then it returns nil if the user types C-g, but `quit-flag' remains set. 110 | 111 | Once the caller uses the password, it can erase the password 112 | by doing (clear-string STRING)." 113 | (with-local-quit 114 | (when confirm 115 | (let (success) 116 | (while (not success) 117 | (let ((first (password-mode-read-passwd--internal prompt initial)) 118 | (second (password-mode-read-passwd--internal "Confirm password: "))) 119 | (if (equal first second) 120 | (progn 121 | (and (arrayp second) (clear-string second)) 122 | (setq success first)) 123 | (and (arrayp first) (clear-string first)) 124 | (and (arrayp second) (clear-string second)) 125 | (message "Password not repeated accurately; please start over") 126 | (setq initial "") 127 | (sit-for 1)))) 128 | success)))) 129 | 130 | (defun password-mode-read-passwd--internal (prompt &optional initial) 131 | "Internal helper for reading password." 132 | (let ((pass initial) 133 | ;; Copy it so that add-text-properties won't modify 134 | ;; the object that was passed in by the caller. 135 | (prompt (copy-sequence prompt)) 136 | (c 0) 137 | (echo-keystrokes 0) 138 | (cursor-in-echo-area t) 139 | (message-log-max nil) 140 | (stop-keys (list 'return ?\r ?\n ?\e)) 141 | (rubout-keys (list 'backspace ?\b ?\177))) 142 | (add-text-properties 0 (length prompt) 143 | minibuffer-prompt-properties prompt) 144 | (while (progn (message "%s%s" 145 | prompt 146 | (make-string (length pass) ?.)) 147 | (setq c (read-key)) 148 | (not (memq c stop-keys))) 149 | (clear-this-command-keys) 150 | (cond ((memq c rubout-keys) ; rubout 151 | (when (> (length pass) 0) 152 | (let ((new-pass (substring pass 0 -1))) 153 | (and (arrayp pass) (clear-string pass)) 154 | (setq pass new-pass)))) 155 | ((eq c ?\C-g) (keyboard-quit)) 156 | ((not (numberp c))) 157 | ((= c ?\C-u) ; kill line 158 | (and (arrayp pass) (clear-string pass)) 159 | (setq pass "")) 160 | ((= c ?\C-y) ; yank 161 | (let* ((str (condition-case nil 162 | (current-kill 0) 163 | (error nil))) 164 | new-pass) 165 | (when str 166 | (setq new-pass 167 | (concat pass 168 | (substring-no-properties str))) 169 | (and (arrayp pass) (clear-string pass)) 170 | (setq c ?\0) 171 | (setq pass new-pass)))) 172 | ((characterp c) ; insert char 173 | (let* ((new-char (char-to-string c)) 174 | (new-pass (concat pass new-char))) 175 | (and (arrayp pass) (clear-string pass)) 176 | (clear-string new-char) 177 | (setq c ?\0) 178 | (setq pass new-pass))))) 179 | (message nil) 180 | pass)) 181 | 182 | (defun password-mode-hide (b e) 183 | "Hide password." 184 | (overlay-put (password-mode-make-overlay b e) 'insert-in-front-hooks '(password-mode-prompt-password))) 185 | 186 | ;;;###autoload 187 | (define-minor-mode password-mode 188 | "Minor mode to hide passwords 189 | With a prefix argument ARG, enable the mode if ARG is positive, 190 | and disable it otherwise. If called from Lisp, enable the mode 191 | if ARG is omitted or nil. 192 | 193 | Passwords are recognized, when the previous word is part of 194 | `password-mode-words' followed by a colon and whitespace 195 | 196 | Lastly, the normal hook `password-mode-hook' is run using `run-hooks'. 197 | " 198 | :group 'password-mode 199 | :lighter " pw" 200 | (if password-mode 201 | (password-mode-hide-all) 202 | ;; hs-show-all does nothing unless h-m-m is non-nil. 203 | (add-hook 'post-self-insert-hook 'password-mode-insert-hook-function) 204 | (let ((passord-mode t)) 205 | (password-mode-discard-overlays (point-min) (point-max))))) 206 | 207 | (defun password-mode-insert-hook-function () 208 | "Function for editing hidden passwords." 209 | (when (save-match-data 210 | (and password-mode 211 | (looking-back (password-mode-regexp) nil) 212 | ;; prevent reinvoking 213 | (= (match-beginning 2) (point)))) 214 | (password-mode-insert-password (password-mode-read-passwd "Password: " t "")))) 215 | 216 | (defun password-mode-regexp () 217 | "Regexp from custom variables `password-mode-password-prefix-regexs' and `password-mode-password-regex'." 218 | (concat "\\(" (mapconcat 'identity password-mode-password-prefix-regexs "\\|") "\\)" 219 | password-mode-password-regex)) 220 | 221 | (defun password-mode-hide-all () 222 | "Hide all passwords using overlays." 223 | (interactive) 224 | (password-mode-discard-overlays (point-min) (point-max)) 225 | (save-excursion 226 | (goto-char (point-min)) 227 | (while 228 | (re-search-forward (password-mode-regexp) (point-max) t) 229 | (password-mode-hide (match-beginning 2) (match-end 2))))) 230 | 231 | (defun password-mode-discard-overlays (from to) 232 | "Delete password overlays in region defined by FROM and TO." 233 | (dolist (ov (overlays-in from to)) 234 | (when (overlay-get ov 'password-mode-length) 235 | (delete-overlay ov)))) 236 | 237 | (defun password-mode-copy-as-kill () 238 | "Copy the next password at point to the `kill-ring'." 239 | (interactive) 240 | (let* ((overlays (nreverse (overlays-in (point) (point-max)))) 241 | (pov (seq-find (lambda (ov) (overlay-get ov 'password-mode-length)) overlays))) 242 | (when pov 243 | (copy-region-as-kill (overlay-start pov) (+ (overlay-start pov) (overlay-get pov 'password-mode-length))) 244 | (message "Password have been copied to the kill ring.")))) 245 | 246 | (provide 'password-mode) 247 | 248 | ;;; password-mode.el ends here 249 | --------------------------------------------------------------------------------