Compare commits
2 Commits
v1.0
...
6dc9100b45
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dc9100b45 | |||
| 1863e82595 |
53
README.md
53
README.md
@@ -1,55 +1,40 @@
|
||||
# clj-totp
|
||||
|
||||
TOTP (Timebased One Time Password) in clojure. It can be used in the command line, web API o simple embeded web.
|
||||
TOTP (Time-based One Time Password) in clojure. It can be used in the command line, web API o simple embedded web.
|
||||
|
||||
## What is TOPT
|
||||
|
||||
The TOPT is an standad used to generate a time-based password. Usually, this password is used as a second
|
||||
The TOPT is a standard used to generate a time-based password. Usually, this password is used as a second
|
||||
factor authentication.
|
||||
|
||||
You can red more about the algorith here:
|
||||
You can read more about the algorithm here:
|
||||
|
||||
- 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
|
||||
- HOTP RFC: https://www.ietf.org/rfc/rfc4226.txt
|
||||
|
||||
|
||||
## The inside
|
||||
|
||||
This project is done 100% in clojure. It uses `deps.edn` for configuring the project.
|
||||
This project is done 100% in clojure. It uses `deps.edn` for configuring the project and `build.clj` for compiling.
|
||||
|
||||
## Features
|
||||
## Implementation timeline
|
||||
|
||||
### v1.0
|
||||
- Functional TOTP generation
|
||||
- Get TOTP from command line
|
||||
- Continuous update every 30 seconds
|
||||
- [x] Functional TOTP generation
|
||||
- [x] Get TOTP from command line
|
||||
- [x] Continuous generation
|
||||
- [ ] Store configuration in a properties file or simple DB
|
||||
|
||||
## Usage
|
||||
You can use the `clojure` command to run the program:
|
||||
```
|
||||
clojure -M:run <params>
|
||||
```
|
||||
### v1.1
|
||||
- [ ] REST API
|
||||
- [ ] User management
|
||||
- [ ] Robust BD backend (H2, datomic, or similar)
|
||||
|
||||
If you prefer using the distributed jar:
|
||||
```
|
||||
java -jar clj-topt-1.0.35-standalone.jar <params>
|
||||
```
|
||||
### v1.2
|
||||
- [ ] Simple web connected to REST API
|
||||
|
||||
You can use the binary (compiled with GraalVM) in linux environments:
|
||||
```
|
||||
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
|
||||
```
|
||||
## Ideas
|
||||
- Import from google auth URL: https://github.com/dim13/otpauth
|
||||
- Store passwords securely: https://github.com/weavejester/crypto-password
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
(:require [clojure.tools.build.api :as b]))
|
||||
|
||||
(def lib 'es.rcorral/clj-topt)
|
||||
(def version (format "1.0.%s" (b/git-count-revs nil)))
|
||||
(def version (format "1.1.%s" (b/git-count-revs nil)))
|
||||
(def class-dir "target/classes")
|
||||
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))
|
||||
|
||||
|
||||
42
clj-totp.iml
Normal file
42
clj-totp.iml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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>
|
||||
5
deps.edn
5
deps.edn
@@ -3,6 +3,11 @@
|
||||
io.github.clojure/tools.build {:mvn/version "0.10.10"}
|
||||
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
|
||||
;; 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)
|
||||
com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"}};; Tutorial: https://shagunagrawal.me/posts/setup-clojure-with-graalvm-for-native-image/
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
NATIVE=~/.sdkman/candidates/java/21.0.2-graalce/bin/native-image
|
||||
UBERJAR=clj-topt-1.0.32-standalone.jar
|
||||
UBERJAR=clj-topt-1.0.38-standalone.jar
|
||||
BIN_FILE=totp
|
||||
|
||||
echo "Creating uberjar"
|
||||
|
||||
132
src/totp/app.clj
132
src/totp/app.clj
@@ -1,5 +1,6 @@
|
||||
(ns totp.app
|
||||
(:require [totp.core :refer :all]
|
||||
[totp.data :refer :all]
|
||||
[cli-matic.core :refer [run-cmd]]
|
||||
[cli-matic.utils :as U]
|
||||
[clojure.pprint :as pp])
|
||||
@@ -28,36 +29,141 @@
|
||||
)
|
||||
|
||||
(defn cmd-generate
|
||||
[& {:keys [secret continuous] :as otps}]
|
||||
;(pp/pprint otps)
|
||||
[& {:keys [secret continuous] :as opts}]
|
||||
;(pp/pprint opts)
|
||||
(if continuous
|
||||
(print-confinuous secret)
|
||||
(println (get-otp secret))
|
||||
))
|
||||
|
||||
|
||||
(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)))))
|
||||
|
||||
|
||||
(defn cmd-list
|
||||
[])
|
||||
|
||||
|
||||
(defn cmd-add
|
||||
[& {:keys [name secret user issuer algorithm digits period update] :as opts}]
|
||||
(pp/pprint opts))
|
||||
|
||||
|
||||
(defn cmd-delete
|
||||
[& {:keys [name force] :as opts}]
|
||||
(pp/pprint opts))
|
||||
|
||||
|
||||
(def cli-options
|
||||
{:app {:command "totp"
|
||||
:version "1.0"
|
||||
: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 exit:"
|
||||
:examples ["Generate one TOTP and exits:"
|
||||
" totp generate \"MJXW42LBORXQ====\""
|
||||
"Generate one TOTP, update each 30 seconds:"
|
||||
" totp g -c \"MJXW42LBORXQ====\""]
|
||||
:opts [{:option "secret"
|
||||
:short 0
|
||||
:as "Secret codified in BASE32"
|
||||
"Generate one TOTP and refresh it continuosly:"
|
||||
" totp g \"MJXW42LBORXQ====\""]
|
||||
:opts [{:option "secret" :short 0
|
||||
:as "Secret encoded in BASE32"
|
||||
:type :string
|
||||
:default :present}
|
||||
{:option "continuous"
|
||||
:short "c"
|
||||
:type :with-flag
|
||||
{:option "continuous" :short "c"
|
||||
:as "Contiuous mode"
|
||||
:type :with-flag
|
||||
:default false}]
|
||||
:runs cmd-generate}]})
|
||||
: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"]
|
||||
: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 :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 :flag
|
||||
:default false}]
|
||||
:runs cmd-delete}]})
|
||||
|
||||
|
||||
(defn -main [& args]
|
||||
|
||||
65
src/totp/db/datomic.clj
Normal file
65
src/totp/db/datomic.clj
Normal file
@@ -0,0 +1,65 @@
|
||||
(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"}))
|
||||
23
src/totp/db/sqlite.clj
Normal file
23
src/totp/db/sqlite.clj
Normal file
@@ -0,0 +1,23 @@
|
||||
(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)
|
||||
)
|
||||
BIN
totp-data.sqlite
Normal file
BIN
totp-data.sqlite
Normal file
Binary file not shown.
Reference in New Issue
Block a user