Compare commits

9 Commits

Author SHA1 Message Date
017291f784 Full modularized project 2025-10-13 01:22:03 +02:00
17a7a09ab0 Fully modularized project 2025-10-13 01:21:36 +02:00
3a6fd107c0 Generate a version if no one is provided 2025-10-12 12:34:48 +02:00
4c31950a88 Reestructured in subprojects 2025-10-12 12:28:40 +02:00
aa71cb1d76 Fix bug 2025-10-07 23:22:08 +02:00
c746675045 Force compression even if file exists 2025-10-07 23:20:24 +02:00
4052995ba8 Native compilation for Windows 2025-10-01 16:07:23 +02:00
44f48fced8 better native compile script 2025-10-01 15:49:23 +02:00
c78e89a94b Use example 2025-10-01 00:08:27 +02:00
122 changed files with 766 additions and 53 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
/.clj-kondo/ /.clj-kondo/
/.cpcache/ /**/.cpcache/
/.lsp/ /.lsp/
/target/ /target/
.nrepl-port .nrepl-port

View File

@@ -152,6 +152,7 @@ clj-totp.sh import <alias> "<url>"
- [x] Native compilation script corrections - [x] Native compilation script corrections
### v2 ### v2
- [x] Restructurate as a multiproject
- [ ] REST API - [ ] REST API
- [ ] User management - [ ] User management
- [ ] Robust BD backend (H2, datomic, or similar) - [ ] Robust BD backend (H2, datomic, or similar)
@@ -176,14 +177,14 @@ The first step is to install Java JDK, version 11 or newer (version 21 recommend
To execute manually the main function, simple use the `:run` alias: To execute manually the main function, simple use the `:run` alias:
```clojure ```bash
clojure -M:run <commands and parameters> clojure -M:run/cli <commands and parameters>
``` ```
To build the uberjar: To build the uberjar:
```clojure ```bash
clojure -T:build uber clojure -T:build :uber/cli
``` ```
There is a utility script to build a native executable using Graal VM. Please, edit the script and There is a utility script to build a native executable using Graal VM. Please, edit the script and

201
build.clj
View File

@@ -1,37 +1,174 @@
(ns build (ns build
(:require [clojure.tools.build.api :as b])) (:refer-clojure :exclude [test])
(:require [clojure.tools.build.api :as b]
[clojure.java.io :as io]
[clojure.pprint :as pp]
[clojure.java.basis :as basis]))
(def lib 'es.rcorral/clj-totp) (def lib-group "es.rcorral")
(def version (format "1.2.%s" (b/git-count-revs nil))) (def artifact-prefix "clj-totp")
(def target-dir "target") (def subprojs-base "projects")
(def class-dir (str target-dir "/classes")) (def curr-version (format "2.0.%s" (b/git-count-revs nil)))
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))
;; delay to defer side effects (artifact downloads)
(def basis (delay (b/create-basis {:project "deps.edn"})))
(defn clean [_]
(b/delete {:path "target"}))
(defn compile-java [_]
(b/javac {:src-dirs ["java"]
:class-dir class-dir
:basis @basis
:javac-opts ["-source" "11" "--target" "11" "-proc:none"]}))
;; Builds artifact's full descriptor for each subproject
(defn lib [subproj]
(symbol (str lib-group "/" artifact-prefix "-" subproj )))
;; Basis for each subproject, using their own deps.edn
;; Injects :extra-deps from :build as additional dependencies
(defn basis [subproj]
(delay (b/create-basis {:project (str subprojs-base "/" subproj "/deps.edn")
;; Inject extra deps as deps
:extra {:deps (get-in (basis/initial-basis) [:aliases :build :extra-deps])}
})))
;; Show basis generated for a subproject
#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]}
(defn uber [_] (defn show-basis [subproj]
(clean nil) (println (with-out-str
(b/copy-dir {:src-dirs ["src"] (pp/pprint
:target-dir class-dir}) @(basis subproj)
(b/copy-file {:src "resources/clj-totp.sh" ;(basis/initial-basis)
:target "target/clj-totp.sh"}) ))))
(compile-java nil)
(b/compile-clj {:basis @basis (comment
:ns-compile '[totp.app] (pp/pprint (keys (basis/initial-basis)))
:class-dir class-dir}) (pp/pprint (:deps (basis/initial-basis)))
(b/uber {:class-dir class-dir (pp/pprint (:libs (basis/initial-basis)))
:uber-file uber-file (pp/pprint (sort (keys (:aliases (basis/initial-basis)))))
:basis @basis (get-in (basis/initial-basis) [:aliases :build :extra-deps])
:main 'totp.app})) )
;; Target dir for each subproject
(defn target-dir [subproj]
(str "target/" subproj))
;; Path for compiled classes
(defn class-dir [subproj]
(str (target-dir subproj) "/" "classes"))
;; Jar file for each subproject. :uber type adds -standalone suffix
(defn jar-file [subproj version type]
(format "target/%s-%s-%s%s.jar" artifact-prefix subproj version
(if (= type :uber) "-standalone" "")))
;; Clean target dir for subproject
(defn clean [{:keys [subproj]}]
(b/delete {:path (target-dir subproj)})
(println "Project" subproj "cleaned"))
;; Compile java classes, only if java subdir exists
(defn compile-java [subproj]
(let [java-dir (str subprojs-base "/" subproj "/java")]
(when (.exists (io/file java-dir))
(println "Compiling java code for" subproj)
(b/javac {:src-dirs [java-dir]
:class-dir (class-dir subproj)
:basis @(basis subproj)
:javac-opts ["-source" "11" "--target" "11" "-proc:none"]}))))
;; Create a jar file
(defn jar
"Build a simple jar file, with no dependencies included."
[{:keys [subproj version]
:or {version curr-version}}]
(let [target-dir (target-dir subproj)
class-dir (class-dir subproj)
src-dir (str subprojs-base "/" subproj "/src")
resources-dir (str subprojs-base "/" subproj "/resources")
basis (basis subproj)
jar-file (jar-file subproj version :plain)]
;; Clean only class dir
(b/delete {:path class-dir})
;; Copy code
(b/copy-dir {:src-dirs [src-dir]
:target-dir class-dir})
;; Copy resources
(b/copy-dir {:src-dirs [resources-dir]
:target-dir target-dir})
;; Compile java code, if exists
(compile-java subproj)
;; Build jar
(b/jar {:class-dir class-dir
:basis @basis
:jar-file jar-file
:lib (lib subproj)
:version version})
(println "Generated jar file:" jar-file)))
;; Create an uber jar, with all dependencies inside
#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]}
(defn uber
"Build a uberjar with all dependencies included"
[{:keys [subproj version main-ns]
:or {version curr-version}}]
(let [target-dir (target-dir subproj)
basis (basis subproj)
class-dir (class-dir subproj)
src-dir (str subprojs-base "/" subproj "/src")
resources-dir (str subprojs-base "/" subproj "/resources")
uber-file (jar-file subproj version :uber)]
;(println "Using basis: ")(show-basis subproj)
(b/delete {:path class-dir})
(b/copy-dir {:src-dirs [src-dir]
:target-dir class-dir})
(b/copy-dir {:src-dirs [resources-dir]
:target-dir target-dir})
(compile-java subproj)
(b/compile-clj {:basis @basis
:src-dirs [src-dir] :class-dir class-dir})
(b/uber {:class-dir class-dir
:uber-file uber-file
:basis @basis
:main main-ns})
(println "Generated uberjar executable:" uber-file)))
;; Multimethod to get the name of all subdirs in a dir.
;; Accepts strings or files
(defmulti get-subdirs type)
(defmethod get-subdirs
java.lang.String [dir]
(get-subdirs (io/file dir)))
(defmethod get-subdirs
java.io.File [dir]
(if (.isDirectory dir)
(filter #(.isDirectory %) (.listFiles dir))
(println "Directory" subprojs-base "doesn't exists!")))
;; Get the name of all subdir in a given directory
(defn get-subdir-names
"Get a list projects in the 'projects' directory"
[dir-name]
(map #(.getName %) (get-subdirs dir-name)))
(comment
(get-subdirs "projects")
(get-subdirs (io/file "projects"))
(get-subdir-names "projects")
)
;; Generate jar files for all projects
(defn jar-all
"Build jar files for all projects"
[& {:keys [version]
:or {version curr-version}}]
(dorun (map #(jar {:subproj % :version version}) (get-subdir-names subprojs-base))))
(comment
(jar-all )
)

View File

@@ -1,24 +1,60 @@
{:paths ["src" "resources" "target/classes"] {:paths ["src" "resources" "target/classes"]
:deps {org.clojure/clojure {:mvn/version "1.12.1"} :deps {org.clojure/clojure {:mvn/version "1.12.1"}
io.github.clojure/tools.build {:mvn/version "0.10.10"} ;; Native image (GraalVM). Tutorial: https://shagunagrawal.me/posts/setup-clojure-with-graalvm-for-native-image/
mvxcvi/alphabase {:mvn/version "3.0.185"} ;; https://github.com/greglook/alphabase com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"}
cli-matic/cli-matic {:mvn/version "0.5.4"} ;; https://github.com/l3nz/cli-matic ;; Local subprojects
com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"} ;; Tutorial: https://shagunagrawal.me/posts/setup-clojure-with-graalvm-for-native-image/ clj-totp/core {:local/root "projects/core"}
;; Protobuf for java clj-totp/cli {:local/root "projects/cli"}
com.google.protobuf/protobuf-java {:mvn/version "3.25.8"} clj-totp/web {:local/root "projects/web"}
;; Progress bar
com.github.pmonks/spinner {:mvn/version "2.0.284"}
} }
:aliases {;; Execute the app :aliases {;; Execute the app.
:run {:main-opts ["-m" "totp.app"]} :run {:main-opts ["-m" "totp.app"]}
;:run {:exec-fn totp.app/-main}
;; Execute the app (prepared for more subprojects)
:run/cli {:main-opts ["-m" "totp.app"]}
;:run {:exec-fn totp.app/-main}
;; Kaocha runner. You can use the 'kaocha' wrapper located in ~/bin/kaocha ;; Kaocha runner. You can use the 'kaocha' wrapper located in ~/bin/kaocha
:test {:extra-paths ["test"] ;; Check test.edn for kaocha runner's config
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}} :test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}
lambdaisland/kaocha-cloverage {:mvn/version "1.1.89"}}
:main-opts ["-m" "kaocha.runner"]} :main-opts ["-m" "kaocha.runner"]}
;; Run with clj -T:build function-in-build ;; Run with clj -T:build function-in-build
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}} :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:ns-default build}}} ;; Used by all compilations
:extra-deps {clj-totp/core {:local/root "projects/core"}}
:ns-default build}
;; Aliases for easy building
:build/core {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:ns-default build
:exec-fn jar
:exec-args {:subproj "core"}}
:build/cli {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}
clj-totp/core {:local/root "projects/core"}}
:ns-default build
:exec-fn jar
:exec-args {:subproj "cli"}}
:build/web {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:replace-deps {clj-totp/core {:local/root "projects/core"}}
:ns-default build
:exec-fn jar
:exec-args {:subproj "web"}}
;; Build the three libraries
:build/all {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:replace-deps {clj-totp/core {:local/root "projects/core"}}
:ns-default build
:exec-fn jar-all}
;; Build uber jar for CLI app
:uber/cli {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:ns-default build
:exec-fn uber
:exec-args {:subproj "cli" :main-ns "totp.app"}}}}

34
doc/db.plantuml Normal file
View File

@@ -0,0 +1,34 @@
@startuml
' configuration
skinparam linetype ortho
entity "user" as user {
id: number
--
login: varchar(64)
passw: varchar(512)
active: shortint
desc: varchar(512)
config: varchar(512)
}
entity "app" as app {
id: number
--
name: varchar(32)
desc: varchar(512)
secret: varchar(512)
period: int
config: varchar(512)
}
entity "user_app" as user_app {
user_id: number
app_id: number
--
}
user ||--o{ user_app
app ||--o{ user_app
@enduml

BIN
doc/db.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -4,7 +4,7 @@ NATIVE=~/.sdkman/candidates/java/21.0.2-graalce/bin/native-image
BIN_FILE=totp BIN_FILE=totp
echo "Creating uberjar" echo "Creating uberjar"
#clojure -T:build uber clojure -T:build uber
UBERJAR=$(realpath --relative-to=target target/clj-totp-*-standalone.jar) UBERJAR=$(realpath --relative-to=target target/clj-totp-*-standalone.jar)
echo "Creating native image" echo "Creating native image"
@@ -27,4 +27,4 @@ cp target/$BIN_FILE ~/bin
echo "Copied to ~/bin/$BIN_FILE" echo "Copied to ~/bin/$BIN_FILE"
echo "Compress executable for distribution" echo "Compress executable for distribution"
xz target/$BIN_FILE xz -fv target/$BIN_FILE

15
projects/cli/deps.edn Executable file
View File

@@ -0,0 +1,15 @@
{:paths ["src" "resources" "target/classes"]
:deps {;clj-totp/core {:local/root "../core"}
org.clojure/clojure {:mvn/version "1.12.1"}
cli-matic/cli-matic {:mvn/version "0.5.4"} ;; https://github.com/l3nz/cli-matic
;; Progress bar
com.github.pmonks/spinner {:mvn/version "2.0.284"}}
:aliases {;; Execute the app
;:run {:main-opts ["-m" "totp.app"]}
;; Kaocha runner. You can use the 'kaocha' wrapper located in ~/bin/kaocha
:test {:extra-paths ["test"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}
:main-opts ["-m" "kaocha.runner"]}}}

1
projects/cli/tests.edn Normal file
View File

@@ -0,0 +1 @@
#kaocha/v1 {}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
-m
kaocha.runner

12
projects/core/deps.edn Executable file
View File

@@ -0,0 +1,12 @@
{:paths ["src" "resources" "target/classes"]
:deps {org.clojure/clojure {:mvn/version "1.12.1"}
mvxcvi/alphabase {:mvn/version "3.0.185"} ;; https://github.com/greglook/alphabase
;; Protobuf for java
com.google.protobuf/protobuf-java {:mvn/version "3.25.8"}
}
:aliases {;; Kaocha runner. You can use the 'kaocha' wrapper located in ~/bin/kaocha
:test {:extra-paths ["test"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}
:main-opts ["-m" "kaocha.runner"]}}}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,101 @@
(ns totp.core
(:require [alphabase.base32 :as b32]
[clojure.math :as m])
(:import (javax.crypto Mac)
(javax.crypto.spec SecretKeySpec)
(java.util Base64 Arrays)
(java.nio ByteBuffer)))
(def ^:private byte-array-type (type (.getBytes "")))
(defn timestamp->steps
"Converts from UNIX timestamp in milliseconds to a number os steps of 's' seconds of duration"
[time, step-size]
(if (or (nil? time) (nil? step-size) (zero? step-size))
0
(int (quot time (* 1000 step-size)))))
(defn bytes-array?
"Return true if x is a byte[]"
[x]
(= byte-array-type (type x)))
(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) (some? (get-alg algorithm))) :string
(and (bytes-array? key) (bytes-array? message) (some? (get-alg algorithm))) :byte
:else :nil)))
;; By default
(defmethod hmac :nil [_ _ _]
nil)
;; When key and message are strings
(defmethod hmac :string [algorithm key message]
(if (or (empty? key) (empty? message))
""
(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 :byte [algorithm key message]
(if (nil? message)
(bytes (byte-array 0))
(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)))
(defn long->bytes
"Converts a long to an array of 8 bytes"
[l]
;;Java equivalent: ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(someLong).array();
(when (integer? l)
(-> (ByteBuffer/allocate (/ Long/SIZE Byte/SIZE))
(.putLong l)
(.array))))
(defn bytes->int
"Converts an array of 4 bytes to an integer"
[bytes]
;;Java equivalent: ByteBuffer.wrap(data).getInt()
(when (some? bytes)
(.getInt (ByteBuffer/wrap bytes))))
(defn get-otp
"Generate an OTP with the given secret (in base32) for the specified timestep"
([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 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 (str "%0" digits "d")
(-> chunk
(bytes->int)
(bit-and 0x7fffffff)
(rem (int (m/pow 10 digits))))))))
([secret]
(get-otp secret "sha1" 6 30)))

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More