From 66eb29bdd877fbae3252cd0e88d3a970703d3afb Mon Sep 17 00:00:00 2001 From: NetRiceCake Date: Sun, 23 Nov 2025 00:04:49 +0900 Subject: [PATCH] Initial commit. --- .gitignore | 38 +++ .idea/.gitignore | 8 + .idea/encodings.xml | 7 + .idea/misc.xml | 14 ++ .idea/vcs.xml | 6 + README.md | 5 + dependency-reduced-pom.xml | 56 +++++ pom.xml | 91 ++++++++ .../java/com/github/netricecake/KakaoApi.java | 218 ++++++++++++++++++ .../github/netricecake/KakaoTalkClient.java | 89 +++++++ .../java/com/github/netricecake/Main.java | 22 ++ .../netricecake/crypto/CryptoManager.java | 82 +++++++ .../netricecake/kakao/KakaoDefaultValues.java | 9 + .../netricecake/message/LocoRequest.java | 9 + .../netricecake/message/LocoResponse.java | 9 + .../message/request/CheckInRequest.java | 44 ++++ .../message/request/GetConfRequest.java | 34 +++ .../message/request/LoginListRequest.java | 79 +++++++ .../message/request/MessageRequest.java | 16 ++ .../message/request/PingRequest.java | 18 ++ .../message/request/SetStatusRequest.java | 34 +++ .../message/request/WriteRequest.java | 42 ++++ .../message/response/CheckInResponse.java | 54 +++++ .../message/response/GetConfResponse.java | 26 +++ .../message/response/LoginListResponse.java | 25 ++ .../message/response/MessageResponse.java | 30 +++ .../message/response/PingResponse.java | 23 ++ .../message/response/SetStatusResponse.java | 24 ++ .../netricecake/network/LocoPacket.java | 40 ++++ .../network/LocoPacketHandler.java | 6 + .../netricecake/network/LocoSocket.java | 92 ++++++++ .../netricecake/network/codec/LocoCodec.java | 64 +++++ .../network/codec/SecureLayerCodec.java | 45 ++++ .../com/github/netricecake/util/BsonUtil.java | 42 ++++ .../com/github/netricecake/util/ByteUtil.java | 74 ++++++ src/main/resources/META-INF/MANIFEST.MF | 3 + test.png | Bin 0 -> 3873 bytes 37 files changed, 1478 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 dependency-reduced-pom.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/github/netricecake/KakaoApi.java create mode 100644 src/main/java/com/github/netricecake/KakaoTalkClient.java create mode 100644 src/main/java/com/github/netricecake/Main.java create mode 100644 src/main/java/com/github/netricecake/crypto/CryptoManager.java create mode 100644 src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java create mode 100644 src/main/java/com/github/netricecake/message/LocoRequest.java create mode 100644 src/main/java/com/github/netricecake/message/LocoResponse.java create mode 100644 src/main/java/com/github/netricecake/message/request/CheckInRequest.java create mode 100644 src/main/java/com/github/netricecake/message/request/GetConfRequest.java create mode 100644 src/main/java/com/github/netricecake/message/request/LoginListRequest.java create mode 100644 src/main/java/com/github/netricecake/message/request/MessageRequest.java create mode 100644 src/main/java/com/github/netricecake/message/request/PingRequest.java create mode 100644 src/main/java/com/github/netricecake/message/request/SetStatusRequest.java create mode 100644 src/main/java/com/github/netricecake/message/request/WriteRequest.java create mode 100644 src/main/java/com/github/netricecake/message/response/CheckInResponse.java create mode 100644 src/main/java/com/github/netricecake/message/response/GetConfResponse.java create mode 100644 src/main/java/com/github/netricecake/message/response/LoginListResponse.java create mode 100644 src/main/java/com/github/netricecake/message/response/MessageResponse.java create mode 100644 src/main/java/com/github/netricecake/message/response/PingResponse.java create mode 100644 src/main/java/com/github/netricecake/message/response/SetStatusResponse.java create mode 100644 src/main/java/com/github/netricecake/network/LocoPacket.java create mode 100644 src/main/java/com/github/netricecake/network/LocoPacketHandler.java create mode 100644 src/main/java/com/github/netricecake/network/LocoSocket.java create mode 100644 src/main/java/com/github/netricecake/network/codec/LocoCodec.java create mode 100644 src/main/java/com/github/netricecake/network/codec/SecureLayerCodec.java create mode 100644 src/main/java/com/github/netricecake/util/BsonUtil.java create mode 100644 src/main/java/com/github/netricecake/util/ByteUtil.java create mode 100644 src/main/resources/META-INF/MANIFEST.MF create mode 100644 test.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af665ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..c3f502a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 디폴트 무시된 파일 +/shelf/ +/workspace.xml +# 에디터 기반 HTTP 클라이언트 요청 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d61b968 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..97f1bc8 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Loco wrapper + +테스트 해볼거면 본계정 쓰지마세요. 영정먹을 수 있음 + +![test](./test.png) diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..16def58 --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + com.github.netricecake + loco-wrapper + 1.0-SNAPSHOT + + + + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + com.github.netricecake.Main + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + + com.squareup.okhttp3 + okhttp-bom + 5.2.0 + pom + import + + + + + 21 + 21 + UTF-8 + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f13d59e --- /dev/null +++ b/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.github.netricecake + loco-wrapper + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + com.github.netricecake.Main + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + + + com.squareup.okhttp3 + okhttp-bom + 5.2.0 + pom + import + + + + + + + com.squareup.okhttp3 + okhttp-jvm + + + com.google.code.gson + gson + 2.13.2 + + + io.netty + netty-all + 4.2.7.Final + + + org.projectlombok + lombok + 1.18.42 + + + org.mongodb + bson + 5.6.1 + + + + diff --git a/src/main/java/com/github/netricecake/KakaoApi.java b/src/main/java/com/github/netricecake/KakaoApi.java new file mode 100644 index 0000000..f6d088e --- /dev/null +++ b/src/main/java/com/github/netricecake/KakaoApi.java @@ -0,0 +1,218 @@ +package com.github.netricecake; + +import com.github.netricecake.message.request.GetConfRequest; +import com.github.netricecake.message.response.GetConfResponse; +import com.github.netricecake.util.ByteUtil; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import okhttp3.*; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.security.auth.login.AccountException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +public class KakaoApi { + + public final static String ALLOW_LIST_URL = "https://katalk.kakao.com/android/account/allowlist.json"; + public final static String LOGIN_URL = "https://katalk.kakao.com/android/account/login.json"; + public final static String PASSCODE_GENERATE_URL = "https://katalk.kakao.com/android/account/passcodeLogin/generate"; + public final static String REGISTER_DEVICE_URL = "https://katalk.kakao.com/android/account/passcodeLogin/registerDevice"; + public final static String CANCEL_REGISTER_URL = "https://katalk.kakao.com/android/account/passcodeLogin/cancel"; + public final static String BOOKING_URL = "booking-loco.kakao.com"; + public final static int BOOKING_PORT = 443; + + public final static String AGENT = "android"; + public final static String VERSION = "25.9.2"; + public final static String OS_VERSION = "13"; + public final static String API_LEVEL = "33"; + public final static String LANGUAGE = "ko"; + public final static String AUTH_USER_AGENT = String.format("KT/%s An/%s %s", VERSION, OS_VERSION, LANGUAGE); + public final static String AUTH_HEADER_AGENT = String.format("%s/%s/%s", AGENT, VERSION, LANGUAGE); + + public final static int UUID_LENGTH = 64; + + private final static OkHttpClient client = new OkHttpClient();; + + private final static Gson gson = new Gson(); + + public static LoginData loginRequest(String email, String password, String deviceName, String deviceUuid) throws IOException, InvalidParameterException, IllegalStateException { + if (deviceUuid == null || deviceUuid.length() != UUID_LENGTH) throw new InvalidParameterException("invalid deviceUuid"); + if (!checkAllowedDevice(deviceName)) throw new InvalidParameterException("This device does not support sub device login"); + RequestBody body = new FormBody.Builder().add("password", password) + .add("device_name", deviceName) + .add("foced", "false") + .add("permanent", "true") + .add("email", email) + .add("device_uuid", deviceUuid).build(); + Request.Builder builder = new Request.Builder().url(LOGIN_URL).post(body); + builder.addHeader("X-VC", calculateXVC(email)) + .addHeader("Accept-Language", LANGUAGE) + .addHeader("User-Agent", AUTH_USER_AGENT) + .addHeader("A", AUTH_HEADER_AGENT); + + Response response = client.newCall(builder.build()).execute(); + JsonObject jsonObject = new JsonParser().parse(response.body().string()).getAsJsonObject(); + int status = jsonObject.get("status").getAsInt(); + + // 12 비번 틀림 30 이메일 틀림 + if (status == 12 || status == 30) throw new InvalidParameterException("Email or password is invalid"); + if (status == -100) throw new IllegalStateException("Register device before login"); + if (status == 0) { + + LoginData data = new LoginData(); + data.userId = jsonObject.get("userId").getAsInt(); + data.countryIso = jsonObject.get("countryIso").getAsString(); + data.countryCode = jsonObject.get("countryCode").getAsString(); + data.accountId = jsonObject.get("accountId").getAsInt(); + data.accessToken = jsonObject.get("access_token").getAsString(); + data.refreshToken = jsonObject.get("refresh_token").getAsString(); + data.tokenType = jsonObject.get("token_type").getAsString(); + data.autoLoginAccountId = jsonObject.get("autoLoginAccountId").getAsString(); + data.displayAccountId = jsonObject.get("displayAccountId").getAsString(); + data.mainDeviceAgentName = jsonObject.get("mainDeviceAgentName").getAsString(); + data.mainDeviceAppVersion = jsonObject.get("mainDeviceAppVersion").getAsString(); + data.recipe = jsonObject.get("recipe").getAsString(); + + return data; + } + return null; + } + + public static Map.Entry generatePasscode(String email, String password, String device_name, String deviceUuid) throws IOException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("email", email); + jsonObject.addProperty("password", password); + jsonObject.addProperty("permanent", true); + JsonObject deviceObject = new JsonObject(); + deviceObject.addProperty("name", device_name); + deviceObject.addProperty("uuid", deviceUuid); + deviceObject.addProperty("model", device_name); + deviceObject.addProperty("osVersion", API_LEVEL); + jsonObject.add("device", deviceObject); + + Request.Builder builder = generateHeader(email).url(PASSCODE_GENERATE_URL).post(RequestBody.create(gson.toJson(jsonObject), MediaType.parse("application/json; charset=utf-8"))); + String json = client.newCall(builder.build()).execute().body().string(); + JsonObject body = JsonParser.parseString(json).getAsJsonObject(); + return Map.entry(body.get("passcode").getAsString(), body.get("remainingSeconds").getAsInt()); + } + + public static boolean registerDevice(String email, String password, String deviceUuid) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("email", email); + jsonObject.addProperty("password", password); + JsonObject deviceObject = new JsonObject(); + deviceObject.addProperty("uuid", deviceUuid); + jsonObject.add("device", deviceObject); + + Request.Builder builder = generateHeader(email).url(REGISTER_DEVICE_URL).post(RequestBody.create(gson.toJson(jsonObject), MediaType.parse("application/json; charset=utf-8"))); + + Request request = builder.build(); + try { + int remainTime = -7777; + do { + JsonObject resBody = JsonParser.parseString(client.newCall(request).execute().body().string()).getAsJsonObject(); + if (resBody.get("status").getAsInt() == 0) return true; + if (resBody.get("status").getAsInt() != -100) return false; + int remain = resBody.get("remainingSeconds").getAsInt(); + int interval = resBody.get("nextRequestIntervalInSeconds").getAsInt(); + remainTime = remain - interval; + Thread.sleep((long) interval * 1000); + } while(remainTime > 0); + builder = generateHeader(email).url(CANCEL_REGISTER_URL).post(RequestBody.create(gson.toJson(jsonObject), MediaType.parse("application/json; charset=utf-8"))); + client.newCall(builder.build()).execute(); + } catch (Exception e) {} + return false; + } + + public static GetConfResponse getBookingData(String MCCMNC, int userId) { + byte[] id = ByteUtil.intToByteArrayLE(1000); + byte[] method = new byte[11]; + System.arraycopy("GETCONF".getBytes(), 0, method, 0, 7); + byte[] b = new GetConfRequest(MCCMNC, AGENT, userId).toBson(); + byte[] m = ByteUtil.concatBytes(id, new byte[2], method, new byte[1], ByteUtil.intToByteArrayLE(b.length), b); + try { + SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + SSLSocket socket = (SSLSocket) socketFactory.createSocket(BOOKING_URL, BOOKING_PORT); + socket.startHandshake(); + + OutputStream writer = socket.getOutputStream(); + InputStream reader = socket.getInputStream(); + + writer.write(m); + writer.flush(); + + byte[] res = new byte[4096]; + reader.read(res); + + int len = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(res, 18, 4)); + GetConfResponse r = new GetConfResponse(); + r.fromBson(ByteUtil.sliceBytes(res, 22, len)); + + socket.close(); + + return r; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static boolean checkAllowedDevice(String deviceName) throws IOException { + Request.Builder builder = new Request.Builder().url(String.format("%s?model_name=%s", ALLOW_LIST_URL, deviceName)).get(); + builder.addHeader("Content-Type", "application/x-www-form-urlencoded"); + builder.addHeader("Accept-Language", LANGUAGE); + builder.addHeader("User-Agent", AUTH_USER_AGENT); + builder.addHeader("A", AUTH_HEADER_AGENT); + builder.addHeader("Accept-Encoding", "gzip"); + + Request request = builder.build(); + + return JsonParser.parseString(client.newCall(request).execute().body().string()).getAsJsonObject().get("allowlisted").getAsBoolean(); + } + + public static Request.Builder generateHeader(String email) { + Request.Builder builder = new Request.Builder(); + builder.addHeader("X-VC", calculateXVC(email)) + .addHeader("User-Agent", AUTH_USER_AGENT) + .addHeader("A", AUTH_HEADER_AGENT); + + return builder; + } + + public static String calculateXVC(String email) { + try { + String str = String.format("BARD|%s|DANTE|%s|SIAN", AUTH_USER_AGENT, email); + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + digest.reset(); + digest.update(str.getBytes()); + + return ByteUtil.byteArrayToHexString(digest.digest()).substring(0, 16).toLowerCase(); + } catch (NoSuchAlgorithmException e) {} + return ""; + } + + public static class LoginData { + public int userId; + public String countryIso; + public String countryCode; + public int accountId; + public String accessToken; + public String refreshToken; + public String tokenType; + public String autoLoginAccountId; + public String displayAccountId; + public String mainDeviceAgentName; + public String mainDeviceAppVersion; + public String recipe; + } + +} diff --git a/src/main/java/com/github/netricecake/KakaoTalkClient.java b/src/main/java/com/github/netricecake/KakaoTalkClient.java new file mode 100644 index 0000000..d9d33b1 --- /dev/null +++ b/src/main/java/com/github/netricecake/KakaoTalkClient.java @@ -0,0 +1,89 @@ +package com.github.netricecake; + +import com.github.netricecake.kakao.KakaoDefaultValues; +import com.github.netricecake.message.request.*; +import com.github.netricecake.message.response.CheckInResponse; +import com.github.netricecake.message.response.GetConfResponse; +import com.github.netricecake.message.response.MessageResponse; +import com.github.netricecake.network.LocoPacket; +import com.github.netricecake.network.LocoSocket; +import com.github.netricecake.util.BsonUtil; + +import java.io.IOException; +import java.security.InvalidParameterException; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class KakaoTalkClient { + + private final static String DEFAULT_MCCMNC = KakaoDefaultValues.MCCMNC; + + private KakaoApi.LoginData loginData; + private GetConfResponse bookingData; + private CheckInResponse checkInData; + + LocoSocket socket; + + public void login(String email, String password, String deviceName, String deviceUuid) throws Exception, IOException, InvalidParameterException, IllegalStateException { + try { + loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid); + } catch (IllegalStateException e) { + Map.Entry registerInfo = KakaoApi.generatePasscode(email, password, deviceName, deviceUuid); + System.out.println("디바이스 등록이 필요합니다."); + System.out.println("카카오톡에서 " + registerInfo.getValue() + "초 안에 코드를 입력해주세요. 코드 : " + registerInfo.getKey()); + boolean registerResult = KakaoApi.registerDevice(email, password, deviceUuid); + if (!registerResult) throw new IllegalStateException("기기 등록 실패"); + System.out.println("기기 등록 성공"); + loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid); + } + System.out.println("로그인 성공"); + + bookingData = KakaoApi.getBookingData(DEFAULT_MCCMNC, loginData.userId); + + LocoSocket checkInSocket = new LocoSocket(bookingData.getAddr(), bookingData.getPort()); + + try { + CheckInRequest checkInRequest = new CheckInRequest(); + checkInRequest.setUserId(loginData.userId); + byte[] body = checkInRequest.toBson(); + checkInSocket.connect(); + checkInSocket.write(new LocoPacket(1001, checkInRequest.getMethod(), body)); + checkInData = new CheckInResponse(); + checkInData.fromBson(checkInSocket.read().getBody()); + checkInSocket.close(); + } catch (Exception e) { + System.out.println("오류 : " + e.getMessage()); + } + + socket = new LocoSocket(checkInData.getHost(), checkInData.getPort()); + socket.connect(); + LoginListRequest req = new LoginListRequest(); + req.setDuuid(deviceUuid); + req.setOauthToken(loginData.accessToken); + socket.write(new LocoPacket(1002, "LOGINLIST", req.toBson())); + } + + public void test() throws Exception { + while(true) { + LocoPacket packet = socket.read(); + if (packet == null) continue; + if (!packet.getMethod().equals("MSG")) continue; + + socket.write(new LocoPacket(packet.getPacketId(), "MSG", new MessageRequest().toBson())); + MessageResponse res = new MessageResponse(); + res.fromBson(packet.getBody()); + if (res.getType() != 1) return; + if (res.getMessage().trim().equals("!!test")) { + WriteRequest req = new WriteRequest(); + req.setChatId(res.getChatId()); + req.setMessage("test!!"); + socket.write(new LocoPacket(1003, "WRITE", req.toBson())); + LocoPacket t = socket.read(); + System.out.println(BsonUtil.bsonToJson(t.getBody())); + System.exit(0); + } + + } + } + +} diff --git a/src/main/java/com/github/netricecake/Main.java b/src/main/java/com/github/netricecake/Main.java new file mode 100644 index 0000000..63b0821 --- /dev/null +++ b/src/main/java/com/github/netricecake/Main.java @@ -0,0 +1,22 @@ +package com.github.netricecake; + +import com.github.netricecake.util.ByteUtil; + +import java.security.SecureRandom; + +public class Main { + + static String EMAIL = ""; + static String PASSWORD = ""; + static String DEVICE_NAME = "SM-X930"; // 갤럭시 탭 s11 울트라 + + public static void main(String[] args) throws Exception { + byte[] uuid = new byte[32]; + new SecureRandom().nextBytes(uuid); + String deviceUuid = ByteUtil.byteArrayToHexString(uuid); + KakaoTalkClient client = new KakaoTalkClient(); + client.login(EMAIL, PASSWORD, DEVICE_NAME, deviceUuid); + client.test(); + } + +} diff --git a/src/main/java/com/github/netricecake/crypto/CryptoManager.java b/src/main/java/com/github/netricecake/crypto/CryptoManager.java new file mode 100644 index 0000000..0254873 --- /dev/null +++ b/src/main/java/com/github/netricecake/crypto/CryptoManager.java @@ -0,0 +1,82 @@ +package com.github.netricecake.crypto; + +import com.github.netricecake.util.ByteUtil; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.GCMParameterSpec; +import java.security.*; +import java.security.spec.X509EncodedKeySpec; + +public class CryptoManager { + + public final static int HANDSHAKE_BODY_SIZE = 256; // ENCRYPTED KEY + + public final static String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + public final static int RSA_LOCO_HEADER = 16; + public final static byte[] RSA_PUBLIC_KEY_BYTES = ByteUtil.hexStringToByteArray("30820120300D06092A864886F70D01010105000382010D00308201080282010100A3B076E8C445851F19A670C231AAC6DB42EFD09717D06048A5CC56906CD1AB27B9DF37FFD5017E7C13A1405B5D1C3E4879A6A499D3C618A72472B0B50CA5EF1EF6EEA70369D9413FE662D8E2B479A9F72142EE70CEE6C2AD12045D52B25C4A204A28968E37F0BA6A49EE3EC9F2AC7A65184160F22F62C43A4067CD8D2A6F13D9B8298AB002763D236C9D1879D7FCE5B8FA910882B21E15247E0D0A24791308E51983614402E9FA03057C57E9E178B1CC39FE67288EFC461945CBCAA11D1FCC123E750B861F0D447EBE3C115F411A42DC95DDB21DA42774A5BCB1DDF7FA5F10628010C74F36F31C40EFCFE289FD81BABA44A6556A6C301210414B6023C3F46371020103"); + + public final static String AES_ALGORITHM = "AES/GCM/NoPadding"; + public final static int AES_KEY_SIZE = 128; + public final static int AES_NONCE_SIZE = 12; + public final static int AES_LOCO_HEADER = 3; + + private Key aesKey; + private final SecureRandom generator = new SecureRandom(); + + public CryptoManager() { + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(AES_KEY_SIZE); + aesKey = keyGenerator.generateKey(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public CryptoManager(Key aesKey) { + this.aesKey = aesKey; + } + + public byte[] generateHandshakeMessage() { + try { + PublicKey rsaPublicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(RSA_PUBLIC_KEY_BYTES)); + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey); + byte[] encryptedKey = cipher.doFinal(aesKey.getEncoded()); + byte[] length = ByteUtil.intToByteArrayLE(HANDSHAKE_BODY_SIZE); + return ByteUtil.concatBytes(length, ByteUtil.intToByteArrayLE(RSA_LOCO_HEADER), ByteUtil.intToByteArrayLE(AES_LOCO_HEADER), encryptedKey); + } catch (Exception e) { + e.printStackTrace(); + } + return new byte[0]; + } + + // 바디 사이즈가 131067가 최대인거 같은데 잘 모르겠음 + public byte[] encryptMessage(byte[] message) { + try { + byte[] nonce = new byte[AES_NONCE_SIZE]; + generator.nextBytes(nonce); + Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(AES_KEY_SIZE, nonce)); + byte[] encryptedBody = cipher.doFinal(message); + return ByteUtil.concatBytes(nonce, encryptedBody); + } catch (Exception e) { + e.printStackTrace(); + } + return new byte[0]; + } + + public byte[] decryptMessage(byte[] message) { + try { + byte[] nonce = ByteUtil.sliceBytes(message, 0, AES_NONCE_SIZE); + Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(AES_KEY_SIZE, nonce)); + return cipher.doFinal(ByteUtil.sliceBytes(message, AES_NONCE_SIZE, message.length - nonce.length)); + } catch (Exception e) { + e.printStackTrace(); + } + return new byte[0]; + } + +} diff --git a/src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java b/src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java new file mode 100644 index 0000000..783d2ea --- /dev/null +++ b/src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java @@ -0,0 +1,9 @@ +package com.github.netricecake.kakao; + +public class KakaoDefaultValues { + + public final static String MCCMNC = "45006"; + public final static int ntype = 0; + public final static String os = "android"; + +} diff --git a/src/main/java/com/github/netricecake/message/LocoRequest.java b/src/main/java/com/github/netricecake/message/LocoRequest.java new file mode 100644 index 0000000..aa715a6 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/LocoRequest.java @@ -0,0 +1,9 @@ +package com.github.netricecake.message; + +public interface LocoRequest { + + String getMethod(); + + byte[] toBson(); + +} diff --git a/src/main/java/com/github/netricecake/message/LocoResponse.java b/src/main/java/com/github/netricecake/message/LocoResponse.java new file mode 100644 index 0000000..1bbdbf0 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/LocoResponse.java @@ -0,0 +1,9 @@ +package com.github.netricecake.message; + +public interface LocoResponse { + + String getMethod(); + + void fromBson(byte[] bson); + +} diff --git a/src/main/java/com/github/netricecake/message/request/CheckInRequest.java b/src/main/java/com/github/netricecake/message/request/CheckInRequest.java new file mode 100644 index 0000000..c1ef6d4 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/request/CheckInRequest.java @@ -0,0 +1,44 @@ +package com.github.netricecake.message.request; + +import com.github.netricecake.KakaoApi; +import com.github.netricecake.kakao.KakaoDefaultValues; +import com.github.netricecake.message.LocoRequest; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CheckInRequest implements LocoRequest { + + private int userId; + + private String os = KakaoDefaultValues.os; + + private int ntype = KakaoDefaultValues.ntype; + + private String appVer = KakaoApi.VERSION; + + private String lang = KakaoApi.LANGUAGE; + + private String MCCMNC = KakaoDefaultValues.MCCMNC; + + @Override + public String getMethod() { + return "CHECKIN"; + } + + @Override + public byte[] toBson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userId", userId); + jsonObject.addProperty("os", os); + jsonObject.addProperty("ntype", ntype); + jsonObject.addProperty("appVer", appVer); + jsonObject.addProperty("lang", lang); + jsonObject.addProperty("MCCMNC", MCCMNC); + + return BsonUtil.jsonObjectToBson(jsonObject); + } +} diff --git a/src/main/java/com/github/netricecake/message/request/GetConfRequest.java b/src/main/java/com/github/netricecake/message/request/GetConfRequest.java new file mode 100644 index 0000000..713290b --- /dev/null +++ b/src/main/java/com/github/netricecake/message/request/GetConfRequest.java @@ -0,0 +1,34 @@ +package com.github.netricecake.message.request; + +import com.github.netricecake.message.LocoRequest; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; + +public class GetConfRequest implements LocoRequest { + + private String MCCMNC; + + private String os; + + private int userId; + + public GetConfRequest(String MCCMNC, String os, int userId) { + this.MCCMNC = MCCMNC; + this.os = os; + this.userId = userId; + } + + @Override + public String getMethod() { + return "GETCONF"; + } + + @Override + public byte[] toBson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("MCCMNC", MCCMNC); + jsonObject.addProperty("os", os); + jsonObject.addProperty("userId", userId); + return BsonUtil.jsonObjectToBson(jsonObject); + } +} diff --git a/src/main/java/com/github/netricecake/message/request/LoginListRequest.java b/src/main/java/com/github/netricecake/message/request/LoginListRequest.java new file mode 100644 index 0000000..b72fa9b --- /dev/null +++ b/src/main/java/com/github/netricecake/message/request/LoginListRequest.java @@ -0,0 +1,79 @@ +package com.github.netricecake.message.request; + +import com.github.netricecake.KakaoApi; +import com.github.netricecake.message.LocoRequest; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginListRequest implements LocoRequest { + + private String appVer = KakaoApi.VERSION; + + private String prtVer = "1"; + + private String os = KakaoApi.AGENT; + + private String lang = "ko"; + + private String duuid; + + private int ntype = 0; // 0 : WIFI, 3: Cellular + + private String MCCMNC = "45006"; // 앞자리 세자리(한국) 450 고정, 뒤에 두자리 SKT: 05 KT: 08 LGU+: 06 ex) 45006 + + private int revision = 0; // TODO 이거뭐임 + + private JsonArray chatIds = new JsonArray(); + + private JsonArray maxIds = new JsonArray(); + + private int lastTokenId = 0; + + private int lbk = 0; // TODO 이거 뭐임 2 + + private JsonObject rp = new JsonObject(); // TODO 이거 뭐임 3 + + private boolean bg = true; // TODO 이거 뭐임 4 + + private String oauthToken; + + public LoginListRequest() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("base64", "AAD//wAA"); + jsonObject.addProperty("subType", "00"); + rp.add("$binary", jsonObject); + } + + @Override + public String getMethod() { + return "LOGINLIST"; + } + + @Override + public byte[] toBson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("appVer", appVer); + jsonObject.addProperty("prtVer", prtVer); + jsonObject.addProperty("os", os); + jsonObject.addProperty("lang", lang); + jsonObject.addProperty("duuid", duuid); + jsonObject.addProperty("ntype", ntype); + jsonObject.addProperty("MCCMNC", MCCMNC); + jsonObject.addProperty("revision", revision); + jsonObject.add("chatIds", chatIds); + jsonObject.add("maxIds", maxIds); + jsonObject.addProperty("lastTokenId", lastTokenId); + jsonObject.addProperty("lbk", lbk); + jsonObject.add("rp", rp); + jsonObject.addProperty("bg", bg); + jsonObject.addProperty("oauthToken", oauthToken); + + return BsonUtil.jsonObjectToBson(jsonObject); + } + +} diff --git a/src/main/java/com/github/netricecake/message/request/MessageRequest.java b/src/main/java/com/github/netricecake/message/request/MessageRequest.java new file mode 100644 index 0000000..756eb92 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/request/MessageRequest.java @@ -0,0 +1,16 @@ +package com.github.netricecake.message.request; + +import com.github.netricecake.message.LocoRequest; +import com.github.netricecake.util.BsonUtil; + +public class MessageRequest implements LocoRequest { + @Override + public String getMethod() { + return "MSG"; + } + + @Override + public byte[] toBson() { + return BsonUtil.jsonToBson("{ notiRead: false }"); + } +} diff --git a/src/main/java/com/github/netricecake/message/request/PingRequest.java b/src/main/java/com/github/netricecake/message/request/PingRequest.java new file mode 100644 index 0000000..1fbe9a0 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/request/PingRequest.java @@ -0,0 +1,18 @@ +package com.github.netricecake.message.request; + +import com.github.netricecake.message.LocoRequest; +import com.github.netricecake.util.BsonUtil; + +public class PingRequest implements LocoRequest { + + + @Override + public String getMethod() { + return "PING"; + } + + @Override + public byte[] toBson() { + return BsonUtil.jsonToBson("{}"); + } +} diff --git a/src/main/java/com/github/netricecake/message/request/SetStatusRequest.java b/src/main/java/com/github/netricecake/message/request/SetStatusRequest.java new file mode 100644 index 0000000..9e78f1b --- /dev/null +++ b/src/main/java/com/github/netricecake/message/request/SetStatusRequest.java @@ -0,0 +1,34 @@ +package com.github.netricecake.message.request; + +import com.github.netricecake.message.LocoRequest; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SetStatusRequest implements LocoRequest { + + /* + 카톡 켜고 있는지 안켜고 있는지 알리는 패킷인듯 + */ + + private int status; // 1 : 본다, 2 : 안본다 + + public SetStatusRequest(int atatus) { + this.status = atatus; + } + + @Override + public String getMethod() { + return "SETST"; + } + + @Override + public byte[] toBson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("st", status); + return BsonUtil.jsonObjectToBson(jsonObject); + } +} diff --git a/src/main/java/com/github/netricecake/message/request/WriteRequest.java b/src/main/java/com/github/netricecake/message/request/WriteRequest.java new file mode 100644 index 0000000..5d9ef27 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/request/WriteRequest.java @@ -0,0 +1,42 @@ +package com.github.netricecake.message.request; + +import com.github.netricecake.message.LocoRequest; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WriteRequest implements LocoRequest { + + private long chatId; + private long msgId; // 이거 뭐임??? + private String message; + private int type = 1; + private boolean noSeen = false; + private String extra = "{}"; + private int scope = 1; + + public WriteRequest() { + msgId = (long) Math.ceil(Math.random() * 99999999); + } + + @Override + public String getMethod() { + return "WRITE"; + } + + @Override + public byte[] toBson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("chatId", chatId); + jsonObject.addProperty("msgId", msgId); + jsonObject.addProperty("msg", message); + jsonObject.addProperty("type", type); + jsonObject.addProperty("noSeen", noSeen); + jsonObject.addProperty("extra", extra); + jsonObject.addProperty("scope", scope); + return BsonUtil.jsonObjectToBson(jsonObject); + } +} diff --git a/src/main/java/com/github/netricecake/message/response/CheckInResponse.java b/src/main/java/com/github/netricecake/message/response/CheckInResponse.java new file mode 100644 index 0000000..eb4a54b --- /dev/null +++ b/src/main/java/com/github/netricecake/message/response/CheckInResponse.java @@ -0,0 +1,54 @@ +package com.github.netricecake.message.response; + +import com.github.netricecake.message.LocoResponse; +import com.github.netricecake.util.BsonUtil; +import com.github.netricecake.util.ByteUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class CheckInResponse implements LocoResponse { + + private String host; + + private String host6; + + private int port; + + private String cshost; + + private String cshost6; + + private int csport; + + private String vshost; + + private String vshost6; + + private int vsport; + + private int cacheExpire; + + private String MCCMNC; + + @Override + public String getMethod() { + return "CHECKIN"; + } + + @Override + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + this.host = jsonObject.get("host").getAsString(); + this.host6 = jsonObject.get("host6").getAsString(); + this.port = jsonObject.get("port").getAsInt(); + this.cshost = jsonObject.get("cshost").getAsString(); + this.cshost6 = jsonObject.get("cshost6").getAsString(); + this.csport = jsonObject.get("csport").getAsInt(); + this.vshost = jsonObject.get("vsshost").getAsString(); + this.vshost6 = jsonObject.get("vsshost6").getAsString(); + this.vsport = jsonObject.get("vssport").getAsInt(); + this.cacheExpire = jsonObject.get("cacheExpire").getAsInt(); + this.MCCMNC = jsonObject.get("MCCMNC").getAsString(); + } +} diff --git a/src/main/java/com/github/netricecake/message/response/GetConfResponse.java b/src/main/java/com/github/netricecake/message/response/GetConfResponse.java new file mode 100644 index 0000000..a6d73fb --- /dev/null +++ b/src/main/java/com/github/netricecake/message/response/GetConfResponse.java @@ -0,0 +1,26 @@ +package com.github.netricecake.message.response; + +import com.github.netricecake.message.LocoResponse; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class GetConfResponse implements LocoResponse { + + private String addr; + + private int port; + + @Override + public String getMethod() { + return "GetConf"; + } + + @Override + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + this.addr = jsonObject.get("ticket").getAsJsonObject().get("lsl").getAsJsonArray().get(0).getAsString(); + this.port = jsonObject.get("wifi").getAsJsonObject().get("ports").getAsJsonArray().get(0).getAsInt(); + } +} diff --git a/src/main/java/com/github/netricecake/message/response/LoginListResponse.java b/src/main/java/com/github/netricecake/message/response/LoginListResponse.java new file mode 100644 index 0000000..3c5fe78 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/response/LoginListResponse.java @@ -0,0 +1,25 @@ +package com.github.netricecake.message.response; + +import com.github.netricecake.message.LocoResponse; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class LoginListResponse implements LocoResponse { + + private int status; + + @Override + public String getMethod() { + return "LOGINLIST"; + } + + @Override + public void fromBson(byte[] bson) { + // 너무 많음;; + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + this.status = jsonObject.get("status").getAsInt(); + } + +} diff --git a/src/main/java/com/github/netricecake/message/response/MessageResponse.java b/src/main/java/com/github/netricecake/message/response/MessageResponse.java new file mode 100644 index 0000000..cb3055f --- /dev/null +++ b/src/main/java/com/github/netricecake/message/response/MessageResponse.java @@ -0,0 +1,30 @@ +package com.github.netricecake.message.response; + +import com.github.netricecake.message.LocoResponse; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class MessageResponse implements LocoResponse { + + private long chatId; + private long logId; + private int type; + private long authorId; + private String message; + + @Override + public String getMethod() { + return "MSG"; + } + + @Override + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + chatId = jsonObject.get("chatId").getAsLong(); + logId = jsonObject.get("logId").getAsLong(); + type = jsonObject.get("chatLog").getAsJsonObject().get("type").getAsInt(); + message = jsonObject.get("chatLog").getAsJsonObject().get("message").getAsString(); + } +} diff --git a/src/main/java/com/github/netricecake/message/response/PingResponse.java b/src/main/java/com/github/netricecake/message/response/PingResponse.java new file mode 100644 index 0000000..9f0e381 --- /dev/null +++ b/src/main/java/com/github/netricecake/message/response/PingResponse.java @@ -0,0 +1,23 @@ +package com.github.netricecake.message.response; + +import com.github.netricecake.message.LocoResponse; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class PingResponse implements LocoResponse { + + private int status; + + @Override + public String getMethod() { + return "PING"; + } + + @Override + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + this.status = jsonObject.get("status").getAsInt(); + } +} diff --git a/src/main/java/com/github/netricecake/message/response/SetStatusResponse.java b/src/main/java/com/github/netricecake/message/response/SetStatusResponse.java new file mode 100644 index 0000000..e0b9f9e --- /dev/null +++ b/src/main/java/com/github/netricecake/message/response/SetStatusResponse.java @@ -0,0 +1,24 @@ +package com.github.netricecake.message.response; + +import com.github.netricecake.message.LocoResponse; +import com.github.netricecake.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class SetStatusResponse implements LocoResponse { + + private int status; + + @Override + public String getMethod() { + return "SETST"; + } + + @Override + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + status = jsonObject.get("status").getAsInt(); + } +} diff --git a/src/main/java/com/github/netricecake/network/LocoPacket.java b/src/main/java/com/github/netricecake/network/LocoPacket.java new file mode 100644 index 0000000..edbff56 --- /dev/null +++ b/src/main/java/com/github/netricecake/network/LocoPacket.java @@ -0,0 +1,40 @@ +package com.github.netricecake.network; + +import lombok.Getter; +import lombok.Setter; + +public class LocoPacket { + + @Getter + private final int packetId; + + @Getter + private final short statusCode; + + @Getter + private String method; + + @Getter + private final byte bodyType; + + @Getter + private int bodyLength; + + @Getter + @Setter + private byte[] body; + + public LocoPacket(int packetId, short statusCode, String method, byte bodyType, int bodyLength, byte[] body) { + this.packetId = packetId; + this.statusCode = statusCode; + this.method = method; + this.bodyType = bodyType; + this.bodyLength = bodyLength; + this.body = body; + } + + public LocoPacket(int packetId, String method, byte[] body) { + this(packetId, (short) 0, method, (byte) 0, body.length, body); + } + +} diff --git a/src/main/java/com/github/netricecake/network/LocoPacketHandler.java b/src/main/java/com/github/netricecake/network/LocoPacketHandler.java new file mode 100644 index 0000000..c4b404f --- /dev/null +++ b/src/main/java/com/github/netricecake/network/LocoPacketHandler.java @@ -0,0 +1,6 @@ +package com.github.netricecake.network; + +@FunctionalInterface +public interface LocoPacketHandler { + void onPacket(LocoPacket packet); +} diff --git a/src/main/java/com/github/netricecake/network/LocoSocket.java b/src/main/java/com/github/netricecake/network/LocoSocket.java new file mode 100644 index 0000000..d2ab52c --- /dev/null +++ b/src/main/java/com/github/netricecake/network/LocoSocket.java @@ -0,0 +1,92 @@ +package com.github.netricecake.network; + +import com.github.netricecake.crypto.CryptoManager; +import com.github.netricecake.network.codec.LocoCodec; +import com.github.netricecake.network.codec.SecureLayerCodec; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.bytes.ByteArrayDecoder; +import io.netty.handler.codec.bytes.ByteArrayEncoder; +import lombok.Getter; + +import java.net.InetSocketAddress; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class LocoSocket { + + @Getter + private String ip; + @Getter + private int port; + + private CryptoManager cryptoManager; + + private Channel channel; + private EventLoopGroup eventLoopGroup; + + @Getter + private boolean alive = false; + + private BlockingQueue locoPacketQueue = new LinkedBlockingQueue<>(); + + public LocoSocket(String ip, int port) { + this.ip = ip; + this.port = port; + cryptoManager = new CryptoManager(); + } + + public void connect() throws Exception { + byte[] handshakePacket = cryptoManager.generateHandshakeMessage(); + eventLoopGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + Bootstrap bootstrap = new Bootstrap(); + bootstrap.remoteAddress(new InetSocketAddress(ip, port)) + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addLast(new ByteArrayEncoder()); + pipeline.addLast(new ByteArrayDecoder()); + } + }); + channel = bootstrap.connect().sync().channel(); + alive = true; + channel.writeAndFlush(handshakePacket).sync(); + channel.pipeline().addLast(new SecureLayerCodec(cryptoManager)); + channel.pipeline().addLast(new LocoCodec(locoPacketQueue)); + new Thread() { + @Override + public void run() { + try { + channel.closeFuture().sync(); + eventLoopGroup.shutdownGracefully(); + locoPacketQueue.offer(null); + alive = false; + } catch (Exception e) {} + } + }.start(); + } + + public void write(LocoPacket packet) { + if (!alive) return; + channel.writeAndFlush(packet); + } + + public LocoPacket read() throws Exception { + if (!alive) return null; + return locoPacketQueue.poll(100000, TimeUnit.SECONDS); + } + + public void close() { + channel.close(); + eventLoopGroup.shutdownGracefully(); + alive = false; + } + +} diff --git a/src/main/java/com/github/netricecake/network/codec/LocoCodec.java b/src/main/java/com/github/netricecake/network/codec/LocoCodec.java new file mode 100644 index 0000000..0d5ea71 --- /dev/null +++ b/src/main/java/com/github/netricecake/network/codec/LocoCodec.java @@ -0,0 +1,64 @@ +package com.github.netricecake.network.codec; + +import com.github.netricecake.network.LocoPacket; +import com.github.netricecake.util.ByteUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; + +import java.util.List; +import java.util.concurrent.BlockingQueue; + +public class LocoCodec extends MessageToMessageCodec { + + private LocoPacket currentLocoPacket = null; + private byte[] buffer = new byte[0]; + + private BlockingQueue locoPacketQueue; + + public LocoCodec(BlockingQueue locoPacketHandler) { + this.locoPacketQueue = locoPacketHandler; + } + + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, LocoPacket packet, List list) throws Exception { + byte[] packetId = ByteUtil.intToByteArrayLE(packet.getPacketId()); + byte[] statusCode = ByteUtil.shortToByteArrayLE(packet.getStatusCode()); + byte[] method = new byte[11]; + System.arraycopy(packet.getMethod().getBytes(), 0, method, 0, packet.getMethod().length()); + byte[] bodyType = { packet.getBodyType() }; + byte[] body = packet.getBody(); + byte[] bodyLength = ByteUtil.intToByteArrayLE(body.length); + list.add(ByteUtil.concatBytes(packetId, statusCode, method, bodyType, bodyLength, body)); + } + + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, byte[] bytes, List list) throws Exception { + if (bytes == null) return; + buffer = ByteUtil.concatBytes(buffer, bytes); + do { + if (currentLocoPacket == null) { + if (buffer.length < 22) return; + int id = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(buffer, 0, 4)); + short statusCode = ByteUtil.byteArrayToShortLE(ByteUtil.sliceBytes(buffer, 4, 2)); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 11; i++) { + if ((buffer[6 + i] & 0xFF) == 0) break; + sb.append((char) buffer[6 + i]); + } + String method = sb.toString(); + byte bodyType = buffer[17]; + int bodyLength = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(buffer, 18, 4)); + currentLocoPacket = new LocoPacket(id, statusCode, method, bodyType, bodyLength, null); + buffer = ByteUtil.sliceBytes(buffer, 22, buffer.length - 22); + } + if (currentLocoPacket.getBodyLength() > buffer.length) break; + byte[] body = ByteUtil.sliceBytes(buffer, 0, currentLocoPacket.getBodyLength()); + buffer = ByteUtil.sliceBytes(buffer, currentLocoPacket.getBodyLength(), buffer.length - currentLocoPacket.getBodyLength()); + currentLocoPacket.setBody(body); + locoPacketQueue.put(currentLocoPacket); + currentLocoPacket = null; + } while (buffer.length > 0); + + } + +} diff --git a/src/main/java/com/github/netricecake/network/codec/SecureLayerCodec.java b/src/main/java/com/github/netricecake/network/codec/SecureLayerCodec.java new file mode 100644 index 0000000..df5a288 --- /dev/null +++ b/src/main/java/com/github/netricecake/network/codec/SecureLayerCodec.java @@ -0,0 +1,45 @@ +package com.github.netricecake.network.codec; + +import com.github.netricecake.crypto.CryptoManager; +import com.github.netricecake.util.ByteUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; + +import java.util.List; + +public class SecureLayerCodec extends MessageToMessageCodec { + + private CryptoManager cryptoManager; + + private int currentLength = -1; + private byte[] buffer = new byte[0]; + + public SecureLayerCodec(CryptoManager cryptoManager) { + this.cryptoManager = cryptoManager; + } + + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, byte[] bytes, List list) throws Exception { + byte[] encryptedBody = cryptoManager.encryptMessage(bytes); + byte[] packet = ByteUtil.concatBytes(ByteUtil.intToByteArrayLE(encryptedBody.length), encryptedBody); + list.add(packet); + } + + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, byte[] bytes, List list) throws Exception { + buffer = ByteUtil.concatBytes(buffer, bytes); + do { + if (currentLength == -1) { + if (buffer.length < 4) break; + currentLength = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(buffer, 0, 4)); + buffer = ByteUtil.sliceBytes(buffer, 4, buffer.length - 4); + } + if (currentLength > buffer.length) break; + byte[] packet = ByteUtil.sliceBytes(buffer, 0, currentLength); + buffer = ByteUtil.sliceBytes(buffer, currentLength, buffer.length - currentLength); + currentLength = -1; + list.add(cryptoManager.decryptMessage(packet)); + } while (buffer.length > 0); + } + +} diff --git a/src/main/java/com/github/netricecake/util/BsonUtil.java b/src/main/java/com/github/netricecake/util/BsonUtil.java new file mode 100644 index 0000000..4dd064d --- /dev/null +++ b/src/main/java/com/github/netricecake/util/BsonUtil.java @@ -0,0 +1,42 @@ +package com.github.netricecake.util; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bson.BsonBinaryReader; +import org.bson.RawBsonDocument; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.DecoderContext; + +import java.nio.ByteBuffer; + +public class BsonUtil { + + private final static BsonDocumentCodec bsonDocumentCodec = new BsonDocumentCodec(); + private final static Gson gson = new Gson(); + + public static byte[] jsonToBson(String json) { + var rawBson = RawBsonDocument.parse(json); + ByteBuffer buffer = rawBson.getByteBuffer().asNIO(); + byte[] exactBytes = new byte[buffer.remaining()]; + buffer.get(exactBytes); + return exactBytes; + } + + public static byte[] jsonObjectToBson(JsonObject jsonObject) { + return jsonToBson(gson.toJson(jsonObject)); + } + + public static String bsonToJson(byte[] bson) { + var doc = bsonDocumentCodec.decode( + new BsonBinaryReader(ByteBuffer.wrap(bson)), + DecoderContext.builder().build() + ); + return doc.toString(); + } + + public static JsonObject bsonToJsonObject(byte[] bson) { + return JsonParser.parseString(bsonToJson(bson)).getAsJsonObject(); + } + +} diff --git a/src/main/java/com/github/netricecake/util/ByteUtil.java b/src/main/java/com/github/netricecake/util/ByteUtil.java new file mode 100644 index 0000000..ae4ea15 --- /dev/null +++ b/src/main/java/com/github/netricecake/util/ByteUtil.java @@ -0,0 +1,74 @@ +package com.github.netricecake.util; + +public class ByteUtil { + + public static byte[] intToByteArrayLE(int value) { + byte[] byteArray = new byte[4]; + byteArray[0] = (byte)(value); + byteArray[1] = (byte)(value >> 8); + byteArray[2] = (byte)(value >> 16); + byteArray[3] = (byte)(value >> 24); + return byteArray; + } + + public static int byteArrayToIntLE(byte[] bytes) { + return ((bytes[0] & 0xFF)) | + ((bytes[1] & 0xFF) << 8) | + ((bytes[2] & 0xFF) << 16) | + ((bytes[3] & 0xFF) << 24); + } + + public static byte[] shortToByteArrayLE(short value) { + byte[] byteArray = new byte[2]; + byteArray[0] = (byte)(value); + byteArray[1] = (byte)(value >> 8); + return byteArray; + } + + public static short byteArrayToShortLE(byte[] bytes) { + return (short) ((bytes[0] & 0xFF) | + ((bytes[1] & 0xFF) << 8)); + } + + 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 byteArrayToHexString(byte[] bytes){ + StringBuilder sb = new StringBuilder(); + for(byte b : bytes){ + sb.append(String.format("%02X", b&0xff)); + } + + return sb.toString(); + } + + public static byte[] concatBytes(byte[]... bytes) { + int len = 0; + for (byte[] i: bytes) len += i.length; + + byte[] r = new byte[len]; + int c = 0; + for (byte[] i: bytes) { + System.arraycopy(i, 0, r, c, i.length); + c += i.length; + } + + return r; + } + + public static byte[] sliceBytes(byte[] bytes, int offset, int length) { + byte[] r = new byte[length]; + System.arraycopy(bytes, offset, r, 0, length); + return r; + } + +} \ No newline at end of file diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..f7fb8a9 --- /dev/null +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: com.github.netricecake.Main + diff --git a/test.png b/test.png new file mode 100644 index 0000000000000000000000000000000000000000..bd33f94ad633fb6121ff05c1c19f9198d34a7b96 GIT binary patch literal 3873 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 literal 0 HcmV?d00001