Compare commits
24 Commits
e32e054c8c
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5651cc1ab2 | |||
| 96ed6ae1e9 | |||
| d8c3f5ee67 | |||
| 9eeb3571d5 | |||
| 58a17dc5bd | |||
| 4f29260f9b | |||
| df82cf3f44 | |||
| f32986f2db | |||
| 1205a79f19 | |||
| 38126f987a | |||
| fa0de1f624 | |||
| efec0ca07c | |||
| 4361782861 | |||
| 8d0fb81a9d | |||
| 4a1abd7fd7 | |||
| b7d3c6ce86 | |||
| 0a4e531b2f | |||
| a22f4a5670 | |||
| f408834726 | |||
| 09d8cd0e10 | |||
| d448ebe001 | |||
| e165dc107f | |||
| e18953b287 | |||
| 290c52a71f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/.clj-kondo/
|
/.clj-kondo/
|
||||||
/.cpcache/
|
/.cpcache/
|
||||||
/.lsp/
|
/.lsp/
|
||||||
|
/target/
|
||||||
.nrepl-port
|
.nrepl-port
|
||||||
.calva
|
.calva
|
||||||
|
|
||||||
|
|||||||
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
6
.idea/ClojureProjectResolveSettings.xml
generated
6
.idea/ClojureProjectResolveSettings.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ClojureProjectResolveSettings">
|
|
||||||
<currentScheme>IDE</currentScheme>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/clj-kondo.xml
generated
6
.idea/clj-kondo.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="KondoProjectSettings">
|
|
||||||
<option name="useCljKondo" value="YES" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
18
.idea/clojure-deps.xml
generated
18
.idea/clojure-deps.xml
generated
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DepsProjectsManager">
|
|
||||||
<option name="dirTypeMappings">
|
|
||||||
<set>
|
|
||||||
<FolderState>
|
|
||||||
<option name="dirUrl" value="file://$PROJECT_DIR$/test" />
|
|
||||||
<option name="type" value="TEST_SOURCE" />
|
|
||||||
</FolderState>
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
<option name="projectFiles">
|
|
||||||
<set>
|
|
||||||
<option value="file://$PROJECT_DIR$/deps.edn" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
8
.idea/dictionaries/project.xml
generated
8
.idea/dictionaries/project.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<component name="ProjectDictionaryState">
|
|
||||||
<dictionary name="project">
|
|
||||||
<words>
|
|
||||||
<w>hotp</w>
|
|
||||||
<w>topt</w>
|
|
||||||
</words>
|
|
||||||
</dictionary>
|
|
||||||
</component>
|
|
||||||
9
.idea/libraries/Deps__aero_1_1_6.xml
generated
9
.idea/libraries/Deps__aero_1_1_6.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: aero:1.1.6">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/aero/aero/1.1.6/aero-1.1.6.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: clojure.java-time:1.4.3">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/clojure/java-time/clojure.java-time/1.4.3/clojure.java-time-1.4.3.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: com.nextjournal/beholder:1.0.2">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/com/nextjournal/beholder/1.0.2/beholder-1.0.2.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
9
.idea/libraries/Deps__expound_0_9_0.xml
generated
9
.idea/libraries/Deps__expound_0_9_0.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: expound:0.9.0">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/expound/expound/0.9.0/expound-0.9.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
9
.idea/libraries/Deps__fipp_0_6_26.xml
generated
9
.idea/libraries/Deps__fipp_0_6_26.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: fipp:0.6.26">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/fipp/fipp/0.6.26/fipp-0.6.26.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
9
.idea/libraries/Deps__hawk_0_2_11.xml
generated
9
.idea/libraries/Deps__hawk_0_2_11.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: hawk:0.2.11">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/hawk/hawk/0.2.11/hawk-0.2.11.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: io.methvin/directory-watcher:0.17.3">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/io/methvin/directory-watcher/0.17.3/directory-watcher-0.17.3.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: lambdaisland/clj-diff:1.4.78">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/lambdaisland/clj-diff/1.4.78/clj-diff-1.4.78.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: lambdaisland/deep-diff2:2.11.216">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/lambdaisland/deep-diff2/2.11.216/deep-diff2-2.11.216.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: lambdaisland/kaocha:1.91.1392">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/lambdaisland/kaocha/1.91.1392/kaocha-1.91.1392.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: lambdaisland/tools.namespace:0.3.256">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/lambdaisland/tools.namespace/0.3.256/tools.namespace-0.3.256.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
9
.idea/libraries/Deps__meta_merge_1_0_0.xml
generated
9
.idea/libraries/Deps__meta_merge_1_0_0.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: meta-merge:1.0.0">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/meta-merge/meta-merge/1.0.0/meta-merge-1.0.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: mvxcvi/arrangement:2.1.0">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/mvxcvi/arrangement/2.1.0/arrangement-2.1.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: net.incongru.watchservice/barbary-watchservice:1.0">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/net/incongru/watchservice/barbary-watchservice/1.0/barbary-watchservice-1.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: net.java.dev.jna/jna:5.12.1">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.12.1/jna-5.12.1.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.clojure/clojure:1.12.1">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/clojure/clojure/1.12.1/clojure-1.12.1.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.clojure/core.rrb-vector:0.1.2">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/clojure/core.rrb-vector/0.1.2/core.rrb-vector-0.1.2.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.clojure/core.specs.alpha:0.4.74">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.clojure/java.classpath:1.0.0">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/clojure/java.classpath/1.0.0/java.classpath-1.0.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.clojure/spec.alpha:0.5.238">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.clojure/tools.cli:1.1.230">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/clojure/tools.cli/1.1.230/tools.cli-1.1.230.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.clojure/tools.reader:1.3.6">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/clojure/tools.reader/1.3.6/tools.reader-1.3.6.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.slf4j/slf4j-api:1.7.36">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: org.tcrawley/dynapath:1.1.0">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/tcrawley/dynapath/1.1.0/dynapath-1.1.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
9
.idea/libraries/Deps__progrock_0_1_2.xml
generated
9
.idea/libraries/Deps__progrock_0_1_2.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: progrock:0.1.2">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/progrock/progrock/0.1.2/progrock-0.1.2.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
9
.idea/libraries/Deps__slingshot_0_12_2.xml
generated
9
.idea/libraries/Deps__slingshot_0_12_2.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="Deps: slingshot:0.12.2">
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/slingshot/slingshot/0.12.2/slingshot-0.12.2.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC />
|
|
||||||
<SOURCES />
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="IDE SDK" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/clj-totp.iml" filepath="$PROJECT_DIR$/clj-totp.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
.idea/runConfigurations/REPL_for_clj_totp.xml
generated
12
.idea/runConfigurations/REPL_for_clj_totp.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="REPL for clj-totp" type="ClojureREPL" factoryName="Local" activateToolWindowBeforeRun="false">
|
|
||||||
<option name="configVersion" value="1" />
|
|
||||||
<option name="displayName" value="REPL for clj-totp" />
|
|
||||||
<option name="execution" value="DEPS" />
|
|
||||||
<module name="clj-totp" />
|
|
||||||
<option name="options" />
|
|
||||||
<option name="profiles" />
|
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
88
README.md
88
README.md
@@ -1,33 +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.
|
||||||
This project is done 100% in clojure. It uses `deps.edn` for configuring the project.
|
|
||||||
|
## Features
|
||||||
## Implementation timeline
|
|
||||||
|
### v1.0
|
||||||
### v1.0
|
- Functional TOTP generation
|
||||||
- [ ] Functional TOTP generation
|
- Get TOTP from command line
|
||||||
- [ ] Get TOTP from command line
|
- Continuous update every 30 seconds
|
||||||
- [ ] Store configuration in a simple BD (sqlite, for example)
|
|
||||||
|
## Usage
|
||||||
### v1.1
|
You can use the `clojure` command to run the program:
|
||||||
- [ ] REST API
|
```
|
||||||
- [ ] User management
|
clojure -M:run <params>
|
||||||
|
```
|
||||||
### v1.2
|
|
||||||
- [ ] Simple web connected to REST API
|
If you prefer using the distributed jar:
|
||||||
|
```
|
||||||
|
java -jar clj-topt-1.0.35-standalone.jar <params>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
25
build.clj
Normal file
25
build.clj
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
(ns build
|
||||||
|
(: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 class-dir "target/classes")
|
||||||
|
(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 uber [_]
|
||||||
|
(clean nil)
|
||||||
|
(b/copy-dir {:src-dirs ["src" "resources"]
|
||||||
|
:target-dir class-dir})
|
||||||
|
(b/compile-clj {:basis @basis
|
||||||
|
:ns-compile '[totp.app]
|
||||||
|
:class-dir class-dir})
|
||||||
|
(b/uber {:class-dir class-dir
|
||||||
|
:uber-file uber-file
|
||||||
|
:basis @basis
|
||||||
|
:main 'totp.app}))
|
||||||
42
clj-totp.iml
42
clj-totp.iml
@@ -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>
|
|
||||||
1
collect-deps.sh
Executable file
1
collect-deps.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
~/.sdkman/candidates/java/21.0.2-graalce/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image -jar target/clj-topt-1.0.32-standalone.jar g TUGOBTEHPSCMUCYAT6UPELNWGE -c
|
||||||
22
deps.edn
22
deps.edn
@@ -1,11 +1,19 @@
|
|||||||
{:paths ["src"]
|
{:paths ["src"]
|
||||||
:deps {clojure.java-time/clojure.java-time {:mvn/version "1.4.3"}
|
:deps {org.clojure/clojure {:mvn/version "1.12.1"}
|
||||||
mvxcvi/alphabase {:mvn/version "3.0.185"}}
|
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
|
||||||
|
;; 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/
|
||||||
|
|
||||||
:aliases {:test {:extra-paths ["test"] ;; https://cljdoc.org/d/uberdeps/uberdeps/1.4.0/doc/readme
|
: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"] ;; https://cljdoc.org/d/uberdeps/uberdeps/1.4.0/doc/readme
|
||||||
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}
|
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}
|
||||||
:main-opts ["-m" "kaocha.runner"]}
|
:main-opts ["-m" "kaocha.runner"]}
|
||||||
:uberdeps {
|
|
||||||
:replace-deps {uberdeps/uberdeps {:mvn/version "1.4.0"}}
|
;; Run with clj -T:build function-in-build
|
||||||
:replace-paths []
|
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}}
|
||||||
:main-opts ["-m" "uberdeps.uberjar"]}}}
|
:ns-default build}}}
|
||||||
|
|||||||
34
doc/db.plantuml
Normal file
34
doc/db.plantuml
Normal 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
BIN
doc/db.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
14
native.sh
Executable file
14
native.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
NATIVE=~/.sdkman/candidates/java/21.0.2-graalce/bin/native-image
|
||||||
|
UBERJAR=clj-topt-1.0.32-standalone.jar
|
||||||
|
BIN_FILE=totp
|
||||||
|
|
||||||
|
echo "Creating uberjar"
|
||||||
|
clojure -T:build uber
|
||||||
|
|
||||||
|
echo "Creating native image"
|
||||||
|
|
||||||
|
$NATIVE -jar target/$UBERJAR -o target/$BIN_FILE -H:+ReportExceptionStackTraces --features=clj_easy.graal_build_time.InitClojureClasses --report-unsupported-elements-at-runtime --verbose --no-fallback -H:ReflectionConfigurationFiles=./reflect_config.json
|
||||||
|
|
||||||
|
echo "Executable created on target/$BIN_FILE"
|
||||||
52
reflect_config.json
Normal file
52
reflect_config.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "com.sun.crypto.provider.HmacSHA1",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "<init>",
|
||||||
|
"parameterTypes": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "java.lang.reflect.Method",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "canAccess",
|
||||||
|
"parameterTypes": [
|
||||||
|
"java.lang.Object"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "java.util.Arrays",
|
||||||
|
"allDeclaredClasses": true,
|
||||||
|
"allPublicClasses": true,
|
||||||
|
"queryAllPublicMethods": true,
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "copyOfRange",
|
||||||
|
"parameterTypes": [
|
||||||
|
"byte[]",
|
||||||
|
"int",
|
||||||
|
"int"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "java.util.Timer",
|
||||||
|
"queryAllPublicMethods": true,
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "scheduleAtFixedRate",
|
||||||
|
"parameterTypes": [
|
||||||
|
"java.util.TimerTask",
|
||||||
|
"long",
|
||||||
|
"long"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
64
src/totp/app.clj
Normal file
64
src/totp/app.clj
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
(ns totp.app
|
||||||
|
(:require [totp.core :refer :all]
|
||||||
|
[cli-matic.core :refer [run-cmd]]
|
||||||
|
[cli-matic.utils :as U]
|
||||||
|
[clojure.pprint :as pp])
|
||||||
|
(:import [java.util TimerTask Timer])
|
||||||
|
(:gen-class))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- print-confinuous
|
||||||
|
([secret] (print-confinuous secret 30))
|
||||||
|
([secret step]
|
||||||
|
(let [step-millis (* 1000 step)
|
||||||
|
now (System/currentTimeMillis)
|
||||||
|
delay (int (- step-millis (rem now step-millis)))
|
||||||
|
fn-show (fn [s] (println (System/currentTimeMillis) "-> "(get-otp s)))
|
||||||
|
task (proxy [TimerTask] []
|
||||||
|
(run [] (fn-show secret)))]
|
||||||
|
(println "\n <Generating continuosly, press enter to stop>\n")
|
||||||
|
;; (println "Now:" now ", Delay:" delay ", Next execution: " (+ now delay))
|
||||||
|
(fn-show secret)
|
||||||
|
(. (new Timer) (scheduleAtFixedRate task delay step-millis)))
|
||||||
|
(read-line))) ;; Waits for a key press
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(print get-otp "MJXW42LBORXQ====")
|
||||||
|
(print-confinuous "MJXW42LBORXQ====")
|
||||||
|
)
|
||||||
|
|
||||||
|
(defn cmd-generate
|
||||||
|
[& {:keys [secret continuous] :as otps}]
|
||||||
|
;(pp/pprint otps)
|
||||||
|
(if continuous
|
||||||
|
(print-confinuous secret)
|
||||||
|
(println (get-otp secret))
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
(def cli-options
|
||||||
|
{:app {:command "totp"
|
||||||
|
:version "1.0"
|
||||||
|
: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:"
|
||||||
|
" totp generate \"MJXW42LBORXQ====\""
|
||||||
|
"Generate one TOTP, update each 30 seconds:"
|
||||||
|
" totp g -c \"MJXW42LBORXQ====\""]
|
||||||
|
:opts [{:option "secret"
|
||||||
|
:short 0
|
||||||
|
:as "Secret codified in BASE32"
|
||||||
|
:type :string
|
||||||
|
:default :present}
|
||||||
|
{:option "continuous"
|
||||||
|
:short "c"
|
||||||
|
:type :with-flag
|
||||||
|
:as "Contiuous mode"
|
||||||
|
:default false}]
|
||||||
|
:runs cmd-generate}]})
|
||||||
|
|
||||||
|
|
||||||
|
(defn -main [& args]
|
||||||
|
(run-cmd args cli-options))
|
||||||
@@ -5,19 +5,20 @@
|
|||||||
(java.util Base64 Arrays)
|
(java.util Base64 Arrays)
|
||||||
(java.nio ByteBuffer)))
|
(java.nio ByteBuffer)))
|
||||||
|
|
||||||
|
(def ^:private byte-array-type (type (.getBytes "")))
|
||||||
|
|
||||||
(defn timestamp->steps
|
(defn timestamp->steps
|
||||||
"Converts from UNIX timestamp in milliseconds to a number os steps of 's' seconds of duration"
|
"Converts from UNIX timestamp in milliseconds to a number os steps of 's' seconds of duration"
|
||||||
[time, step-size]
|
[time, step-size]
|
||||||
(if (or (nil? time) (nil? step-size) (zero? step-size))
|
(if (or (nil? time) (nil? step-size) (zero? step-size))
|
||||||
0
|
0
|
||||||
(quot (/ time 1000) step-size)))
|
(int (quot time (* 1000 step-size)))))
|
||||||
|
|
||||||
|
|
||||||
(defn bytes-array?
|
(defn bytes-array?
|
||||||
"Return true if x is a byte[]"
|
"Return true if x is a byte[]"
|
||||||
[x]
|
[x]
|
||||||
(= byte/1 (type x)))
|
(= byte-array-type (type x)))
|
||||||
|
|
||||||
|
|
||||||
(defmulti hmac-sha1
|
(defmulti hmac-sha1
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
(fn [key message]
|
(fn [key message]
|
||||||
(cond
|
(cond
|
||||||
(and (string? key) (string? message)) :string
|
(and (string? key) (string? message)) :string
|
||||||
(and (= byte/1 (type key)) (= byte/1 (type message))) :byte
|
(and (bytes-array? key) (bytes-array? message)) :byte
|
||||||
:else :nil)))
|
:else :nil)))
|
||||||
|
|
||||||
;; By default
|
;; By default
|
||||||
@@ -71,15 +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 step]
|
([secret step]
|
||||||
(when (and secret step)
|
(when (and secret step)
|
||||||
(let [k (b32/decode secret)
|
(let [k (b32/decode secret)
|
||||||
c (long->bytes step)
|
c (long->bytes step)
|
||||||
hs (hmac-sha1 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 "%6d" (-> chunk
|
(format "%06d" (-> chunk
|
||||||
(bytes->int)
|
(bytes->int)
|
||||||
(bit-and 0x7fffffff)
|
(bit-and 0x7fffffff)
|
||||||
(rem 1000000))))))
|
(rem 1000000))))))
|
||||||
|
([secret]
|
||||||
|
(get-otp secret (timestamp->steps (System/currentTimeMillis) 30))))
|
||||||
|
|||||||
106
src/totp/data.clj
Normal file
106
src/totp/data.clj
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
(ns totp.data
|
||||||
|
(:require [clojure.edn :as e]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[clojure.pprint :as pp]))
|
||||||
|
|
||||||
|
(defn join-path
|
||||||
|
"Joins several subpaths using system's path separator (/ un *NIX and \\ in windows)"
|
||||||
|
[& col]
|
||||||
|
(str/join java.io.File/separator col))
|
||||||
|
|
||||||
|
|
||||||
|
(def cfg-path (join-path (System/getProperty "user.home") ".config" "totp"))
|
||||||
|
|
||||||
|
(def cfg-file (join-path cfg-path "apps.edn"))
|
||||||
|
|
||||||
|
(def cfg-header ";; clj-totp configuration file
|
||||||
|
;; This file contents a list of maps with :name and :secret entries
|
||||||
|
;; Secrets must be encoded in BASE32
|
||||||
|
|
||||||
|
")
|
||||||
|
|
||||||
|
|
||||||
|
(defn exists-config
|
||||||
|
"Checks if the config file exists"
|
||||||
|
[]
|
||||||
|
(.exists (io/file cfg-file)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn create-cfg-file
|
||||||
|
"Creates the config file"
|
||||||
|
[]
|
||||||
|
(println "Creating " cfg-file)
|
||||||
|
(io/delete-file cfg-file true)
|
||||||
|
(io/make-parents cfg-file)
|
||||||
|
(spit cfg-file cfg-header)
|
||||||
|
true)
|
||||||
|
|
||||||
|
|
||||||
|
(defn create-cfg?
|
||||||
|
"Create configuration file if not exists. Overridable with allways = true"
|
||||||
|
([] (create-cfg? false))
|
||||||
|
([allways]
|
||||||
|
(if (or allways (not (exists-config)))
|
||||||
|
(create-cfg-file)
|
||||||
|
false)))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(exists-config)
|
||||||
|
(create-cfg?)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
(defn load-config
|
||||||
|
"Loads configuration from file"
|
||||||
|
[]
|
||||||
|
(e/read-string (slurp cfg-file)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn store-config
|
||||||
|
"Store configuration to file"
|
||||||
|
[cfg]
|
||||||
|
(when cfg
|
||||||
|
(spit cfg-file (str cfg-header (with-out-str
|
||||||
|
(binding [pp/*print-right-margin* 50]
|
||||||
|
(pp/pprint cfg)))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn delete-app
|
||||||
|
[cfg name]
|
||||||
|
(filter #(not= name (:name %)) cfg))
|
||||||
|
|
||||||
|
|
||||||
|
(defn add-app
|
||||||
|
[cfg name secret]
|
||||||
|
(conj (delete-app cfg name) {:name name :secret secret}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn list-apps
|
||||||
|
[cfg]
|
||||||
|
(map :name cfg))
|
||||||
|
|
||||||
|
|
||||||
|
(defn get-app
|
||||||
|
[cfg name]
|
||||||
|
(first (filter #(= name (:name %)) cfg)))
|
||||||
|
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(exists-config)
|
||||||
|
(create-cfg?)
|
||||||
|
(load-config)
|
||||||
|
|
||||||
|
(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"}])
|
||||||
|
|
||||||
|
(-> nil
|
||||||
|
(add-app "app1" "abc123abc123")
|
||||||
|
(add-app "app2" "abc123abc123")
|
||||||
|
(add-app "app1" "123456789012")
|
||||||
|
(store-config))
|
||||||
|
|
||||||
|
)
|
||||||
@@ -67,4 +67,5 @@
|
|||||||
(is (nil? (get-otp nil "")))
|
(is (nil? (get-otp nil "")))
|
||||||
(is (nil? (get-otp nil 1000))))
|
(is (nil? (get-otp nil 1000))))
|
||||||
(testing "Common usage"
|
(testing "Common usage"
|
||||||
(is (= "837552" (get-otp "MJXW42LBORXQ====" 10000)))))
|
(is (= "837552" (get-otp "MJXW42LBORXQ====" 10000)))
|
||||||
|
(is (= 6 (count (get-otp "MJXW42LBORXQ===="))))))
|
||||||
Reference in New Issue
Block a user