diff options
| -rw-r--r-- | kasi-totp.el | 64 |
1 files changed, 64 insertions, 0 deletions
diff --git a/kasi-totp.el b/kasi-totp.el new file mode 100644 index 0000000..3d197d6 --- /dev/null +++ b/kasi-totp.el | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | ;; TOTP code taken from Mastering Emacs which took from Jürgen Hötzel's `totp.el'. | ||
| 2 | ;; Base32 code taken from same Mastering Emacs article. | ||
| 3 | ;; https://www.masteringemacs.org/article/securely-generating-totp-tokens-emacs | ||
| 4 | ;; https://github.com/juergenhoetzel/emacs-totp | ||
| 5 | (require 'bindat) | ||
| 6 | (require 'gnutls) | ||
| 7 | (require 'hexl) | ||
| 8 | (require 'auth-source) | ||
| 9 | |||
| 10 | (defconst base32-alphabet | ||
| 11 | (let ((tbl (make-char-table nil))) | ||
| 12 | (dolist (mapping '(("A" . 0) ("B" . 1) ("C" . 2) ("D" . 3) | ||
| 13 | ("E" . 4) ("F" . 5) ("G" . 6) | ||
| 14 | ("H" . 7) ("I" . 8) ("J" . 9) ("K" . 10) | ||
| 15 | ("L" . 11) ("M" . 12) ("N" . 13) | ||
| 16 | ("O" . 14) ("P" . 15) ("Q" . 16) ("R" . 17) | ||
| 17 | ("S" . 18) ("T" . 19) ("U" . 20) | ||
| 18 | ("V" . 21) ("W" . 22) ("X" . 23) ("Y" . 24) | ||
| 19 | ("Z" . 25) ("2" . 26) ("3" . 27) | ||
| 20 | ("4" . 28) ("5" . 29) ("6" . 30) ("7" . 31))) | ||
| 21 | (aset tbl (string-to-char (car mapping)) (cdr mapping))) | ||
| 22 | tbl) | ||
| 23 | "Base-32 mapping table, as defined in RFC 4648.") | ||
| 24 | |||
| 25 | (defun base32-hex-decode (string) | ||
| 26 | "The cheats' version of base-32 decode. | ||
| 27 | |||
| 28 | This is not a 100% faithful implementation of RFC 4648. The | ||
| 29 | concept of encoding partial quanta is not implemented fully. | ||
| 30 | |||
| 31 | No attempt is made to pad the output either as that is not | ||
| 32 | required for HMAC-TOTP." | ||
| 33 | (unless (mod (length string) 8) | ||
| 34 | (error "Padding is incorrect")) | ||
| 35 | (setq string (upcase string)) | ||
| 36 | (let ((trimmed-array (append (string-trim-right string "=+") nil))) | ||
| 37 | (format "%X" (seq-reduce | ||
| 38 | (lambda (acc char) (+ (ash acc 5) (aref base32-alphabet char))) | ||
| 39 | trimmed-array 0)))) | ||
| 40 | |||
| 41 | (defun totp--hex-decode-string (string) | ||
| 42 | "Hex-decode STRING and return the result as a unibyte string." | ||
| 43 | (apply #'unibyte-string | ||
| 44 | (seq-map (lambda (s) (hexl-htoi (aref s 0) (aref s 1))) | ||
| 45 | (seq-partition string 2)))) | ||
| 46 | |||
| 47 | (defun totp (string &optional time digits) | ||
| 48 | "Return a TOTP token using the secret hex STRING and current time. | ||
| 49 | TIME is used as counter value instead of current time, if non-nil. | ||
| 50 | DIGITS is the number of pin digits and defaults to 6." | ||
| 51 | (let* ((key-bytes (totp--hex-decode-string (upcase string))) | ||
| 52 | (counter (truncate (/ (or time (time-to-seconds)) 30))) | ||
| 53 | (digits (or digits 6)) | ||
| 54 | (format-string (format "%%0%dd" digits)) | ||
| 55 | ;; we have to manually split the 64 bit number (u64 not supported in Emacs 27.2) | ||
| 56 | (counter-bytes (bindat-pack '((:high u32) (:low u32)) | ||
| 57 | `((:high . ,(ash counter -32)) (:low . ,(logand counter #xffffffff))))) | ||
| 58 | (mac (gnutls-hash-mac 'SHA1 key-bytes counter-bytes)) | ||
| 59 | (offset (logand (bindat-get-field (bindat-unpack '((:offset u8)) mac 19) :offset) #xf))) | ||
| 60 | (format format-string | ||
| 61 | (mod | ||
| 62 | (logand (bindat-get-field (bindat-unpack '((:totp-pin u32)) mac offset) :totp-pin) | ||
| 63 | #x7fffffff) | ||
| 64 | (expt 10 digits))))) | ||
