Repository navigation
Expand file tree
/
Copy pathIntegrationExample.java
More file actions
98 lines (88 loc) · 5.01 KB
/
Copy pathIntegrationExample.java
File metadata and controls
98 lines (88 loc) · 5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import ru.loolzaaa.AesGcmSecretCipher;
import ru.loolzaaa.ClientError;
import ru.loolzaaa.Cotp;
import ru.loolzaaa.CotpCard;
import ru.loolzaaa.CotpCardPrinter;
import ru.loolzaaa.CotpChallenge;
import ru.loolzaaa.CotpCredential;
import ru.loolzaaa.CotpCredentialCodec;
import ru.loolzaaa.CotpEnrollment;
import ru.loolzaaa.CotpPolicyResolver;
import ru.loolzaaa.CotpScheme;
import ru.loolzaaa.SchemeRegistry;
import ru.loolzaaa.VerificationResult;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
/**
* Self-contained, compilable COTP integration example.
*
* <p>It exercises the full server-side lifecycle with the real library:</p>
* <ol>
* <li>{@code new Cotp(scheme, resolver)} — default 5x5/BIP-39 scheme and per-context policies;</li>
* <li>{@code enroll(...)} — generate a card, print it, obtain a PIN hash + credential;</li>
* <li>seal the credential with {@link AesGcmSecretCipher} and unseal it again (persistence);</li>
* <li>{@code challenge(card, "kiosk_login", now)} — issue a cell;</li>
* <li>{@code authenticate(...)} — verify the printed word + PIN;</li>
* <li>{@link VerificationResult#toClientError()} — the coarse, anti-oracle client outcome;</li>
* <li>a wrong-word attempt to show that factor-specific failures collapse to one generic error.</li>
* </ol>
*
* <p>Build/run (JDK 17+; the default scheme loads the bundled dictionary from the classpath).
* Note {@code Select-Object -ExpandProperty FullName}: on Windows PowerShell 5.1 a bare
* {@code FileInfo} argument stringifies to the file name only:</p>
* <pre>
* javac -encoding UTF-8 -d out (Get-ChildItem -Recurse cotp-java\src\main\java -Filter *.java ^
* | Where-Object Name -ne 'module-info.java' | Select-Object -ExpandProperty FullName) ^
* examples\IntegrationExample.java
* java -cp "out;cotp-java\src\main\resources" IntegrationExample
* </pre>
*/
public final class IntegrationExample {
private IntegrationExample() {
}
public static void main(String[] args) {
// 1. Facade: default scheme (5x5, BIP-39, HMAC-SHA-256) and the recommended
// per-context policies (kiosk_login -> COOLDOWN 24h, stepup -> CHALLENGE 60s,
// service_desk -> CELL, default -> CELL).
Cotp cotp = new Cotp(CotpScheme.defaults(), CotpPolicyResolver.defaults());
// 2. Enrolment. The PIN is never stored; only its PBKDF2-HMAC-SHA256 hash is returned.
char[] pin = "1234".toCharArray();
CotpEnrollment enrollment = cotp.enroll("COTP-DEMO-0001", pin);
System.out.println("Enrolled: " + enrollment.card());
System.out.print(CotpCardPrinter.render(enrollment.card()));
// 3. Persist the credential (it contains the raw card_secret) encrypted at rest.
AesGcmSecretCipher cipher = AesGcmSecretCipher.generate();
String encodedCredential = enrollment.credentialEncoded(); // COTP1|...
byte[] sealedBytes = cipher.encrypt(encodedCredential.getBytes(StandardCharsets.UTF_8));
String storedSecret = Base64.getEncoder().encodeToString(sealedBytes);
System.out.println("Stored (encrypted) credential length=" + storedSecret.length()
+ " chars; pinHash saved separately.");
// 4. Later request / another node: unseal and deterministically rebuild the card.
byte[] openedBytes = Base64.getDecoder().decode(storedSecret);
CotpCredential credential = CotpCredentialCodec.decode(
new String(cipher.decrypt(openedBytes), StandardCharsets.UTF_8));
CotpCard restoredCard = credential.toCard(SchemeRegistry.defaults());
System.out.println("Restored: " + restoredCard
+ "; scheme=" + credential.schemeId());
// 5. Challenge + correct answer. The user reads the word printed at the challenged cell.
Instant now = Instant.now();
CotpChallenge challenge = cotp.challenge(restoredCard, "kiosk_login", now);
String word = restoredCard.wordAt(challenge.cell());
VerificationResult result = cotp.authenticate(
restoredCard, enrollment.pinHash(), challenge, word, pin, now);
ClientError client = result.toClientError();
System.out.println("Challenge: cell=" + challenge.cellId()
+ " mode=" + challenge.mode() + " expiresAt=" + challenge.expiresAt());
System.out.println("Verify (correct): result=" + result
+ " client=" + client + " success=" + client.isSuccess());
// 6. Anti-oracle: a wrong word produces the same generic ClientError as a wrong PIN would.
CotpChallenge second = cotp.challenge(restoredCard, "kiosk_login", now);
VerificationResult bad = cotp.authenticate(
restoredCard, enrollment.pinHash(), second, "definitely-not-the-word", pin, now);
System.out.println("Verify (wrong word): result=" + bad
+ " client=" + bad.toClientError());
String accept = client == ClientError.NONE ? "OK" : "UNEXPECTED";
System.out.println("FLOW: " + accept);
}
}