Compare commits

3 Commits

Author SHA1 Message Date
5651cc1ab2 Merge pull request 'release/1.0' (#1) from release/1.0 into main
Reviewed-on: #1
2025-09-01 18:38:12 +02:00
96ed6ae1e9 Updated READMEç 2025-09-01 18:33:47 +02:00
d8c3f5ee67 Deleting unfinished features 2025-09-01 10:37:11 +02:00
12 changed files with 110 additions and 422 deletions

View File

@@ -1,40 +1,55 @@
# clj-totp # clj-totp
TOTP (Time-based One Time Password) in clojure. It can be used in the command line, web API o simple embedded web. TOTP (Timebased One Time Password) in clojure. It can be used in the command line, web API o simple embeded web.
## What is TOPT ## What is TOPT
The TOPT is a standard used to generate a time-based password. Usually, this password is used as a second The TOPT is an standad used to generate a time-based password. Usually, this password is used as a second
factor authentication. factor authentication.
You can read more about the algorithm here: You can red more about the algorith here:
- Wikipedia: https://en.wikipedia.org/wiki/Time-based_one-time_password - Wikipedia: https://en.wikipedia.org/wiki/Time-based_one-time_password
- TOTP RFC: https://web.archive.org/web/20110711124823/http://tools.ietf.org/html/rfc6238 - TOTP RFC: https://web.archive.org/web/20110711124823/http://tools.ietf.org/html/rfc6238
- HOTP RFC: https://www.ietf.org/rfc/rfc4226.txt - HOTP RFC: https://www.ietf.org/rfc/rfc4226.txt
## The inside ## The inside
This project is done 100% in clojure. It uses `deps.edn` for configuring the project and `build.clj` for compiling. This project is done 100% in clojure. It uses `deps.edn` for configuring the project.
## Implementation timeline ## Features
### v1.0 ### v1.0
- [x] Functional TOTP generation - Functional TOTP generation
- [x] Get TOTP from command line - Get TOTP from command line
- [x] Continuous generation - Continuous update every 30 seconds
- [ ] Store configuration in a properties file or simple DB
### v1.1 ## Usage
- [ ] REST API You can use the `clojure` command to run the program:
- [ ] User management ```
- [ ] Robust BD backend (H2, datomic, or similar) clojure -M:run <params>
```
### v1.2 If you prefer using the distributed jar:
- [ ] Simple web connected to REST API ```
java -jar clj-topt-1.0.35-standalone.jar <params>
```
## Ideas You can use the binary (compiled with GraalVM) in linux environments:
- Import from google auth URL: https://github.com/dim13/otpauth ```
- Store passwords securely: https://github.com/weavejester/crypto-password totp <params>
```
All three methods are equivalent.
### Generate a single TOTP
You can simple run:
```
totp generate <secret in BASE32>
```
If want to update coninously the generated TOTP, you cand add the `-s` param:
```
totp generate <secret in BASE32> -s
```

View File

@@ -2,7 +2,7 @@
(:require [clojure.tools.build.api :as b])) (:require [clojure.tools.build.api :as b]))
(def lib 'es.rcorral/clj-topt) (def lib 'es.rcorral/clj-topt)
(def version (format "1.1.%s" (b/git-count-revs nil))) (def version (format "1.0.%s" (b/git-count-revs nil)))
(def class-dir "target/classes") (def class-dir "target/classes")
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version)) (def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="BuildSystem">
<option name="buildSystemId" value="CLOJURE_DEPS" />
<option name="displayName" value="clj-totp" />
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Deps: org.clojure/clojure:1.12.1" level="project" />
<orderEntry type="library" name="Deps: lambdaisland/deep-diff2:2.11.216" level="project" />
<orderEntry type="library" name="Deps: org.clojure/core.specs.alpha:0.4.74" level="project" />
<orderEntry type="library" name="Deps: lambdaisland/kaocha:1.91.1392" level="project" />
<orderEntry type="library" name="Deps: expound:0.9.0" level="project" />
<orderEntry type="library" name="Deps: org.clojure/spec.alpha:0.5.238" level="project" />
<orderEntry type="library" name="Deps: org.clojure/tools.cli:1.1.230" level="project" />
<orderEntry type="library" name="Deps: lambdaisland/clj-diff:1.4.78" level="project" />
<orderEntry type="library" name="Deps: net.incongru.watchservice/barbary-watchservice:1.0" level="project" />
<orderEntry type="library" name="Deps: slingshot:0.12.2" level="project" />
<orderEntry type="library" name="Deps: fipp:0.6.26" level="project" />
<orderEntry type="library" name="Deps: com.nextjournal/beholder:1.0.2" level="project" />
<orderEntry type="library" name="Deps: aero:1.1.6" level="project" />
<orderEntry type="library" name="Deps: lambdaisland/tools.namespace:0.3.256" level="project" />
<orderEntry type="library" name="Deps: mvxcvi/arrangement:2.1.0" level="project" />
<orderEntry type="library" name="Deps: io.methvin/directory-watcher:0.17.3" level="project" />
<orderEntry type="library" name="Deps: progrock:0.1.2" level="project" />
<orderEntry type="library" name="Deps: org.clojure/java.classpath:1.0.0" level="project" />
<orderEntry type="library" name="Deps: clojure.java-time:1.4.3" level="project" />
<orderEntry type="library" name="Deps: org.clojure/core.rrb-vector:0.1.2" level="project" />
<orderEntry type="library" name="Deps: net.java.dev.jna/jna:5.12.1" level="project" />
<orderEntry type="library" name="Deps: org.clojure/tools.reader:1.3.6" level="project" />
<orderEntry type="library" name="Deps: org.tcrawley/dynapath:1.1.0" level="project" />
<orderEntry type="library" name="Deps: org.slf4j/slf4j-api:1.7.36" level="project" />
<orderEntry type="library" name="Deps: hawk:0.2.11" level="project" />
<orderEntry type="library" name="Deps: meta-merge:1.0.0" level="project" />
</component>
</module>

View File

@@ -3,11 +3,6 @@
io.github.clojure/tools.build {:mvn/version "0.10.10"} io.github.clojure/tools.build {:mvn/version "0.10.10"}
mvxcvi/alphabase {:mvn/version "3.0.185"} ;; https://github.com/greglook/alphabase mvxcvi/alphabase {:mvn/version "3.0.185"} ;; https://github.com/greglook/alphabase
cli-matic/cli-matic {:mvn/version "0.5.4"} ;; https://github.com/l3nz/cli-matic cli-matic/cli-matic {:mvn/version "0.5.4"} ;; https://github.com/l3nz/cli-matic
;; For SQLite
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1048"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
;; For Datomic local
com.datomic/local {:mvn/version "1.0.291"};; https://docs.datomic.com/datomic-local.html
;; Native image (GraalVM) ;; Native image (GraalVM)
com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"}};; Tutorial: https://shagunagrawal.me/posts/setup-clojure-with-graalvm-for-native-image/ com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"}};; Tutorial: https://shagunagrawal.me/posts/setup-clojure-with-graalvm-for-native-image/

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
NATIVE=~/.sdkman/candidates/java/21.0.2-graalce/bin/native-image NATIVE=~/.sdkman/candidates/java/21.0.2-graalce/bin/native-image
UBERJAR=clj-topt-1.0.38-standalone.jar UBERJAR=clj-topt-1.0.32-standalone.jar
BIN_FILE=totp BIN_FILE=totp
echo "Creating uberjar" echo "Creating uberjar"

View File

@@ -1,21 +1,19 @@
(ns totp.app (ns totp.app
(:require [totp.core :refer :all] (:require [totp.core :refer :all]
[totp.data :refer :all]
[cli-matic.core :refer [run-cmd]] [cli-matic.core :refer [run-cmd]]
[cli-matic.utils :as U] [cli-matic.utils :as U]
[clojure.pprint :as pp] [clojure.pprint :as pp])
[clojure.pprint :as pprint])
(:import [java.util TimerTask Timer]) (:import [java.util TimerTask Timer])
(:gen-class)) (:gen-class))
(defn- print-confinuous (defn- print-confinuous
([secret] (print-confinuous secret "sha1" 6 30)) ([secret] (print-confinuous secret 30))
([secret algorithm digits period] ([secret step]
(let [step-millis (* 1000 period) (let [step-millis (* 1000 step)
now (System/currentTimeMillis) now (System/currentTimeMillis)
delay (int (- step-millis (rem now step-millis))) delay (int (- step-millis (rem now step-millis)))
fn-show (fn [s] (println (System/currentTimeMillis) "-> "(get-otp s algorithm digits period))) fn-show (fn [s] (println (System/currentTimeMillis) "-> "(get-otp s)))
task (proxy [TimerTask] [] task (proxy [TimerTask] []
(run [] (fn-show secret)))] (run [] (fn-show secret)))]
(println "\n <Generating continuosly, press enter to stop>\n") (println "\n <Generating continuosly, press enter to stop>\n")
@@ -30,210 +28,36 @@
) )
(defn cmd-generate (defn cmd-generate
[& {:keys [secret continuous algorithm digits period] :as opts}] [& {:keys [secret continuous] :as otps}]
;;(pp/pprint opts) ;(pp/pprint otps)
(if continuous (if continuous
(print-confinuous secret algorithm digits period) (print-confinuous secret)
(println (get-otp secret algorithm digits period)))) (println (get-otp secret))
))
(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)
(case command
"info" (println "Configuration file: "
(if (exists-config)
cfg-file
(str "<not found. default:" cfg-file ">")))
"init" (if (exists-config)
(do
(println "Configuration already exists, this will delete it. Are you sure? [N/y]")
(case (read-line)
"y" (create-cfg-file)
"Y" (create-cfg-file)
(println "Cancelling operation.")))
(create-cfg-file))
"show" (do
(println "Config file:\n")
;(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)
(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)
(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 (def cli-options
{:app {:command "totp" {:app {:command "totp"
:version "1.1" :version "1.0"
:description ["Generate a TOTP"]} :description ["Generate a TOTP"]}
:commands [{:command "generate" :commands [{:command "generate" :short "g"
:description "Generate one TOTP for a BASE32 secret, ignoring configured apps" :description "Generate one TOTP with a given secret in BASE32"
:examples ["Generate one TOTP for a provided BASE32 secret:" :examples ["Generate one TOTP and exit:"
" totp generate ABCD1234" " totp generate \"MJXW42LBORXQ====\""
"Generate one TOTP and refresh it continuosly:" "Generate one TOTP, update each 30 seconds:"
" totp generate -c ABCD1234"] " totp g -c \"MJXW42LBORXQ====\""]
:opts [{:option "secret" :short 0 :opts [{:option "secret"
:as "Secret encoded in BASE32" :short 0
:as "Secret codified in BASE32"
:type :string :type :string
:default :present} :default :present}
{:option "continuous" :short "c" {:option "continuous"
:short "c"
:type :with-flag
:as "Contiuous mode" :as "Contiuous mode"
:type :with-flag
: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}] :default false}]
:runs cmd-get} :runs cmd-generate}]})
{:command "config" :short "c"
:description "Manage configuration"
:examples ["Show location for the configuration file:"
" totp config info"
"Recreate config (warning: it will delete the existing config file):"
" totp config init"
"Show configuration:"
" totp config show"]
:opts [{:option "command" :short 0
:as "Command to execute. See examples"
:type #{"info" "init" "show"}
:default :present}]
:runs cmd-config}
{:command "list" :short "l"
:description "List existing apps"
:examples ["List apps:"
" 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"
:description "Add a new application with an unique name"
:examples ["Add a new application named 'app1' with a BASE32 secred, with defaults:"
" totp add app1 \"MJXW42LBORXQ====\""
"Add an application, with all posible configuration params:"
" topt add app2 \"MJXW42LBORXQ====\" -u \"user1@server\" -i my_provider -a sha1 -d 6 -p 30 --update"]
:opts [{:option "name" :short 0
:as "Unique name (alias) for the application"
:type :string
:default :present}
{:option "secret" :short 1
:as "Secret encoded in BASE32"
:type :string
:default :present}
{:option "user" :short "u"
:as "Username in the format <user>@<server>"
:type :string}
{:option "issuer" :short "i"
:as "The issuer (provider) of the service"
:type :string}
{: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}
{:option "update"
:as "Update an app with the same name if exists"
:type :with-flag
:default false}]
:runs cmd-add}
{:command "delete" :short "d"
:description "Removes an existing application by it's unique name"
:examples ["Remove the application name app1"
" totp remove app1"]
:opts [{:option "name" :short 0
:as "Unique name of the application"
:type :string
:default :present}
{:option "force" :short "f"
:as "Don't ask, just remove"
:type :with-flag
:default false}]
:runs cmd-delete}]})
(defn -main [& args] (defn -main [& args]

View File

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

View File

@@ -6,7 +6,7 @@
(defn join-path (defn join-path
"Joins several subpaths using system's path separator (/ un *NIX and \\ in windows)" "Joins several subpaths using system's path separator (/ un *NIX and \\ in windows)"
[& col] [& col]
(str/join java.io.File/separator col)) (str/join java.io.File/separator col))
@@ -47,7 +47,8 @@
(comment (comment
(exists-config) (exists-config)
(create-cfg?)) (create-cfg?)
)
(defn load-config (defn load-config
@@ -60,9 +61,9 @@
"Store configuration to file" "Store configuration to file"
[cfg] [cfg]
(when cfg (when cfg
(spit cfg-file (str cfg-header (with-out-str (spit cfg-file (str cfg-header (with-out-str
(binding [pp/*print-right-margin* 50] (binding [pp/*print-right-margin* 50]
(pp/pprint cfg))))))) (pp/pprint cfg)))))))
(defn delete-app (defn delete-app
@@ -71,18 +72,14 @@
(defn add-app (defn add-app
([cfg name secret] (add-app cfg name secret nil nil "sha1" 6 30)) [cfg name secret]
([cfg name secret user issuer algorithm digits period] (conj (delete-app cfg name) {:name name :secret secret}))
(conj (delete-app cfg name) {:name name :secret secret :user user :issuer issuer :algorithm algorithm :digits digits :period period})))
(defn list-apps (defn list-apps
[cfg] [cfg]
(map :name (map :name cfg))
(filter #(contains? % :name) cfg)))
(comment
(list-apps (load-config)))
(defn get-app (defn get-app
[cfg name] [cfg name]
@@ -94,16 +91,16 @@
(create-cfg?) (create-cfg?)
(load-config) (load-config)
(get-app [{:name "abc" :secret "def"} {:name "my-app" :secret "abc123"} {:name "another app" :secret "ABCDEF1234"}] "my-app2")
(with-out-str (with-out-str
(binding [pp/*print-right-margin* 50] (binding [pp/*print-right-margin* 50]
(pp/pprint [{:name "abc" :secret "def"} {:name "my-app" :secret "abc123"}]))) (pp/pprint [{: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"}]) (store-config [{:name "abc" :secret "def"} {:name "my-app" :secret "abc123"}])
(-> nil (-> nil
(add-app "app1" "abc123abc123") (add-app "app1" "abc123abc123")
(add-app "app2" "abc123abc123") (add-app "app2" "abc123abc123")
(add-app "app1" "123456789012") (add-app "app1" "123456789012")
(store-config))) (store-config))
)

View File

@@ -1,65 +0,0 @@
(ns totp.db.datomic
(:require [datomic.client.api :as d]))
(def db-path (str cfg-path java.io.File/separator "data"))
(def cfg {:server-type :datomic-local
:system "local-data"
:storage-dir db-path})
(def client (d/client cfg))
;; Schema for our database
(def totp-schema [{:db/ident :app/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity
:db/doc "Unique name of the application. Between 2 and 32 chars"
:db.attr/preds (fn [s] (<= 3 (count s) 15))}
{:db/ident :app/desc
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "(optional) Description of the application"}
{:db/ident :app/secret
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "Secret BASE32"}
{:db/ident :app/period
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one
:db/doc "Time slice in seconds (30 by default)"}
{:db/ident :app/config
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "(Optional) Extra config"}
{:db/ident :user/login
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity
:db/doc "Identifier for the user"}
{:db/ident :user/passwd
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "Password"}
{:db/ident :user/active
:db/valueType :db.type/boolean
:db/cardinality :db.cardinality/one
:db/doc "Is the user active?"}
{:db/ident :user/desc
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "(Optional) Description of the user"}
{:db/ident :user/config
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "(Optional) Extra config"}
{:db/ident :user/apps
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many
:db/doc "Applications for this user"}])
(defn init-db
[client]
(d/create-database client {:db-name "totp"}))

View File

@@ -1,23 +0,0 @@
(ns totp.db.sqlite
(:require [next.jdbc :as jdbc]))
;; DB configuration
(def db {:dbname "totp-data.sqlite"
:dbtype "sqlite"})
;; DB parsed config
(def data-source (jdbc/get-datasource db))
(defn init-db
"Create an empty DB"
[]
(jdbc/execute! data-source ["
create table apps (
id int auto_increment primary key,
name varchar(32),
desc varchar(255)
)"]))
(comment
(init-db)
)

View File

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

Binary file not shown.