kasi.el

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

kasi.el (5236B)


      1 ;; kasi.el --- emacs secrets manager  -*- lexical-binding:t -*-
      2 
      3 ;; Copyright (C) 2026 Vineet K
      4 
      5 ;; Author: Vineet K <git@vineetk.net>
      6 ;; Version: 1.0
      7 ;; Keywords: secret, password
      8 ;; Package-Requires: ((emacs "28.1"))
      9 
     10 ;; This program is free software; you can redistribute it and/or modify
     11 ;; it under the terms of the GNU General Public License as published by
     12 ;; the Free Software Foundation, either version 3 of the License, or
     13 ;; (at your option) any later version.
     14 
     15 ;; This program is distributed in the hope that it will be useful,
     16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18 ;; GNU General Public License for more details.
     19 
     20 ;; You should have received a copy of the GNU General Public License
     21 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     22 
     23 ;;; Commentary:
     24 
     25 ;; This package provides an easy way to manage secrets via M-x kasi.
     26 
     27 (defgroup kasi nil
     28   "An easy way to manage secrets."
     29   :group 'convenience)
     30 
     31 (require 'kasi-totp)
     32 
     33 ;;;###autoload
     34 (defcustom kasi-dir "~/.kasi/"
     35   "Default base directory for kasi-related functions."
     36   :type '(string)
     37   :group 'kasi)
     38 
     39 ;;;###autoload
     40 (defcustom kasi-extension ".age"
     41   "Default file extension for creating kasi secrets."
     42   :type '(string)
     43   :group 'kasi)
     44 
     45 ;;;###autoload
     46 (defcustom kasi-template "\
     47 ; -*- mode: lisp -*-
     48 ((password \"%s\")
     49  (username \"%s\")
     50  (email \"\")
     51  (totp \"\")
     52  (extra-notes \"\"))\n"
     53   "Template that gets filled in to newly created secret files.
     54 Two %s are needed for password and username for now."
     55   :type 'string
     56   :group 'kasi)
     57 
     58 (defvar kasi-version "1.0")
     59 
     60 (defun kasi--filename-short (file)
     61   "Returns shortened name of filename."
     62   (file-name-sans-extension
     63    (file-relative-name file kasi-dir)))
     64 
     65 (defun kasi--filename ()
     66   "Returns full filename of the secret file."
     67   (let* ((filenames (file-expand-wildcards (concat kasi-dir "/*/*")))
     68 	 (options (mapcar (lambda (f)
     69 			    (kasi--filename-short f))
     70 			  filenames))
     71 	 (selected (completing-read "File: " options))
     72 	 (file (elt filenames (seq-position options selected))))
     73     file))
     74 
     75 ;; TODO implement some kind of validation of the sexp when saving
     76 (defun kasi--get (file elem)
     77   "Returns element from secret file."
     78   (car (with-temp-buffer
     79 	 (insert-file-contents file)
     80 	 (alist-get elem (read (current-buffer))))))
     81 
     82 (defun kasi-get (file elem)
     83   "Prints decrypted element. Wrapper around kasi--get for non-Emacs use."
     84   (kasi--get (concat kasi-dir "/" file kasi-extension) elem))
     85 
     86 (defun kasi--totp (file)
     87   "Returns current TOTP code from secret file."
     88   (let* ((totp-secret (kasi--get file 'totp))
     89 	 (totp-code (totp totp-secret)))
     90     (kill-new totp-code)
     91     (message (format "TOTP %s for %s copied to kill ring"
     92 		     totp-code
     93 		     (kasi--filename-short file)))))
     94 
     95 (defun kasi--random-string (length charset)
     96   "Generate a random string of LENGTH using characters from CHARSET.
     97 Uses /dev/urandom for cryptographic security."
     98   ;; I have not seen any other built-in way to get cryptographically
     99   ;; random outputs in Emacs.
    100   (let* ((bytes (with-temp-buffer
    101                   (call-process "dd" nil (current-buffer) nil
    102                                 "if=/dev/urandom" "bs=1" (format "count=%d" (* length 4)))
    103                   (buffer-string)))
    104          (charset-len (length charset))
    105          (result ""))
    106     (dotimes (i length)
    107       (setq result (concat result
    108 			   (char-to-string
    109 			    (aref charset
    110 				  (% (aref bytes i)
    111 				     charset-len))))))
    112     result))
    113 
    114 ;; TODO implement some kind of validation of the sexp when saving
    115 (defun kasi--edit (file)
    116   "Edits secret file."
    117   (find-file-other-window file))
    118 
    119 (defun kasi--create (file)
    120   "Creates secret file."
    121   (let* ((filename (concat kasi-dir "/"
    122 			   file kasi-extension)))
    123     (mkdir (format kasi-dir "/"
    124 		   (car (split-string file "/")))
    125 	   t)
    126     (with-temp-file filename
    127       (insert (format kasi-template
    128 		      (kasi-generate-password)
    129 		      (kasi-generate-username))))
    130     (kasi--edit filename)))
    131 
    132 ;;;###autoload
    133 (defun kasi-generate-password (&optional length)
    134   "Generate a random password of LENGTH (default 32)."
    135   (interactive "P")
    136   (let ((charset "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"!#$%&'()*+,-./:;<=>?@[]^_`{|}~"))
    137     (message "%s" (kasi--random-string (or length 32) charset))))
    138 
    139 ;;;###autoload
    140 (defun kasi-generate-username (&optional length)
    141   "Generate a random username of LENGTH (default 12)."
    142   (interactive "P")
    143   (let ((charset "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"))
    144     (message "%s" (kasi--random-string (or length 12) charset))))
    145 
    146 ;;;###autoload
    147 (defun kasi (command file)
    148   "Invoke the kasi secret manager."
    149   (interactive (list (intern (completing-read "Command: "
    150 					      '(edit
    151 						create
    152 						password
    153 						username
    154 						email
    155 						totp)))
    156 		     (kasi--filename)))
    157   (pcase command
    158     ('edit (kasi--edit file))
    159     ('create (kasi--create file))
    160     ('totp (kasi--totp file))
    161     ((or 'password 'username 'email)
    162      (progn
    163        (kill-new (kasi--get file command))
    164        (message (format "Copied %s for %s to kill ring."
    165 			command
    166 			(kasi--filename-short file)))))))
    167 
    168 (provide 'kasi)