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 + +[![Java CI with Maven](https://github.com/makeOurCity/moc-java/actions/workflows/test.yml/badge.svg)](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