kasi.el

This package provides an easy way to manage secrets via M-x kasi.
Log | Files | Refs | LICENSE

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)