Compare commits

..

2 Commits

Author SHA1 Message Date
75075b81fb Show the correct name 2025-09-02 00:34:57 +02:00
0e88cddc24 Works with a config file 2025-09-02 00:18:34 +02:00
4 changed files with 162 additions and 76 deletions

View File

@@ -3,18 +3,19 @@
[totp.data :refer :all]
[cli-matic.core :refer [run-cmd]]
[cli-matic.utils :as U]
[clojure.pprint :as pp])
[clojure.pprint :as pp]
[clojure.pprint :as pprint])
(:import [java.util TimerTask Timer])
(:gen-class))
(defn- print-confinuous
([secret] (print-confinuous secret 30))
([secret step]
(let [step-millis (* 1000 step)
([secret] (print-confinuous secret "sha1" 6 30))
([secret algorithm digits period]
(let [step-millis (* 1000 period)
now (System/currentTimeMillis)
delay (int (- step-millis (rem now step-millis)))
fn-show (fn [s] (println (System/currentTimeMillis) "-> "(get-otp s)))
fn-show (fn [s] (println (System/currentTimeMillis) "-> "(get-otp s algorithm digits period)))
task (proxy [TimerTask] []
(run [] (fn-show secret)))]
(println "\n <Generating continuosly, press enter to stop>\n")
@@ -29,23 +30,40 @@
)
(defn cmd-generate
[& {:keys [secret continuous] :as opts}]
;(pp/pprint opts)
[& {:keys [secret continuous algorithm digits period] :as opts}]
;;(pp/pprint opts)
(if continuous
(print-confinuous secret)
(println (get-otp secret))
))
(print-confinuous secret algorithm digits period)
(println (get-otp secret algorithm digits period))))
(defn cmd-get
[& {:keys [name continuous] :as opts}]
;;(pp/pprint opts)
(if (exists-config)
(let [cfg (load-config)
app (get-app cfg name)
{:keys [secret algorithm digits period]
:or {algorithm "sha1"
digits 6
period 30}} app]
(if (some? secret)
(if continuous
(print-confinuous secret algorithm digits period)
(println (get-otp secret algorithm digits period)))
(println "The app" name "is not configured")))
(println "Config file not found.")))
(defn cmd-config
[& {:keys [command] :as opts}]
(pp/pprint opts)
;;(pp/pprint opts)
(case command
"info" (println "Configuration file: "
(if exists-config
(if (exists-config)
cfg-file
(str "<not found. default:" cfg-file ">")))
"init" (if exists-config
"init" (if (exists-config)
(do
(println "Configuration already exists, this will delete it. Are you sure? [N/y]")
(case (read-line)
@@ -55,21 +73,39 @@
(create-cfg-file))
"show" (do
(println "Config file:\n")
(println (slurp cfg-file)))))
;(println (slurp cfg-file))
(pp/print-table (load-config))
)))
(defn cmd-list
[])
[& {:keys [sorted] :as opts}]
;;(pp/pprint opts)
(if (exists-config)
(let [apps-list (list-apps (load-config))
s-list (if sorted (sort apps-list) apps-list)]
(dorun (map #(println %) s-list)))
(println "Config file not found.")))
(defn cmd-add
[& {:keys [name secret user issuer algorithm digits period update] :as opts}]
(pp/pprint opts))
;;(pp/pprint opts)
(if (exists-config)
(let [cfg (load-config)]
(when (or update (nil? (get-app cfg name))) ;; get-app returns nil if app don't exists
(store-config (add-app cfg name secret user issuer algorithm digits period))))
(println "Config file not found.")))
(defn cmd-delete
[& {:keys [name force] :as opts}]
(pp/pprint opts))
;;(pp/pprint opts)
(if (exists-config)
(let [cfg (load-config)]
(when (or force (some? (get-app cfg name)))
(store-config (delete-app cfg name))))
(println "Config file not found.")))
(def cli-options
@@ -77,12 +113,12 @@
:version "1.1"
:description ["Generate a TOTP"]}
:commands [{:command "generate" :short "g"
:description "Generate one TOTP with a given secret in BASE32"
:examples ["Generate one TOTP and exits:"
" totp generate \"MJXW42LBORXQ====\""
:commands [{:command "generate"
:description "Generate one TOTP for a BASE32 secret, ignoring configured apps"
:examples ["Generate one TOTP for a provided BASE32 secret:"
" totp generate ABCD1234"
"Generate one TOTP and refresh it continuosly:"
" totp g \"MJXW42LBORXQ====\""]
" totp generate -c ABCD1234"]
:opts [{:option "secret" :short 0
:as "Secret encoded in BASE32"
:type :string
@@ -90,9 +126,37 @@
{:option "continuous" :short "c"
:as "Contiuous mode"
:type :with-flag
:default false}]
:default false}
{:option "algorithm" :short "a"
:as "Algorithm used for the key generation"
:type #{"sha1" "sha256" "sha512"}
:default "sha1"}
{:option "digits" :short "d"
:as "Number of digits for OTP. Usually 6 or 8"
:type :int
:default 6}
{:option "period" :short "p"
:as "Validity time in seconds"
:type :int
:default 30}]
:runs cmd-generate}
{:command "get" :short "g"
:description "Generate one TOTP for a configured app"
:examples ["Generate one TOTP for a provided app:"
" totp get app1"
"Generate one TOTP and refresh it continuosly:"
" totp get -c app1"]
:opts [{:option "name" :short 0
:as "Name of the previous configured app"
:type :string
:default :present}
{:option "continuous" :short "c"
:as "Contiuous mode"
:type :with-flag
:default false}]
:runs cmd-get}
{:command "config" :short "c"
:description "Manage configuration"
:examples ["Show location for the configuration file:"
@@ -110,7 +174,13 @@
{:command "list" :short "l"
:description "List existing apps"
:examples ["List apps:"
" totp list"]
" totp list"
"List apps sorted by name:"
" totp list --sorted"]
:opts [{:option "sorted" :short "s"
:as "If provided, the list willl be sorted by name"
:type :with-flag
:default false}]
:runs cmd-list}
{:command "add" :short "a"
@@ -147,7 +217,7 @@
:default 30}
{:option "update"
:as "Update an app with the same name if exists"
:type :flag
:type :with-flag
:default false}]
:runs cmd-add}
@@ -161,7 +231,7 @@
:default :present}
{:option "force" :short "f"
:as "Don't ask, just remove"
:type :flag
:type :with-flag
:default false}]
:runs cmd-delete}]})

View File

@@ -1,5 +1,6 @@
(ns totp.core
(:require [alphabase.base32 :as b32])
(:require [alphabase.base32 :as b32]
[clojure.math :as m])
(:import (javax.crypto Mac)
(javax.crypto.spec SecretKeySpec)
(java.util Base64 Arrays)
@@ -21,32 +22,42 @@
(= byte-array-type (type x)))
(defmulti hmac-sha1
"Generates an HMAC-SHA1. The key and the message can be (both) string or array of bytes, nil otherwise"
(fn [key message]
(defn get-alg
[alg]
(case alg
"sha1" "HmacSHA1"
"sha256" "HmacSHA256"
"sha512" "HmacSHA512"
""))
(defmulti hmac
"Generates an HMAC. Algorithms supported: sha1, sha256, sha512.
The key and the message can be (both) string or array of bytes, nil otherwise"
(fn [algorithm key message]
(cond
(and (string? key) (string? message)) :string
(and (bytes-array? key) (bytes-array? message)) :byte
(and (string? key) (string? message) (some? (get-alg algorithm))) :string
(and (bytes-array? key) (bytes-array? message) (some? (get-alg algorithm))) :byte
:else :nil)))
;; By default
(defmethod hmac-sha1 :nil [_ _]
(defmethod hmac :nil [_ _ _]
nil)
;; When key and message are strings
(defmethod hmac-sha1 :string [key message]
(defmethod hmac :string [algorithm key message]
(if (or (empty? key) (empty? message))
""
(let [mac (doto (Mac/getInstance "HmacSHA1") (.init (SecretKeySpec. (.getBytes key) "HmacSHA1")))
(let [mac (doto (Mac/getInstance (get-alg algorithm)) (.init (SecretKeySpec. (.getBytes key) (get-alg algorithm))))
hmac-bytes (.doFinal mac (.getBytes message))]
;; Return the Base64 encoded HMAC
(.encodeToString (Base64/getEncoder) hmac-bytes))))
;; When key and message are arrays of bytes
(defmethod hmac-sha1 :byte [key message]
(defmethod hmac :byte [algorithm key message]
(if (nil? message)
(bytes (byte-array 0))
(let [mac (doto (Mac/getInstance "HmacSHA1") (.init (SecretKeySpec. key "HmacSHA1")))
(let [mac (doto (Mac/getInstance (get-alg algorithm)) (.init (SecretKeySpec. key (get-alg algorithm))))
hmac-bytes (.doFinal mac message)]
;; Return the Base64 encoded HMAC
(Base64/getEncoder) hmac-bytes)))
@@ -72,17 +83,19 @@
(defn get-otp
"Generate an OTP with the given secret (in base32) for the specified timestep"
([secret step]
(when (and secret step)
(let [k (b32/decode secret)
([secret algorithm digits period] ;;algorithm digits period
(when (and secret period)
(let [step (timestamp->steps (System/currentTimeMillis) period)
k (b32/decode secret)
c (long->bytes step)
hs (hmac-sha1 k c)
hs (hmac algorithm k c)
offset (bit-and (get hs (dec (count hs))) 0x0f) ;; int offset = hs[hs.length-1] & 0xf;
chunk (Arrays/copyOfRange hs offset (+ offset 4)) ;(take 4 (drop offset hs)) ;; byte[] chunk = Arrays.copyOfRange(hs, offset, offset+4)
]
(format "%06d" (-> chunk
(bytes->int)
(bit-and 0x7fffffff)
(rem 1000000))))))
(format (str "%0" digits "d")
(-> chunk
(bytes->int)
(bit-and 0x7fffffff)
(rem (int (m/pow 10 digits))))))))
([secret]
(get-otp secret (timestamp->steps (System/currentTimeMillis) 30))))
(get-otp secret "sha1" 6 30)))

View File

@@ -47,8 +47,7 @@
(comment
(exists-config)
(create-cfg?)
)
(create-cfg?))
(defn load-config
@@ -62,8 +61,8 @@
[cfg]
(when cfg
(spit cfg-file (str cfg-header (with-out-str
(binding [pp/*print-right-margin* 50]
(pp/pprint cfg)))))))
(binding [pp/*print-right-margin* 50]
(pp/pprint cfg)))))))
(defn delete-app
@@ -72,14 +71,18 @@
(defn add-app
[cfg name secret]
(conj (delete-app cfg name) {:name name :secret secret}))
([cfg name secret] (add-app cfg name secret nil nil "sha1" 6 30))
([cfg name secret user issuer algorithm digits period]
(conj (delete-app cfg name) {:name name :secret secret :user user :issuer issuer :algorithm algorithm :digits digits :period period})))
(defn list-apps
[cfg]
(map :name cfg))
(map :name
(filter #(contains? % :name) cfg)))
(comment
(list-apps (load-config)))
(defn get-app
[cfg name]
@@ -91,16 +94,16 @@
(create-cfg?)
(load-config)
(get-app [{:name "abc" :secret "def"} {:name "my-app" :secret "abc123"} {:name "another app" :secret "ABCDEF1234"}] "my-app2")
(with-out-str
(binding [pp/*print-right-margin* 50]
(pp/pprint [{:name "abc" :secret "def"} {:name "my-app" :secret "abc123"}])))
(store-config [{:name "abc" :secret "def"} {:name "my-app" :secret "abc123"}])
(store-config [{:name "abc" :secret "def"} {:name "my-app" :secret "abc123"} {:name "another app" :secret "ABCDEF1234"}])
(-> nil
(add-app "app1" "abc123abc123")
(add-app "app2" "abc123abc123")
(add-app "app1" "123456789012")
(store-config))
)
(store-config)))

View File

@@ -25,21 +25,21 @@
(is (= true (bytes-array? (.getBytes ""))))
(is (= true (bytes-array? (bytes (byte-array [0 0 0 0 0 0 0 0])))))))
(deftest hmac-sha1-test
(deftest hmac-test
(testing "border cases"
(is (= nil (hmac-sha1 nil nil)))
(is (= nil (hmac-sha1 "" nil)))
(is (= nil (hmac-sha1 nil "")))
(is (= nil (hmac-sha1 (.getBytes "") nil)))
(is (= nil (hmac-sha1 nil (.getBytes ""))))
(is (= "" (hmac-sha1 "" ""))))
(is (= nil (hmac nil nil nil)))
(is (= nil (hmac nil "" nil)))
(is (= nil (hmac nil nil "")))
(is (= nil (hmac nil (.getBytes "") nil)))
(is (= nil (hmac nil nil (.getBytes ""))))
(is (= "" (hmac "" "" ""))))
(testing "String params"
(is (= "63h3K4sN+c3NDEl3EGeA23jq/EY=" (hmac-sha1 "12345" "this is a message")))
(is (= "MA+ieo7t7MeQfyZR/X52dB1aXDI=" (hmac-sha1 "12345" "this is a longer message
(is (= "63h3K4sN+c3NDEl3EGeA23jq/EY=" (hmac "sha1" "12345" "this is a message")))
(is (= "MA+ieo7t7MeQfyZR/X52dB1aXDI=" (hmac "sha1" "12345" "this is a longer message
with some lines"))))
(testing "byte[] params"
(is (Arrays/equals (b64/decode "63h3K4sN+c3NDEl3EGeA23jq/EY=") (hmac-sha1 (.getBytes "12345") (.getBytes "this is a message"))))
(is (Arrays/equals (b64/decode "MA+ieo7t7MeQfyZR/X52dB1aXDI=") (hmac-sha1 (.getBytes "12345") (.getBytes "this is a longer message
(is (Arrays/equals (b64/decode "63h3K4sN+c3NDEl3EGeA23jq/EY=") (hmac "sha1" (.getBytes "12345") (.getBytes "this is a message"))))
(is (Arrays/equals (b64/decode "MA+ieo7t7MeQfyZR/X52dB1aXDI=") (hmac "sha1" (.getBytes "12345") (.getBytes "this is a longer message
with some lines"))))))
@@ -62,10 +62,10 @@
(deftest get-otp-test
(testing "Border cases"
(is (nil? (get-otp nil nil)))
(is (nil? (get-otp "" nil)))
(is (nil? (get-otp nil "")))
(is (nil? (get-otp nil 1000))))
(is (nil? (get-otp nil nil nil nil)))
(is (nil? (get-otp "" nil nil nil)))
(is (nil? (get-otp nil "" nil nil)))
(is (nil? (get-otp nil 1000 nil nil))))
(testing "Common usage"
(is (= "837552" (get-otp "MJXW42LBORXQ====" 10000)))
(is (= 6 (count (get-otp "MJXW42LBORXQ====" "sha1" 6 10000))))
(is (= 6 (count (get-otp "MJXW42LBORXQ===="))))))