From 2cdc143ce7c205cd62ce33753cb88fc3e87d5b88 Mon Sep 17 00:00:00 2001 From: rearman Date: Wed, 17 Sep 2025 12:58:07 -0400 Subject: [PATCH] Add in pass-alike password and otp code management Uses the same directory, format, and files as default pass(1), and uses system gpg setup via built-in easypg. Defuns to get passwords, generate otp codes, and to copy them to clipboard. Entry is as simple as editing the file, with easypg taking care of the rest. --- init.el | 3 +- re/re-editing-defuns.el | 14 ++++++ re/re-pass-alike.el | 102 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 re/re-pass-alike.el diff --git a/init.el b/init.el index a4e8291..c23520d 100644 --- a/init.el +++ b/init.el @@ -21,7 +21,8 @@ (add-hook 'after-init-hook (lambda () (require 're-repl-defuns) - (require 're-howm-defuns))) + (require 're-howm-defuns) + (require 're-pass-alike))) (add-to-list 'auto-mode-alist '("\\.keymap\\'" . c-mode)) (add-hook 'c-mode-hook (lambda () (require 're-c-style))) diff --git a/re/re-editing-defuns.el b/re/re-editing-defuns.el index 902b148..1aa3557 100644 --- a/re/re-editing-defuns.el +++ b/re/re-editing-defuns.el @@ -171,5 +171,19 @@ Stolen from emacsredux.com." (abort-recursive-edit)) (keyboard-quit))) +(defun re/get-line-from-file (file-path line-number) + "Return the specified LINE-NUMBER from FILE-PATH as a string. +Start from bottom if given a negative line number." + (with-temp-buffer + (insert-file-contents file-path) + (if (< line-number 0) + (progn + (goto-char (point-max)) + (forward-line line-number)) + (progn + (goto-char (point-min)) + (forward-line (1- line-number)))) + (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) + (provide 're-editing-defuns) ;;; re-editing-defuns.el ends here diff --git a/re/re-pass-alike.el b/re/re-pass-alike.el new file mode 100644 index 0000000..b9550fb --- /dev/null +++ b/re/re-pass-alike.el @@ -0,0 +1,102 @@ +;;; re-pass-alike.el -*- lexical-binding: t; -*- +;; This file is not part of GNU Emacs. + +;;; Code: + +(require 'bindat) +(require 'gnutls) +(require 'hexl) +(require 'auth-source) + +(defun totp--hex-decode-string (string) + "Hex-decode STRING and return the result as a unibyte string." + (apply #'unibyte-string + (seq-map (lambda (s) (hexl-htoi (aref s 0) (aref s 1))) + (seq-partition string 2)))) + +(defun totp (string &optional time digits) + "Return a TOTP token using the secret hex STRING and current time. +TIME is used as counter value instead of current time, if non-nil. +DIGITS is the number of pin digits and defaults to 6." + (let* ((key-bytes (totp--hex-decode-string (upcase string))) + (counter (truncate (/ (or time (time-to-seconds)) 30))) + (digits (or digits 6)) + (format-string (format "%%0%dd" digits)) + ;; we have to manually split the 64 bit number (u64 not supported in Emacs 27.2) + (counter-bytes (bindat-pack '((:high u32) (:low u32)) + `((:high . ,(ash counter -32)) (:low . ,(logand counter #xffffffff))))) + (mac (gnutls-hash-mac 'SHA1 key-bytes counter-bytes)) + (offset (logand (bindat-get-field (bindat-unpack '((:offset u8)) mac 19) :offset) #xf))) + (format format-string + (mod + (logand (bindat-get-field (bindat-unpack '((:totp-pin u32)) mac offset) :totp-pin) + #x7fffffff) + (expt 10 digits))))) + +(defconst base32-alphabet + (let ((tbl (make-char-table nil))) + (dolist (mapping '(("A" . 0) ("B" . 1) ("C" . 2) ("D" . 3) + ("E" . 4) ("F" . 5) ("G" . 6) + ("H" . 7) ("I" . 8) ("J" . 9) ("K" . 10) + ("L" . 11) ("M" . 12) ("N" . 13) + ("O" . 14) ("P" . 15) ("Q" . 16) ("R" . 17) + ("S" . 18) ("T" . 19) ("U" . 20) + ("V" . 21) ("W" . 22) ("X" . 23) ("Y" . 24) + ("Z" . 25) ("2" . 26) ("3" . 27) + ("4" . 28) ("5" . 29) ("6" . 30) ("7" . 31))) + (aset tbl (string-to-char (car mapping)) (cdr mapping))) + tbl) + "Base-32 mapping table, as defined in RFC 4648.") + +(defun base32-hex-decode (string) + "The cheats' version of base-32 decode. + +This is not a 100% faithful implementation of RFC 4648. The +concept of encoding partial quanta is not implemented fully. + +No attempt is made to pad the output either as that is not +required for HMAC-TOTP." + (unless (mod (length string) 8) + (error "Padding is incorrect")) + (setq string (upcase string)) + (let ((trimmed-array (append (string-trim-right string "=+") nil))) + (format "%X" (seq-reduce + (lambda (acc char) (+ (ash acc 5) (aref base32-alphabet char))) + trimmed-array 0)))) + +(defun keyfile-string (filename) + "Return a string which is the filepath to a `pass(1)' file." + (let ((directory "~/.password-store/")) + (if (null filename) + (read-file-name "Select a password: " directory nil t nil) + (concat directory + (if (stringp filename) + filename + (symbol-name filename)) + ".gpg")))) + +(defun get-password (&optional filename) + "Return a password given a file where it is stored. +Assumes same structure as `pass(1)'." + (re/get-line-from-file (keyfile-string filename) 1)) + +(defun password-to-clipboard (&optional filename) + "Copy the returned password to the clipboard." + (interactive) + (kill-new (get-password filename))) + +(defun get-totp (&optional filename) + "Generate a totp code given a file with the secret hex string. +If no filename given, prompt for one. +Assumes the file has the string by itself on the last line of the file, +similar to what pass-otp does, but without the full uri. +Also assumes the file is in `~/.password-store' and has the `.gpg' extension." + (totp (base32-hex-decode (re/get-line-from-file (keyfile-string filename) -1)))) + +(defun otp-to-clipboard (&optional filename) + "Copy the returned totp code to the clipboard." + (interactive) + (kill-new (get-totp filename))) + +(provide 're-pass-alike) +;;; re-pass-alike.el ends here -- 2.39.5