summaryrefslogtreecommitdiff
path: root/kasi-totp.el
blob: 5a54397ad6868a8bcd939c1a101f0080592a7643 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
;; kasi-totp.el --- TOTP support for kasi  -*- lexical-binding:t -*-

;; Copyright (C) 2026 Vineet K

;; Author: Vineet K <git@vineetk.net>
;; Keywords: processes

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; TOTP code taken from Mastering Emacs which took from Jürgen Hötzel's `totp.el'.
;; Base32 code taken from same Mastering Emacs article.
;; HMAC code taken from Sean McAfee.
;; https://www.masteringemacs.org/article/securely-generating-totp-tokens-emacs
;; https://github.com/juergenhoetzel/emacs-totp
;; https://github.com/grimnebulin/emacs-hmac

;;; Code:
(require 'bindat)
(require 'cl-lib)
(require 'hexl)
(require 'subr-x)

(defconst hmac-algorithm-blocksizes
  '((md5 . 64)
    (sha1 . 64)
    (sha224 . 64)
    (sha256 . 64)
    (sha384 . 128)
    (sha512 . 128))
  "Mapping from HMAC algorithm to the algorithm's blocksize.")

(defun hmac (algorithm key message &optional binary)
  "Compute the HMAC for a given message, private key and algorithm.

ALGORITHM is a symbol naming one of the algorithms recognized by
`secure-hash'.  KEY is a the private key to use.  MESSAGE, a
string, is the message to hash.  If BINARY is non-nil, the hmac
will be returned as a binary string, otherwise as a hexadecimal
string."
  (if-let (blocksize (alist-get algorithm hmac-algorithm-blocksizes))
      (progn
        (when (> (length key) blocksize)
          (setq key (secure-hash algorithm key)))
        (when (< (length key) blocksize)
          (setq key (concat key (make-string (- blocksize (length key)) 0))))
        (let ((o-key-pad (cl-map 'string #'logxor key (make-string blocksize #x5c)))
              (i-key-pad (cl-map 'string #'logxor key (make-string blocksize #x36))))
          (secure-hash algorithm (concat o-key-pad (secure-hash algorithm (concat i-key-pad message) nil nil t)) nil nil binary)))
    (error "Unsupported hash algorithm %s" algorithm)))

(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."
  (unless (zerop (mod (length string) 8))
    (error "Padding is incorrect"))
  (setq string (upcase string))
  (let* ((trimmed-array (append (string-trim-right string "=+") nil))
         (hex (format "%X" (seq-reduce
                            (lambda (acc char) (+ (ash acc 5) (aref base32-alphabet char)))
                            trimmed-array 0))))
    (if (cl-oddp (length hex))
        (concat "0" hex)
      hex)))

(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)
  "Return a TOTP token using the secret hex STRING and current time."
  (let* ((key-bytes (totp--hex-decode-string (upcase string)))
         (counter (truncate (/ (time-to-seconds) 30)))
         ;; 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 (hmac 'sha1 key-bytes counter-bytes))
         (offset (logand (bindat-get-field (bindat-unpack '((:offset u8)) mac 19) :offset) #xf)))
    (format "%06d"
            (mod
             (logand (bindat-get-field (bindat-unpack '((:totp-pin u32)) mac  offset) :totp-pin)
                     #x7fffffff)
             (expt 10 6)))))

(provide 'kasi-totp)