diff --git a/.editorconfig b/.editorconfig
index b1faa38..865f611 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,11 +5,11 @@ root = true
[*]
indent_style = space
-indent_size = 4
+indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
-[*.xml]
-indent_size = 2
+[*.java]
+indent_size = 4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..a9e8668
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,24 @@
+name: Java CI with Maven
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: "17"
+ distribution: "temurin"
+ cache: maven
+
+ - name: Run tests with Maven
+ run: mvn -B test --file pom.xml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..741e3f4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# MoC Java
+MoC client for Java
+
+[](https://github.com/makeOurCity/moc-java/actions/workflows/test.yml)
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e235bcd..78d5058 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,6 +3,7 @@
4.0.0
+
city.makeour
moc
@@ -14,22 +15,57 @@
UTF-8
- 1.8
- 1.8
+ 17
+ 17
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
- junit
- junit
- 4.11
+ org.junit.jupiter
+ junit-jupiter
+ 5.9.2
test
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.1
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.1
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.3.1
+ test
+
software.amazon.awssdk
- aws-sdk-java
+ cognitoidentityprovider
2.31.20
+
+ com.github.makeOurCity
+ ngsiv2-java
+ 0.0.2
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ 1.70
+
@@ -51,7 +87,12 @@
maven-surefire-plugin
- 2.22.1
+ 3.5.3
+
+
+ **/*Test.java
+
+
maven-jar-plugin
@@ -77,4 +118,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/city/makeour/FetchCognitoToken.java b/src/main/java/city/makeour/FetchCognitoToken.java
deleted file mode 100644
index 945bd91..0000000
--- a/src/main/java/city/makeour/FetchCognitoToken.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package city.makeour;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient;
-import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminInitiateAuthRequest;
-import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminInitiateAuthResponse;
-import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthFlowType;
-import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthenticationResultType;
-
-public class FetchCognitoToken {
-
- protected CognitoIdentityProviderClient client;
- protected String cognitoUserPoolId;
- protected String cognitoClientId;
- protected String refreshToken;
-
- protected String username;
- protected String password;
-
- public void construct(String cognitoUserPoolId, String cognitoClientId) {
- this.cognitoClientId = cognitoClientId;
- this.cognitoUserPoolId = cognitoUserPoolId;
- this.refreshToken = null;
- // this.client = CognitoIdentityProviderClient.getInstance();
- }
-
- public void setAuthParameters(String username, String password) {
- this.username = username;
- this.password = password;
- }
-
- public String fetchToken() {
- AdminInitiateAuthRequest authRequest = null;
-
- if (this.refreshToken != null) {
- Map authParameters = new HashMap<>();
- authParameters.put("REFRESH_TOKEN", this.refreshToken);
-
- authRequest = AdminInitiateAuthRequest.builder()
- .clientId(this.cognitoClientId)
- .userPoolId(this.cognitoUserPoolId)
- .authParameters(authParameters)
- .authFlow(AuthFlowType.REFRESH_TOKEN_AUTH)
- .build();
- } else {
- Map authParameters = new HashMap<>();
- authParameters.put("USERNAME", username);
- authParameters.put("PASSWORD", password);
-
- authRequest = AdminInitiateAuthRequest.builder()
- .clientId(this.cognitoClientId)
- .userPoolId(this.cognitoUserPoolId)
- .authParameters(authParameters)
- .authFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH)
- .build();
- }
-
- AdminInitiateAuthResponse response = this.client.adminInitiateAuth(authRequest);
- AuthenticationResultType result = response.authenticationResult();
-
- this.refreshToken = result.refreshToken();
-
- if (this.refreshToken == null) {
- System.out.println("Failed to fetch token");
- }
-
- return result.idToken();
- }
-}
diff --git a/src/main/java/city/makeour/moc/FetchCognitoToken.java b/src/main/java/city/makeour/moc/FetchCognitoToken.java
new file mode 100644
index 0000000..9be8b45
--- /dev/null
+++ b/src/main/java/city/makeour/moc/FetchCognitoToken.java
@@ -0,0 +1,96 @@
+package city.makeour.moc;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+import city.makeour.moc.auth.srp.SrpAuthenticationHelper;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient;
+import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthFlowType;
+import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthenticationResultType;
+import software.amazon.awssdk.services.cognitoidentityprovider.model.ChallengeNameType;
+import software.amazon.awssdk.services.cognitoidentityprovider.model.InitiateAuthRequest;
+import software.amazon.awssdk.services.cognitoidentityprovider.model.InitiateAuthResponse;
+import software.amazon.awssdk.services.cognitoidentityprovider.model.RespondToAuthChallengeRequest;
+import software.amazon.awssdk.services.cognitoidentityprovider.model.RespondToAuthChallengeResponse;
+
+public class FetchCognitoToken implements TokenFetcherInterface {
+
+ private String cognitoUserPoolId;
+ private String cognitoClientId;
+ private String username;
+ private String password;
+ private Region region = Region.AP_NORTHEAST_1;
+
+ private SrpAuthenticationHelper helper;
+ private CognitoIdentityProviderClient cognitoClient;
+
+ public FetchCognitoToken(String cognitoUserPoolId, String cognitoClientId) {
+ this(Region.AP_NORTHEAST_1, cognitoUserPoolId, cognitoClientId);
+ }
+
+ public FetchCognitoToken(Region region, String cognitoUserPoolId, String cognitoClientId) {
+ this.cognitoUserPoolId = cognitoUserPoolId;
+ this.cognitoClientId = cognitoClientId;
+ this.region = region;
+
+ this.helper = new SrpAuthenticationHelper(this.cognitoUserPoolId);
+
+ this.cognitoClient = CognitoIdentityProviderClient.builder()
+ .region(this.region)
+ .credentialsProvider(DefaultCredentialsProvider.create())
+ .build();
+ }
+
+ public void setAuthParameters(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public Token fetchTokenWithSrpAuth() throws InvalidKeyException, NoSuchAlgorithmException {
+ InitiateAuthRequest initiateAuthRequest = InitiateAuthRequest.builder()
+ .authFlow(AuthFlowType.USER_SRP_AUTH)
+ .clientId(this.cognitoClientId)
+ .authParameters(Map.of(
+ "USERNAME", this.username,
+ "SRP_A", helper.getA()))
+ .build();
+ InitiateAuthResponse initiateAuthResponse = cognitoClient.initiateAuth(initiateAuthRequest);
+ Map challengeParameters = initiateAuthResponse.challengeParameters();
+
+ String userIdForSrp = challengeParameters.get("USER_ID_FOR_SRP");
+ String salt = challengeParameters.get("SALT");
+ String srpB = challengeParameters.get("SRP_B");
+ String secretBlock = challengeParameters.get("SECRET_BLOCK");
+
+ byte[] signatureKey = helper.getPasswordAuthenticationKey(userIdForSrp, password,
+ srpB, salt,
+ secretBlock);
+ String timestamp = helper.getCurrentFormattedTimestamp();
+ String signature = helper.calculateSignature(userIdForSrp, secretBlock, timestamp, signatureKey);
+
+ Map challengeResponses = new HashMap<>();
+ challengeResponses.put("USERNAME", userIdForSrp);
+ challengeResponses.put("PASSWORD_CLAIM_SECRET_BLOCK", secretBlock);
+ challengeResponses.put("PASSWORD_CLAIM_SIGNATURE", signature);
+ challengeResponses.put("TIMESTAMP", timestamp);
+
+ RespondToAuthChallengeRequest respondRequest = RespondToAuthChallengeRequest.builder()
+ .challengeName(ChallengeNameType.PASSWORD_VERIFIER)
+ .clientId(this.cognitoClientId)
+ .challengeResponses(challengeResponses)
+ .build();
+
+ RespondToAuthChallengeResponse authChallengeResponse = cognitoClient.respondToAuthChallenge(respondRequest);
+ AuthenticationResultType authResult = authChallengeResponse.authenticationResult();
+
+ return new Token(authResult.idToken(), authResult.refreshToken());
+ }
+
+ public Token fetchToken() throws InvalidKeyException, NoSuchAlgorithmException {
+ return this.fetchTokenWithSrpAuth();
+ }
+}
diff --git a/src/main/java/city/makeour/moc/MocClient.java b/src/main/java/city/makeour/moc/MocClient.java
new file mode 100644
index 0000000..14526a9
--- /dev/null
+++ b/src/main/java/city/makeour/moc/MocClient.java
@@ -0,0 +1,52 @@
+package city.makeour.moc;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import city.makeour.ngsi.v2.api.EntitiesApi;
+import city.makeour.ngsi.v2.invoker.ApiClient;
+
+public class MocClient {
+ protected ApiClient apiClient;
+
+ protected EntitiesApi entitiesApi;
+
+ protected TokenFetcherInterface tokenFetcher;
+
+ protected RefreshTokenStorageInterface refreshTokenStorage;
+
+ public MocClient() {
+ this("https://orion.sandbox.makeour.city");
+ }
+
+ public MocClient(String basePath) {
+ this.apiClient = new ApiClient();
+ this.apiClient.setBasePath(basePath);
+
+ this.entitiesApi = new EntitiesApi(this.apiClient);
+ this.refreshTokenStorage = new RefreshTokenStorage();
+ }
+
+ public EntitiesApi entities() {
+ return this.entitiesApi;
+ }
+
+ public void setMocAuthInfo(String cognitoUserPoolId, String cognitoClientId) {
+ this.tokenFetcher = new FetchCognitoToken(cognitoUserPoolId, cognitoClientId);
+ }
+
+ public void login(String username, String password) throws InvalidKeyException, NoSuchAlgorithmException {
+ if (this.tokenFetcher == null) {
+ throw new IllegalStateException("MocClient is not initialized with Cognito auth info.");
+ }
+
+ this.tokenFetcher.setAuthParameters(username, password);
+ Token token = this.tokenFetcher.fetchToken();
+ this.setToken(token.getIdToken());
+ this.refreshTokenStorage.setRefreshToken(token.getRefreshToken());
+ }
+
+ public void setToken(String token) {
+ this.apiClient.addDefaultHeader("Authorization", token);
+ }
+}
diff --git a/src/main/java/city/makeour/moc/RefreshTokenStorage.java b/src/main/java/city/makeour/moc/RefreshTokenStorage.java
new file mode 100644
index 0000000..dddedc5
--- /dev/null
+++ b/src/main/java/city/makeour/moc/RefreshTokenStorage.java
@@ -0,0 +1,22 @@
+package city.makeour.moc;
+
+public class RefreshTokenStorage implements RefreshTokenStorageInterface {
+
+ private String refreshToken;
+
+ public RefreshTokenStorage() {
+ this.refreshToken = null;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ public void setRefreshToken(String refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ public boolean hasRefreshToken() {
+ return this.refreshToken != null;
+ }
+}
diff --git a/src/main/java/city/makeour/moc/RefreshTokenStorageInterface.java b/src/main/java/city/makeour/moc/RefreshTokenStorageInterface.java
new file mode 100644
index 0000000..a73892c
--- /dev/null
+++ b/src/main/java/city/makeour/moc/RefreshTokenStorageInterface.java
@@ -0,0 +1,10 @@
+package city.makeour.moc;
+
+public interface RefreshTokenStorageInterface {
+
+ public String getRefreshToken();
+
+ public void setRefreshToken(String refreshToken);
+
+ public boolean hasRefreshToken();
+}
diff --git a/src/main/java/city/makeour/moc/Token.java b/src/main/java/city/makeour/moc/Token.java
new file mode 100644
index 0000000..984834d
--- /dev/null
+++ b/src/main/java/city/makeour/moc/Token.java
@@ -0,0 +1,19 @@
+package city.makeour.moc;
+
+public class Token {
+ private String idToken;
+ private String refreshToken;
+
+ public Token(String idToken, String refreshToken) {
+ this.idToken = idToken;
+ this.refreshToken = refreshToken;
+ }
+
+ public String getIdToken() {
+ return idToken;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+}
diff --git a/src/main/java/city/makeour/moc/TokenFetcherInterface.java b/src/main/java/city/makeour/moc/TokenFetcherInterface.java
new file mode 100644
index 0000000..fc4c7af
--- /dev/null
+++ b/src/main/java/city/makeour/moc/TokenFetcherInterface.java
@@ -0,0 +1,10 @@
+package city.makeour.moc;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+public interface TokenFetcherInterface {
+ void setAuthParameters(String username, String password);
+
+ Token fetchToken() throws InvalidKeyException, NoSuchAlgorithmException;
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/HKDF.java b/src/main/java/city/makeour/moc/auth/srp/HKDF.java
new file mode 100644
index 0000000..05c4815
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/HKDF.java
@@ -0,0 +1,38 @@
+package city.makeour.moc.auth.srp;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class HKDF {
+
+ private LargeS S;
+ private SmallU u;
+
+ public HKDF(LargeS S, SmallU u) {
+ this.S = S;
+ this.u = u;
+ }
+
+ public byte[] value() throws NoSuchAlgorithmException, InvalidKeyException {
+ byte[] ikm = Helper.padHex(S.value());
+ byte[] salt = u.value().toByteArray();
+
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(salt, "HmacSHA256"));
+ byte[] prk = mac.doFinal(ikm);
+
+ mac.init(new SecretKeySpec(prk, "HmacSHA256"));
+ byte[] info = "Caldera Derived Key".getBytes(StandardCharsets.UTF_8);
+ byte[] infoWithCounter = new byte[info.length + 1];
+ System.arraycopy(info, 0, infoWithCounter, 0, info.length);
+ infoWithCounter[info.length] = 0x01;
+
+ byte[] okm = mac.doFinal(infoWithCounter);
+ return Arrays.copyOfRange(okm, 0, 16);
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/Helper.java b/src/main/java/city/makeour/moc/auth/srp/Helper.java
new file mode 100644
index 0000000..97884dd
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/Helper.java
@@ -0,0 +1,55 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+
+public class Helper {
+ public static byte[] padHex(BigInteger bigInt) {
+ boolean isNegative = bigInt.signum() < 0;
+ String hex = bigInt.abs().toString(16);
+
+ // 奇数桁なら先頭に 0 を追加
+ if (hex.length() % 2 != 0) {
+ hex = "0" + hex;
+ }
+
+ // MSBが立っていれば "00" を先頭に追加(符号ビットを避ける)
+ if (hex.matches("^[89a-fA-F].*")) {
+ hex = "00" + hex;
+ }
+
+ if (isNegative) {
+ // 補数計算: 反転 → +1
+ StringBuilder flipped = new StringBuilder();
+ for (char c : hex.toCharArray()) {
+ int nibble = ~Character.digit(c, 16) & 0xF;
+ flipped.append(Integer.toHexString(nibble));
+ }
+ BigInteger flippedBigInt = new BigInteger(flipped.toString(), 16).add(BigInteger.ONE);
+ hex = flippedBigInt.toString(16);
+
+ // MSBがFF8〜なら短縮される→不要(JSでも無視できるレベル)
+ if (hex.length() % 2 != 0)
+ hex = "0" + hex;
+ }
+
+ return hexStringToByteArray(hex);
+ }
+
+ public static byte[] hexStringToByteArray(String s) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ + Character.digit(s.charAt(i + 1), 16));
+ }
+ return data;
+ }
+
+ public static String toHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/LargeA.java b/src/main/java/city/makeour/moc/auth/srp/LargeA.java
new file mode 100644
index 0000000..3bc52ff
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/LargeA.java
@@ -0,0 +1,22 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+
+public class LargeA {
+ private LargeN N;
+ private SmallA a;
+ private SmallG g;
+
+ public LargeA(LargeN N, SmallA a, SmallG g) {
+ this.N = N;
+ this.a = a;
+ this.g = g;
+ }
+
+ public BigInteger value() {
+ BigInteger gValue = this.g.value();
+ BigInteger aValue = this.a.value();
+ BigInteger NValue = this.N.value();
+ return gValue.modPow(aValue, NValue);
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/LargeB.java b/src/main/java/city/makeour/moc/auth/srp/LargeB.java
new file mode 100644
index 0000000..dcbe9bc
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/LargeB.java
@@ -0,0 +1,16 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+
+public class LargeB {
+
+ private String B;
+
+ public LargeB(String B) {
+ this.B = B;
+ }
+
+ public BigInteger value() {
+ return new BigInteger(1, Helper.hexStringToByteArray(this.B));
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/LargeN.java b/src/main/java/city/makeour/moc/auth/srp/LargeN.java
new file mode 100644
index 0000000..7b9538c
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/LargeN.java
@@ -0,0 +1,26 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+
+public class LargeN {
+ private static final BigInteger N = new BigInteger(1, Helper.hexStringToByteArray(
+ "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD"
+ +
+ "3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F"
+ +
+ "24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552"
+ +
+ "BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF0"
+ +
+ "6F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64EC"
+ +
+ "FB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A"
+ +
+ "0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D1"
+ +
+ "20A93AD2CAFFFFFFFFFFFFFFFF"));
+
+ public BigInteger value() {
+ return N;
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/LargeS.java b/src/main/java/city/makeour/moc/auth/srp/LargeS.java
new file mode 100644
index 0000000..010a73e
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/LargeS.java
@@ -0,0 +1,41 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+
+public class LargeS {
+
+ private LargeB B;
+ private SmallK k;
+ private SmallG g;
+ private SmallX x;
+ private SmallA a;
+ private SmallU u;
+ private LargeN N;
+
+ public LargeS(LargeB B, SmallK k, SmallG g, SmallX x, SmallA a, SmallU u, LargeN N) {
+ this.B = B;
+ this.k = k;
+ this.g = g;
+ this.x = x;
+ this.a = a;
+ this.u = u;
+ this.N = N;
+ }
+
+ public BigInteger value() throws NoSuchAlgorithmException {
+ BigInteger NValue = this.N.value();
+ BigInteger BValue = this.B.value();
+ BigInteger xValue = this.x.value();
+ BigInteger aValue = this.a.value();
+ BigInteger uValue = this.u.value();
+ BigInteger kValue = this.k.value();
+ BigInteger gValue = this.g.value();
+
+ BigInteger gModPowX = gValue.modPow(xValue, NValue);
+ BigInteger kgx = kValue.multiply(gModPowX).mod(NValue);
+ BigInteger base = BValue.subtract(kgx).mod(NValue);
+ BigInteger exp = aValue.add(uValue.multiply(xValue));
+ return base.modPow(exp, NValue);
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/SmallA.java b/src/main/java/city/makeour/moc/auth/srp/SmallA.java
new file mode 100644
index 0000000..25062c7
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/SmallA.java
@@ -0,0 +1,24 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+
+public class SmallA {
+
+ private LargeN N;
+ private final BigInteger a;
+
+ public SmallA(LargeN N) {
+ this.N = N;
+ a = new BigInteger(1, generateRandomBytes(128)).mod(this.N.value());
+ }
+
+ public BigInteger value() {
+ return a;
+ }
+
+ private static byte[] generateRandomBytes(int size) {
+ byte[] bytes = new byte[size];
+ new java.security.SecureRandom().nextBytes(bytes);
+ return bytes;
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/SmallG.java b/src/main/java/city/makeour/moc/auth/srp/SmallG.java
new file mode 100644
index 0000000..9debe97
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/SmallG.java
@@ -0,0 +1,12 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+
+public class SmallG {
+ private static final BigInteger G = BigInteger.valueOf(2);
+
+ public BigInteger value() {
+ return G;
+ }
+
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/SmallK.java b/src/main/java/city/makeour/moc/auth/srp/SmallK.java
new file mode 100644
index 0000000..7debdb7
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/SmallK.java
@@ -0,0 +1,26 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class SmallK {
+ private LargeN N;
+ private SmallG g;
+
+ SmallK(LargeN N, SmallG g) {
+ this.N = N;
+ this.g = g;
+ }
+
+ public BigInteger value() throws NoSuchAlgorithmException {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] nPadded = Helper.padHex(this.N.value());
+ byte[] gPadded = Helper.padHex(this.g.value());
+ byte[] ng = new byte[nPadded.length + gPadded.length];
+ System.arraycopy(nPadded, 0, ng, 0, nPadded.length);
+ System.arraycopy(gPadded, 0, ng, nPadded.length, gPadded.length);
+ byte[] hash = digest.digest(ng);
+ return new BigInteger(1, hash);
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/SmallU.java b/src/main/java/city/makeour/moc/auth/srp/SmallU.java
new file mode 100644
index 0000000..c246211
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/SmallU.java
@@ -0,0 +1,27 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class SmallU {
+
+ private LargeA A;
+ private LargeB B;
+
+ public SmallU(LargeA A, LargeB B) {
+ this.A = A;
+ this.B = B;
+ }
+
+ public BigInteger value() throws NoSuchAlgorithmException {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] aPadded = Helper.padHex(A.value());
+ byte[] bPadded = Helper.padHex(B.value());
+ byte[] combined = new byte[aPadded.length + bPadded.length];
+ System.arraycopy(aPadded, 0, combined, 0, aPadded.length);
+ System.arraycopy(bPadded, 0, combined, aPadded.length, bPadded.length);
+ byte[] uHash = digest.digest(combined);
+ return new BigInteger(1, uHash);
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/SmallX.java b/src/main/java/city/makeour/moc/auth/srp/SmallX.java
new file mode 100644
index 0000000..6e79cec
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/SmallX.java
@@ -0,0 +1,42 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class SmallX {
+
+ private String userPoolName;
+ private String userIdForSrp;
+ private String password;
+ private BigInteger salt;
+
+ public SmallX(String userPoolName, String userIdForSrp, String password, BigInteger salt) {
+ this.userPoolName = userPoolName;
+ this.userIdForSrp = userIdForSrp;
+ this.password = password;
+ this.salt = salt;
+ }
+
+ public BigInteger value() throws NoSuchAlgorithmException {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ String userPass = this.userPoolName + this.userIdForSrp + ":" + password;
+ byte[] userPassHash = digest.digest(userPass.getBytes(StandardCharsets.UTF_8)); // inner hash
+
+ // JS の padHex 相当:符号なしの even-length hex 文字列を作成
+ String saltHex = this.salt.toString(16);
+ if (saltHex.length() % 2 != 0)
+ saltHex = "0" + saltHex;
+ if (saltHex.matches("^[89a-fA-F].*"))
+ saltHex = "00" + saltHex;
+ byte[] saltBytes = Helper.hexStringToByteArray(saltHex);
+
+ byte[] combined = new byte[saltBytes.length + userPassHash.length];
+ System.arraycopy(saltBytes, 0, combined, 0, saltBytes.length);
+ System.arraycopy(userPassHash, 0, combined, saltBytes.length, userPassHash.length);
+
+ byte[] xHash = digest.digest(combined); // final hash
+ return new BigInteger(1, xHash);
+ }
+}
diff --git a/src/main/java/city/makeour/moc/auth/srp/SrpAuthenticationHelper.java b/src/main/java/city/makeour/moc/auth/srp/SrpAuthenticationHelper.java
new file mode 100644
index 0000000..8826bff
--- /dev/null
+++ b/src/main/java/city/makeour/moc/auth/srp/SrpAuthenticationHelper.java
@@ -0,0 +1,91 @@
+package city.makeour.moc.auth.srp;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Base64;
+import java.util.Date;
+import java.util.TimeZone;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class SrpAuthenticationHelper {
+
+ private final SmallA a;
+ private final LargeA A;
+ private final LargeN N;
+ private final SmallG g = new SmallG();
+ private final SmallK k;
+ private final String userPoolName;;
+
+ public SrpAuthenticationHelper(String userPoolId) {
+ this.N = new LargeN();
+ this.a = new SmallA(N);
+ this.A = new LargeA(N, this.a, this.g);
+ this.k = new SmallK(N, g);
+ this.userPoolName = userPoolId.split("_")[1];
+ }
+
+ public String getA() {
+ return this.A.value().toString(16);
+ }
+
+ public String getHexA() {
+ String hex = A.value().toString(16);
+ return String.format("%0256x", new BigInteger(hex, 16)); // 256桁になるように0埋め
+ }
+
+ public byte[] getPasswordAuthenticationKey(
+ String userIdForSrp,
+ String password,
+ String srpBHex,
+ String saltHex,
+ String secretBlock) throws InvalidKeyException, NoSuchAlgorithmException {
+
+ LargeB B = new LargeB(srpBHex);
+ if (B.value().mod(this.N.value()).equals(BigInteger.ZERO)) {
+ throw new IllegalStateException("Invalid server B value");
+ }
+ SmallU u = new SmallU(A, B);
+ BigInteger salt = new BigInteger(1, Helper.hexStringToByteArray(saltHex));
+ SmallX x = new SmallX(this.userPoolName, userIdForSrp, password, salt);
+ LargeS S = new LargeS(B, this.k, this.g, x, this.a, u, this.N);
+ HKDF hkdf = new HKDF(S, u);
+
+ return hkdf.value();
+ }
+
+ public String calculateSignature(String userIdForSRP, String secretBlock, String timestamp, byte[] key)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(key, "HmacSHA256"));
+
+ byte[] poolNameBytes = this.userPoolName.getBytes(StandardCharsets.UTF_8);
+ byte[] userIdBytes = userIdForSRP.getBytes(StandardCharsets.UTF_8);
+ byte[] secretBlockBytes = Base64.getDecoder().decode(secretBlock);
+ byte[] timestampBytes = timestamp.getBytes(StandardCharsets.UTF_8);
+
+ byte[] message = new byte[poolNameBytes.length + userIdBytes.length + secretBlockBytes.length
+ + timestampBytes.length];
+ int pos = 0;
+ System.arraycopy(poolNameBytes, 0, message, pos, poolNameBytes.length);
+ pos += poolNameBytes.length;
+ System.arraycopy(userIdBytes, 0, message, pos, userIdBytes.length);
+ pos += userIdBytes.length;
+ System.arraycopy(secretBlockBytes, 0, message, pos, secretBlockBytes.length);
+ pos += secretBlockBytes.length;
+ System.arraycopy(timestampBytes, 0, message, pos, timestampBytes.length);
+
+ byte[] rawSignature = mac.doFinal(message);
+ return Base64.getEncoder().encodeToString(rawSignature);
+ }
+
+ public String getCurrentFormattedTimestamp() {
+ SimpleDateFormat format = new SimpleDateFormat("EEE MMM d HH:mm:ss z yyyy", java.util.Locale.US);
+ format.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return format.format(new Date());
+ }
+}
diff --git a/src/test/java/city/makeour/moc/FetchCognitoTokenTest.java b/src/test/java/city/makeour/moc/FetchCognitoTokenTest.java
new file mode 100644
index 0000000..b24fc0f
--- /dev/null
+++ b/src/test/java/city/makeour/moc/FetchCognitoTokenTest.java
@@ -0,0 +1,28 @@
+package city.makeour.moc;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+class FetchCognitoTokenTest {
+
+ // TODO: テストのスキップについてきちんと動作確認をする。
+ @Test
+ @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_USER_POOL_ID", matches = ".+")
+ @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_CLIENT_ID", matches = ".+")
+ @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_USERNAME", matches = ".+")
+ @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_PASSWORD", matches = ".+")
+ public void testFetchTokenWithSrpAuth() throws Exception {
+ String cognitoUserPoolId = System.getenv("TEST_COGNITO_USER_POOL_ID");
+ String cognitoClientId = System.getenv("TEST_COGNITO_CLIENT_ID");
+ String username = System.getenv("TEST_COGNITO_USERNAME");
+ String password = System.getenv("TEST_COGNITO_PASSWORD");
+
+ FetchCognitoToken fetchCognitoToken = new FetchCognitoToken(cognitoUserPoolId, cognitoClientId);
+ fetchCognitoToken.setAuthParameters(username, password);
+ Token token = fetchCognitoToken.fetchTokenWithSrpAuth();
+ assertNotNull(token.getIdToken());
+ assertNotNull(token.getRefreshToken());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/city/makeour/moc/MocClientTest.java b/src/test/java/city/makeour/moc/MocClientTest.java
new file mode 100644
index 0000000..7dc17e8
--- /dev/null
+++ b/src/test/java/city/makeour/moc/MocClientTest.java
@@ -0,0 +1,55 @@
+package city.makeour.moc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.List;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import city.makeour.ngsi.v2.api.EntitiesApi;
+import city.makeour.ngsi.v2.model.ListEntitiesResponse;
+
+class MocClientTest {
+
+ @Test
+ @DisplayName("デフォルトコンストラクタで正しいベースパスが設定されることを確認")
+ void defaultConstructorShouldSetCorrectBasePath() {
+ MocClient client = new MocClient();
+ assertEquals("https://orion.sandbox.makeour.city", client.apiClient.getBasePath());
+ }
+
+ @Test
+ @DisplayName("カスタムベースパスが正しく設定されることを確認")
+ void constructorWithBasePathShouldSetCustomBasePath() {
+ String customBasePath = "https://custom.orion.example.com";
+ MocClient client = new MocClient(customBasePath);
+ assertEquals(customBasePath, client.apiClient.getBasePath());
+ }
+
+ @Test
+ @DisplayName("entities()メソッドが正しいEntitiesApiインスタンスを返すことを確認")
+ void entitiesMethodShouldReturnEntitiesApiInstance() {
+ MocClient client = new MocClient();
+ EntitiesApi entitiesApi = client.entities();
+
+ assertNotNull(entitiesApi);
+ assertEquals(client.entitiesApi, entitiesApi);
+ }
+
+ @Test
+ @DisplayName("entities apiで、entityの一覧を取得するテスト")
+ void testEntitiesApi() {
+ MocClient client = new MocClient();
+ EntitiesApi entitiesApi = client.entities();
+
+ List list = entitiesApi.listEntities(null, null, null, null, null, null, null, null, null,
+ null, null, null,
+ null,
+ null,
+ null);
+ assertNotNull(list);
+ System.out.println("Entities: " + list);
+ }
+}
\ No newline at end of file