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)