summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVineet Kumar <git@vineetk.net>2026-05-22 14:45:57 -0400
committerVineet Kumar <git@vineetk.net>2026-05-22 14:45:57 -0400
commitfc307ad821becea95353db58a7133bd5317856c1 (patch)
tree9b34fa86baa12761285de0a2f20091c3b57ec73e
parent1d1de0dd6ab81c17667b43c4a7900efa3c262c0f (diff)
import totp and base32 code from mastering emacs
both from: https://www.masteringemacs.org/article/securely-generating-totp-tokens-emacs https://github.com/juergenhoetzel/emacs-totp
-rw-r--r--kasi-totp.el64
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
28This is not a 100% faithful implementation of RFC 4648. The
29concept of encoding partial quanta is not implemented fully.
30
31No attempt is made to pad the output either as that is not
32required 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.
49TIME is used as counter value instead of current time, if non-nil.
50DIGITS 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)))))