kasi-totp.el (5308B)
1 ;; kasi-totp.el --- TOTP support for kasi -*- lexical-binding:t -*- 2 3 ;; Copyright (C) 2026 Vineet K 4 5 ;; Author: Vineet K <git@vineetk.net> 6 ;; Keywords: processes 7 8 ;; This program is free software; you can redistribute it and/or modify 9 ;; it under the terms of the GNU General Public License as published by 10 ;; the Free Software Foundation, either version 3 of the License, or 11 ;; (at your option) any later version. 12 13 ;; This program is distributed in the hope that it will be useful, 14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 ;; GNU General Public License for more details. 17 18 ;; You should have received a copy of the GNU General Public License 19 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 20 21 ;;; Commentary: 22 23 ;; TOTP code taken from Mastering Emacs which took from Jürgen Hötzel's `totp.el'. 24 ;; Base32 code taken from same Mastering Emacs article. 25 ;; HMAC code taken from Sean McAfee. 26 ;; https://www.masteringemacs.org/article/securely-generating-totp-tokens-emacs 27 ;; https://github.com/juergenhoetzel/emacs-totp 28 ;; https://github.com/grimnebulin/emacs-hmac 29 30 ;;; Code: 31 (require 'bindat) 32 (require 'cl-lib) 33 (require 'hexl) 34 (require 'subr-x) 35 36 (defconst hmac-algorithm-blocksizes 37 '((md5 . 64) 38 (sha1 . 64) 39 (sha224 . 64) 40 (sha256 . 64) 41 (sha384 . 128) 42 (sha512 . 128)) 43 "Mapping from HMAC algorithm to the algorithm's blocksize.") 44 45 (defun hmac (algorithm key message &optional binary) 46 "Compute the HMAC for a given message, private key and algorithm. 47 48 ALGORITHM is a symbol naming one of the algorithms recognized by 49 `secure-hash'. KEY is a the private key to use. MESSAGE, a 50 string, is the message to hash. If BINARY is non-nil, the hmac 51 will be returned as a binary string, otherwise as a hexadecimal 52 string." 53 (if-let (blocksize (alist-get algorithm hmac-algorithm-blocksizes)) 54 (progn 55 (when (> (length key) blocksize) 56 (setq key (secure-hash algorithm key))) 57 (when (< (length key) blocksize) 58 (setq key (string-as-unibyte (concat key (make-string (- blocksize (length key)) 0))))) 59 (let* ((key-list (string-to-list key)) 60 (o-key-pad (apply #'unibyte-string 61 (cl-map 'list #'logxor 62 key-list 63 (string-to-list (make-string blocksize #x5c))))) 64 (i-key-pad (apply #'unibyte-string 65 (cl-map 'list #'logxor 66 key-list 67 (string-to-list (make-string blocksize #x36))))) 68 (inner (secure-hash algorithm (concat i-key-pad message) nil nil t))) 69 (secure-hash algorithm (concat o-key-pad inner) nil nil binary))) 70 (error "Unsupported hash algorithm %s" algorithm))) 71 72 (defconst base32-alphabet 73 (let ((tbl (make-char-table nil))) 74 (dolist (mapping '(("A" . 0) ("B" . 1) ("C" . 2) ("D" . 3) 75 ("E" . 4) ("F" . 5) ("G" . 6) 76 ("H" . 7) ("I" . 8) ("J" . 9) ("K" . 10) 77 ("L" . 11) ("M" . 12) ("N" . 13) 78 ("O" . 14) ("P" . 15) ("Q" . 16) ("R" . 17) 79 ("S" . 18) ("T" . 19) ("U" . 20) 80 ("V" . 21) ("W" . 22) ("X" . 23) ("Y" . 24) 81 ("Z" . 25) ("2" . 26) ("3" . 27) 82 ("4" . 28) ("5" . 29) ("6" . 30) ("7" . 31))) 83 (aset tbl (string-to-char (car mapping)) (cdr mapping))) 84 tbl) 85 "Base-32 mapping table, as defined in RFC 4648.") 86 87 (defun base32-hex-decode (string) 88 "The cheats' version of base-32 decode. 89 90 This is not a 100% faithful implementation of RFC 4648. The 91 concept of encoding partial quanta is not implemented fully." 92 (setq string (upcase string)) 93 (let* ((trimmed-array (string-trim-right string "=+")) 94 (hex (format "%X" (seq-reduce 95 (lambda (acc char) (+ (ash acc 5) (aref base32-alphabet char))) 96 trimmed-array 0)))) 97 (if (cl-oddp (length hex)) 98 (concat "0" hex) 99 hex))) 100 101 (defun totp--hex-decode-string (string) 102 "Hex-decode STRING and return the result as a unibyte string." 103 (apply #'unibyte-string 104 (seq-map (lambda (s) (hexl-htoi (aref s 0) (aref s 1))) 105 (seq-partition string 2)))) 106 107 (defun totp (string) 108 "Return a TOTP token using the secret hex STRING and current time." 109 (let* ((key-bytes (totp--hex-decode-string (upcase (base32-hex-decode string)))) 110 (counter (truncate (/ (time-to-seconds) 30))) 111 ;; we have to manually split the 64 bit number (u64 not supported in Emacs 27.2) 112 (counter-bytes (bindat-pack '((:high u32) (:low u32)) 113 `((:high . ,(ash counter -32)) (:low . ,(logand counter #xffffffff))))) 114 (mac (hmac 'sha1 key-bytes counter-bytes t)) 115 (offset (logand (bindat-get-field (bindat-unpack '((:offset u8)) mac 19) :offset) #xf))) 116 (format "%06d" 117 (mod 118 (logand (bindat-get-field (bindat-unpack '((:totp-pin u32)) mac offset) :totp-pin) 119 #x7fffffff) 120 (expt 10 6))))) 121 122 (provide 'kasi-totp)