├── README.md
├── SendMsg
├── README
├── SendMsg.dex
└── SendMsg.java
├── SendMsgDB
├── README.MD
├── SendMsgDB.dex
├── SendMsgDB.java
└── config.json
├── app.py
├── chatbot.service
├── chatbot
└── Response.py
├── config.json
├── dbobserver.service
├── guess_user_id.py
├── helper
├── KakaoDB.py
├── KakaoDecrypt.py
├── ObserverHelper.py
├── Replier.py
└── SharedDict.py
├── kill_sendmsg.sh
├── observer.py
├── requirements.txt
├── setup1.sh
└── setup2.sh
/README.md:
--------------------------------------------------------------------------------
1 | # PyKakaoDBBot
2 | ### DEPRECATED : use Iris ( https://iris.qwer.party )
3 |
4 | Redroid 및 노티 기반 봇앱을 이용한 파이썬 DB 봇
5 |
6 | ### 흐름도
7 | ```mermaid
8 | sequenceDiagram
9 | box kakaotalk
10 | participant Kakaotalk
11 | end
12 | box redroid(android)
13 | participant SendMsg
14 | participant DB
15 | end
16 | box PyKakaoDBBot(linux)
17 | participant DBObserver
18 | participant Flask
19 | end
20 | DB->>DBObserver: detect changes
21 | DBObserver->>Flask: send commands
22 | Flask->>SendMsg:send result via socket
23 | SendMsg->>Kakaotalk:reply
24 | ```
25 |
26 | ## 1. Installation
27 | ### 1.1 Clone repository
28 | - root가 아닌 user계정으로 진행합니다.
29 | ```shell
30 | cd ~
31 | git clone https://github.com/dolidolih/PyKakaoDBBot.git
32 | cd PyKakaoDBBot
33 | ```
34 |
35 | ### 1.2 Ubuntu 24.04.1 버전 x86_64 환경 자동화(다른 OS의 경우 1.3부터 진행해주세요.)
36 | - 쉘 스크립트 실행
37 | ```shell
38 | chmod +x *.sh
39 | ./setup1.sh
40 | ```
41 |
42 | - 카카오톡 설치
43 | setup1.sh 실행 후 리드로이드가 설치되었다는 메세지가 나타났다면,
44 | 동일 네트워크의 다른 기기에서 adb, scrcpy를 통해 접속할 수 있습니다.
45 |
46 | Redroid 내부에 카카오톡 설치 후 로그인 한 후 오픈채팅방, 일반채팅방에 메세지를 5~10개 이상 적어주세요.
47 |
48 | - 두번째 쉘 스크립트 실행
49 | ```shell
50 | ./setup2.sh
51 | ```
52 |
53 | 실행 완료 후 두개의 서비스가 실행됩니다.
54 | ```shell
55 | sudo systemctl status chatbot
56 | sudo systemctl status dbobserver
57 | ```
58 |
59 | 코드 수정후에는 chatbot을 restart 해주면 새로운 코드가 적용됩니다. (sudo systemctl restart chatbot)
60 | 모두 완료되었다면 아래 단계들은 skip해도 됩니다.
61 |
62 | ### 1.3 Docker 설치
63 | Docker의 공식 설치 가이드에 따라 설치하세요:
64 | https://docs.docker.com/engine/install/
65 |
66 | ### 1.4 Redroid 설치 및 실행
67 | - docker container 실행
68 | ```shell
69 | sudo docker run -itd --privileged --name redroid\
70 | -v ~/data:/data \
71 | -p 5555:5555 \
72 | -p 3000:3000 \
73 | redroid/redroid:11.0.0-latest \
74 | ro.product.model=SM-T970 \
75 | ro.product.brand=Samsung
76 | ```
77 | - adb, scrcpy, bot app, kakaotalk 설치
78 | ```shell
79 | sudo apt install android-sdk-platform-tools scrcpy
80 | adb connect localhost:5555
81 | adb install YOUR_APP.apk
82 | scrcpy -s localhost:5555
83 | ```
84 |
85 | ### 1.5 Config 설정
86 | - config.json 생성하여 아래와 같이 설정합니다.
87 | ```javascript
88 | # config.json
89 | {
90 | "bot_name" : "YOUR_BOT_NAME", // 봇 이름
91 | "bot_id" : YOUR_BOT_ID, // 봇 ID
92 | "db_path" : "/home/YOUR_LINUX_USERNAME/data/data/com.kakao.talk/databases", // 리눅스 username 반영
93 | "bot_ip" : "127.0.0.1", // 그대로 두세요
94 | "bot_socket_port" : 3000 // 그대로 두세요
95 | }
96 | ```
97 | ※ BOT_ID(봇 계정의 user_id)는 아래 스크립트를 이용하여 유추할 수 있습니다. (일반적으로 가장 짧은 데이터):
98 | https://github.com/jiru/kakaodecrypt/blob/master/guess_user_id.py
99 |
100 | ### 1.6 파이썬 Virtual env 설정 및 기본 패키지 설치
101 | ```shell
102 | python3 -m venv venv
103 | source venv/bin/activate
104 | pip install pip -- upgrade
105 | pip install -r requirements.txt
106 | ```
107 | ### 1.7 /data 퍼미션 설정
108 | - 초기 퍼미션 설정
109 | ```shell
110 | sudo chmod -R -c 777 ~/data/data/.
111 | ```
112 | - cron job 설정(정기적 퍼미션 변경)
113 | ```shell
114 | sudo crontab -e
115 |
116 | * * * * * /bin/chmod -R -c 777 /home/YOUR_USER_NAME/data/data/.
117 | ```
118 | ### 1.8 SendMsg 설치
119 | - adb를 이용하여 SendMsg.dex를 안드로이드로 옮깁니다.
120 | ```shell
121 | adb push SendMsg.dex /data/local/tmp/.
122 | ```
123 | ----
124 | ## 2. 사용 방법
125 | ### 2.1 Python script 실행
126 | ```shell
127 | venv/bin/python observer.py &
128 | venv/bin/python venv/bin/gunicorn -b 0.0.0.0:5000 -w 9 app:app &
129 | ```
130 |
131 | - Systemctl을 통한 서비스를 등록하고자 하는 경우, 2개의 .service를 열어 YOUR_PYKAKAODBBOT_HOME을 pykakaodbbot의 디렉토리로 바꿔줍니다.
132 | - 이후 /etc/systemd/system/ 에 2개의 .service 파일을 복사하고,
133 | ```shell
134 | sudo systemctl daemon-reload
135 | sudo systemctl enable --now dbobserver
136 | sudo systemctl enable --now chatbot
137 | ```
138 |
139 | - 서비스 시작 종료는 sudo systemctl start/stop/restart chatbot 등으로 수행하고, 로그는 sudo journalctl -fu chatbot 등으로 확인합니다.
140 |
141 |
142 | ### 2.2 봇 스크립트 수정
143 | - chatbot/Response.py 를 수정하여 봇 스크립트를 작성하고, replier.reply() 메소드를 통해 채팅창에 출력할 수 있습니다.
144 | - 다른 방으로 보내는 경우, replier.send_socket(self,is_success,type,data,room,msg_json) 을 이용할 수 있습니다.
145 |
146 |
147 | ## 3. Trouble shooting
148 | - /dev/binder가 없는 경우
149 | ```shell
150 | https://github.com/remote-android/redroid-doc/tree/master/deploy 의 배포판 별 설치방법에 따라 binder, ashmem 을 설정합니다.
151 | ```
152 | - sudo docker exec -it [container 이름] sh 로 파일시스템 접근은 되나 adb는 안되는 경우
153 | ```shell
154 | docker 실행 시 가장 뒤에 androidboot.redroid_gpu_mode=guest를 추가합니다.
155 | ```
156 | - 생성되었으나 일정 시간 후 container가 죽는 경우(실행 중인 container 확인 : sudo docker ps)
157 | ```shell
158 | CPU 가상화가 가능한 환경인지 확인합니다.
159 | ```
160 | ### End
161 |
--------------------------------------------------------------------------------
/SendMsg/README:
--------------------------------------------------------------------------------
1 | Original SendMsg : https://github.com/ye-seola/gok-db/blob/main/JAVA_SENDMSG/SendMsg.java
2 |
3 | 1. Push SendMsg.dex into /data/local/tmp:
4 | adb push SendMsg.dex /data/local/tmp/.
5 |
6 | 2. Run below command on host linux environment:
7 | adb shell "su root sh -c 'CLASSPATH=/data/local/tmp/SendMsg.dex app_process / SendMsg'"
8 |
9 |
10 |
11 | Tips,
12 | 1) You don't have to push everytime. Once it has been copied, just run the second command.
13 | 2) You can add the adb command in the dbobserver. Use python adb module or subprocess to run it inside python code.
14 | 3) SendMsg is using android-30.jar from https://github.com/anggrayudi/android-hidden-api
15 | 4) To compile SendMsg.java, download android-30.jar and android build-tools.
16 | - java to class : javac SendMsg.java -cp android-30.jar
17 | - class to dex : BUILD_TOOLS_DIRECTORY/d8 *.class
18 | - rename(optional) : mv classes.dex SendMsg.dex
19 | 5) Kill messangerbot/starlight's socket listener before running SendMsg since it is using the same port number.
20 | 6) To kill SendMsg process, run ps aux | grep SendMsg, and get the PID.
21 |
--------------------------------------------------------------------------------
/SendMsg/SendMsg.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolidolih/PyKakaoDBBot/0225b21e064f60586ee702371552da3e4fbcd57c/SendMsg/SendMsg.dex
--------------------------------------------------------------------------------
/SendMsg/SendMsg.java:
--------------------------------------------------------------------------------
1 | import android.os.IBinder;
2 | import android.os.ServiceManager;
3 | import android.app.IActivityManager;
4 | import android.content.Intent;
5 | import android.content.ComponentName;
6 | import android.app.RemoteInput;
7 | import android.os.Bundle;
8 | import org.json.JSONObject;
9 | import java.util.HashMap;
10 | import java.util.Map;
11 | import java.net.ServerSocket;
12 | import java.net.Socket;
13 | import java.io.BufferedReader;
14 | import java.io.InputStreamReader;
15 | import java.io.PrintWriter;
16 | import java.io.IOException;
17 | import android.util.Base64;
18 | import java.io.File;
19 | import java.io.FileReader;
20 | import java.io.FileOutputStream;
21 | import android.net.Uri;
22 |
23 | class SendMsg {
24 | private static IBinder binder = ServiceManager.getService("activity");
25 | private static IActivityManager activityManager = IActivityManager.Stub.asInterface(binder);
26 | private static final int PORT = 3000;
27 | private static final String NOTI_REF;
28 |
29 | static {
30 | String notiRefValue = null;
31 | File prefsFile = new File("/data/data/com.kakao.talk/shared_prefs/KakaoTalk.hw.perferences.xml");
32 | BufferedReader prefsReader = null;
33 | try {
34 | prefsReader = new BufferedReader(new FileReader(prefsFile));
35 | String line;
36 | while ((line = prefsReader.readLine()) != null) {
37 | if (line.contains("")) {
38 | int start = line.indexOf(">") + 1;
39 | int end = line.indexOf("");
40 | notiRefValue = line.substring(start, end);
41 | break;
42 | }
43 | }
44 | } catch (IOException e) {
45 | System.err.println("Error reading preferences file: " + e.toString());
46 | notiRefValue = "default_noti_ref";
47 | } finally {
48 | if (prefsReader != null) {
49 | try {
50 | prefsReader.close();
51 | } catch (IOException e) {
52 | System.err.println("Error closing preferences file reader: " + e.toString());
53 | }
54 | }
55 | }
56 |
57 | if (notiRefValue == null || notiRefValue.equals("default_noti_ref")) {
58 | System.err.println("NotificationReferer not found in preferences file or error occurred, using default or potentially failed to load.");
59 | } else {
60 | System.out.println("NotificationReferer loaded: " + notiRefValue);
61 | }
62 | NOTI_REF = (notiRefValue != null) ? notiRefValue : "default_noti_ref";
63 | }
64 |
65 |
66 | public static void main(String[] args) {
67 | ServerSocket serverSocket = null;
68 |
69 | try {
70 | serverSocket = new ServerSocket(PORT);
71 | System.out.println("Server listening on port " + PORT);
72 |
73 | while (true) {
74 | Socket clientSocket = null;
75 | BufferedReader in = null;
76 | PrintWriter out = null;
77 | try {
78 | clientSocket = serverSocket.accept();
79 | System.out.println("Client connected: " + clientSocket.getInetAddress());
80 |
81 | in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
82 | out = new PrintWriter(clientSocket.getOutputStream(), true);
83 |
84 | String line;
85 | while ((line = in.readLine()) != null) {
86 | try {
87 | long start = System.currentTimeMillis();
88 | System.out.println("Message received: " + line);
89 | JSONObject obj = new JSONObject(line);
90 | String type = obj.optString("type");
91 | String encodedRoom = obj.getString("room");
92 | String decodedRoom = new String(Base64.decode(encodedRoom, Base64.DEFAULT));
93 | Long chatId = Long.parseLong(decodedRoom);
94 | String encodedMsg = obj.getString("data");
95 | String decodedMsg = new String(Base64.decode(encodedMsg, Base64.DEFAULT));
96 | if ("image".equals(type)) {
97 | SendPhoto(decodedRoom, decodedMsg);
98 | } else {
99 | SendMessage(NOTI_REF, chatId, decodedMsg);
100 | }
101 |
102 | long end = System.currentTimeMillis();
103 |
104 | Map mmap = new HashMap();
105 | mmap.put("success", true);
106 | mmap.put("time", end - start);
107 | String successJson = new JSONObject(mmap).toString();
108 |
109 | out.println(successJson);
110 | } catch (Exception e) {
111 | Map map = new HashMap();
112 | map.put("success", false);
113 | map.put("error", e.toString());
114 |
115 | out.println(new JSONObject(map).toString());
116 | }
117 | }
118 | } catch (IOException e) {
119 | System.err.println("IO Exception in client connection: " + e.toString());
120 | } finally {
121 | try {
122 | if (out != null) {
123 | out.close();
124 | }
125 | if (in != null) {
126 | in.close();
127 | }
128 | if (clientSocket != null) {
129 | clientSocket.close();
130 | }
131 | } catch (IOException e) {
132 | System.err.println("Error closing socket resources: " + e.toString());
133 | }
134 | System.out.println("Client disconnected");
135 | }
136 | }
137 | } catch (IOException e) {
138 | System.err.println("Could not listen on port " + PORT + ": " + e.toString());
139 | System.exit(1);
140 | } finally {
141 | if (serverSocket != null) {
142 | try {
143 | serverSocket.close();
144 | } catch (IOException e) {
145 | System.err.println("Error closing server socket: " + e.toString());
146 | }
147 | }
148 | }
149 | }
150 |
151 | private static void SendMessage(String notiRef, Long chatId, String msg) throws Exception {
152 | Intent intent = new Intent();
153 | intent.setComponent(new ComponentName("com.kakao.talk", "com.kakao.talk.notification.NotificationActionService"));
154 |
155 | intent.putExtra("noti_referer", notiRef);
156 | intent.putExtra("chat_id", chatId);
157 | intent.setAction("com.kakao.talk.notification.REPLY_MESSAGE");
158 |
159 | Bundle results = new Bundle();
160 | results.putCharSequence("reply_message", msg);
161 |
162 | RemoteInput remoteInput = new RemoteInput.Builder("reply_message").build();
163 | RemoteInput[] remoteInputs = new RemoteInput[]{remoteInput};
164 | RemoteInput.addResultsToIntent(remoteInputs, intent, results);
165 | activityManager.startService(
166 | null,
167 | intent,
168 | intent.getType(),
169 | false,
170 | "com.android.shell",
171 | null,
172 | -2
173 | );
174 | }
175 |
176 | private static void SendPhoto(String room, String base64ImageDataString) throws Exception {
177 | byte[] decodedImage = Base64.decode(base64ImageDataString, Base64.DEFAULT);
178 | String timestamp = String.valueOf(System.currentTimeMillis());
179 | File picDir = new File("/sdcard/Android/data/com.kakao.talk/files");
180 | if (!picDir.exists()) {
181 | picDir.mkdirs();
182 | }
183 | File imageFile = new File(picDir, timestamp + ".png");
184 | FileOutputStream fos = null;
185 | try {
186 | fos = new FileOutputStream(imageFile);
187 | fos.write(decodedImage);
188 | fos.flush();
189 | } catch (IOException e) {
190 | System.err.println("Error saving image to file: " + e.toString());
191 | throw e;
192 | } finally {
193 | if (fos != null) {
194 | try {
195 | fos.close();
196 | } catch (IOException e) {
197 | System.err.println("Error closing FileOutputStream: " + e.toString());
198 | }
199 | }
200 | }
201 |
202 | Uri imageUri = Uri.fromFile(imageFile);
203 |
204 | Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
205 | mediaScanIntent.setData(imageUri);
206 | try {
207 | activityManager.broadcastIntent(
208 | null,
209 | mediaScanIntent,
210 | null,
211 | null,
212 | 0,
213 | null,
214 | null,
215 | null,
216 | -1,
217 | null,
218 | false,
219 | false,
220 | -2
221 | );
222 | System.out.println("Media scanner broadcast intent sent for: " + imageUri.toString());
223 | } catch (Exception e) {
224 | System.err.println("Error broadcasting media scanner intent: " + e.toString());
225 | throw e;
226 | }
227 |
228 | Intent intent = new Intent();
229 | intent.setAction(Intent.ACTION_SENDTO);
230 | intent.setType("image/png");
231 | intent.putExtra(Intent.EXTRA_STREAM, imageUri);
232 | intent.putExtra("key_id", Long.parseLong(room));
233 | intent.putExtra("key_type", 1);
234 | intent.putExtra("key_from_direct_share", true);
235 | intent.setPackage("com.kakao.talk");
236 |
237 | try {
238 | activityManager.startActivityAsUserWithFeature(
239 | null,
240 | "com.android.shell",
241 | null,
242 | intent,
243 | intent.getType(),
244 | null, null, 0, 0,
245 | null,
246 | null,
247 | -2
248 | );
249 | } catch (Exception e) {
250 | System.err.println("Error starting activity for sending image: " + e.toString());
251 | throw e;
252 | }
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/SendMsgDB/README.MD:
--------------------------------------------------------------------------------
1 | # SendMsgDB
2 |
3 | ### To run,
4 | ```shell
5 | adb push SendMsgDB.dex /data/local/tmp
6 | sdb push config.json /data/local/tmp
7 | adb shell "su root sh -c 'CLASSPATH=/data/local/tmp/SendMsgDB.dex /system/bin/app_process / SendMsgDB' &"
8 | ```
9 | ### SendMsgDB will send below json msg via x-www-form-urlencoded post request with key named "data" :
10 | ```json
11 | {
12 | "msg" : decryptedMsg,
13 | "room" : nameOfRoom,
14 | "sender" : decryptedSender,
15 | "json" : rowDBRecord
16 | }
17 | ```
18 |
19 | ### SendMsgDB listens with TCP socket port 3000. See Replier.py for json message needed.
20 |
21 | ### You don't need to run dbobserver service if you use this.
22 |
--------------------------------------------------------------------------------
/SendMsgDB/SendMsgDB.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolidolih/PyKakaoDBBot/0225b21e064f60586ee702371552da3e4fbcd57c/SendMsgDB/SendMsgDB.dex
--------------------------------------------------------------------------------
/SendMsgDB/SendMsgDB.java:
--------------------------------------------------------------------------------
1 | //SendMsg : ye-seola/go-kdb
2 | //Kakaodecrypt : jiru/kakaodecrypt
3 |
4 | //WIP: Decrypt all values when sending a db record to a web server.
5 | //WIP: Socket based remote SQL Query
6 |
7 | import android.os.IBinder;
8 | import android.os.ServiceManager;
9 | import android.app.IActivityManager;
10 | import android.content.Intent;
11 | import android.content.ComponentName;
12 | import android.app.RemoteInput;
13 | import android.os.Bundle;
14 | import org.json.JSONObject;
15 | import org.json.JSONException;
16 |
17 | import java.util.HashMap;
18 | import java.util.Map;
19 | import java.util.Locale;
20 | import java.net.ServerSocket;
21 | import java.net.Socket;
22 | import java.net.URLEncoder;
23 | import java.io.BufferedReader;
24 | import java.io.InputStreamReader;
25 | import java.io.PrintWriter;
26 | import java.io.IOException;
27 | import android.util.Base64;
28 | import java.io.File;
29 | import java.io.FileReader;
30 | import java.io.FileOutputStream;
31 | import android.net.Uri;
32 | import java.nio.file.*;
33 | import java.nio.file.attribute.BasicFileAttributes;
34 | import java.nio.charset.StandardCharsets;
35 | import java.security.MessageDigest;
36 | import javax.crypto.Cipher;
37 | import javax.crypto.spec.IvParameterSpec;
38 | import javax.crypto.spec.SecretKeySpec;
39 | import java.util.ArrayList;
40 | import java.util.List;
41 | import android.database.Cursor;
42 | import android.database.sqlite.SQLiteDatabase;
43 | import android.database.sqlite.SQLiteException;
44 | import java.net.HttpURLConnection;
45 | import java.net.URL;
46 | import java.io.OutputStream;
47 |
48 |
49 | class SendMsgDB {
50 | private static IBinder binder = ServiceManager.getService("activity");
51 | private static IActivityManager activityManager = IActivityManager.Stub.asInterface(binder);
52 | private static final int PORT = 3000;
53 | private static final String NOTI_REF;
54 | private static String DB_PATH_CONFIG;
55 | private static String WATCH_FILE;
56 | private static long lastModifiedTime = 0;
57 | private static final String CONFIG_FILE_PATH = "/data/local/tmp/config.json";
58 |
59 | static {
60 | String notiRefValue = null;
61 | File prefsFile = new File("/data/data/com.kakao.talk/shared_prefs/KakaoTalk.hw.perferences.xml");
62 | BufferedReader prefsReader = null;
63 | try {
64 | prefsReader = new BufferedReader(new FileReader(prefsFile));
65 | String line;
66 | while ((line = prefsReader.readLine()) != null) {
67 | if (line.contains("")) {
68 | int start = line.indexOf(">") + 1;
69 | int end = line.indexOf("");
70 | notiRefValue = line.substring(start, end);
71 | break;
72 | }
73 | }
74 | } catch (IOException e) {
75 | System.err.println("Error reading preferences file: " + e.toString());
76 | notiRefValue = "default_noti_ref";
77 | } finally {
78 | if (prefsReader != null) {
79 | try {
80 | prefsReader.close();
81 | } catch (IOException e) {
82 | System.err.println("Error closing preferences file reader: " + e.toString());
83 | }
84 | }
85 | }
86 |
87 | if (notiRefValue == null || notiRefValue.equals("default_noti_ref")) {
88 | System.err.println("NotificationReferer not found in preferences file or error occurred, using default or potentially failed to load.");
89 | } else {
90 | System.out.println("NotificationReferer loaded: " + notiRefValue);
91 | }
92 | NOTI_REF = (notiRefValue != null) ? notiRefValue : "default_noti_ref";
93 |
94 | String dbPathValue = "/data/data/com.kakao.talk/databases";
95 |
96 | DB_PATH_CONFIG = dbPathValue;
97 | WATCH_FILE = DB_PATH_CONFIG + "/KakaoTalk.db-wal";
98 | }
99 |
100 |
101 | public static void main(String[] args) {
102 | ServerSocket serverSocket = null;
103 | SendMsgDB.KakaoDB kakaoDb = new SendMsgDB.KakaoDB();
104 | SendMsgDB.ObserverHelper observerHelper = new SendMsgDB.ObserverHelper();
105 |
106 | checkDbChanges(kakaoDb, observerHelper);
107 |
108 | Thread dbWatcherThread = new Thread(() -> {
109 | while (true) {
110 | checkDbChanges(kakaoDb, observerHelper);
111 | try {
112 | Thread.sleep(100);
113 | } catch (InterruptedException e) {
114 | Thread.currentThread().interrupt();
115 | System.err.println("DB Watcher thread interrupted: " + e.toString());
116 | break;
117 | }
118 | }
119 | });
120 | dbWatcherThread.start();
121 |
122 |
123 | try {
124 | serverSocket = new ServerSocket(PORT);
125 | System.out.println("Server listening on port " + PORT);
126 |
127 | while (true) {
128 | Socket clientSocket = null;
129 | BufferedReader in = null;
130 | PrintWriter out = null;
131 | try {
132 | clientSocket = serverSocket.accept();
133 | System.out.println("Client connected: " + clientSocket.getInetAddress());
134 |
135 | in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
136 | out = new PrintWriter(clientSocket.getOutputStream(), true);
137 |
138 | String line;
139 | while ((line = in.readLine()) != null) {
140 | try {
141 | long start = System.currentTimeMillis();
142 | System.out.println("Message received: " + line);
143 | JSONObject obj = new JSONObject(line);
144 | String type = obj.optString("type");
145 | String encodedRoom = obj.getString("room");
146 | String decodedRoom = new String(android.util.Base64.decode(encodedRoom, android.util.Base64.DEFAULT));
147 | Long chatId = Long.parseLong(decodedRoom);
148 | String encodedMsg = obj.getString("data");
149 | String decodedMsg = new String(android.util.Base64.decode(encodedMsg, android.util.Base64.DEFAULT));
150 | if ("image".equals(type)) {
151 | SendPhoto(decodedRoom, decodedMsg);
152 | } else {
153 | SendMessage(NOTI_REF, chatId, decodedMsg);
154 | }
155 |
156 | long end = System.currentTimeMillis();
157 |
158 | Map mmap = new HashMap();
159 | mmap.put("success", true);
160 | mmap.put("time", end - start);
161 | String successJson = new JSONObject(mmap).toString();
162 |
163 | out.println(successJson);
164 | } catch (Exception e) {
165 | Map map = new HashMap();
166 | map.put("success", false);
167 | map.put("error", e.toString());
168 |
169 | out.println(new JSONObject(map).toString());
170 | }
171 | }
172 | } catch (IOException e) {
173 | System.err.println("IO Exception in client connection: " + e.toString());
174 | } finally {
175 | try {
176 | if (out != null) {
177 | out.close();
178 | }
179 | if (in != null) {
180 | in.close();
181 | }
182 | if (clientSocket != null) {
183 | clientSocket.close();
184 | }
185 | } catch (IOException e) {
186 | System.err.println("Error closing socket resources: " + e.toString());
187 | }
188 | System.out.println("Client disconnected");
189 | }
190 | }
191 | } catch (IOException e) {
192 | System.err.println("Could not listen on port " + PORT + ": " + e.toString());
193 | System.exit(1);
194 | } finally {
195 | if (serverSocket != null) {
196 | try {
197 | serverSocket.close();
198 | } catch (IOException e) {
199 | System.err.println("Error closing server socket: " + e.toString());
200 | }
201 | }
202 | kakaoDb.closeConnection();
203 | dbWatcherThread.interrupt();
204 | try {
205 | dbWatcherThread.join();
206 | } catch (InterruptedException e) {
207 | System.err.println("Error joining DB watcher thread: " + e.toString());
208 | }
209 | }
210 | }
211 |
212 |
213 | private static void checkDbChanges(SendMsgDB.KakaoDB kakaoDb, SendMsgDB.ObserverHelper observerHelper) {
214 | File watchFile = new File(WATCH_FILE);
215 | long currentModifiedTime = watchFile.lastModified();
216 |
217 | if (currentModifiedTime > lastModifiedTime) {
218 | lastModifiedTime = currentModifiedTime;
219 | System.out.println("Database file changed detected at: " + new java.util.Date(currentModifiedTime));
220 | observerHelper.checkChange(kakaoDb, WATCH_FILE);
221 | }
222 | }
223 |
224 |
225 | private static void SendMessage(String notiRef, Long chatId, String msg) throws Exception {
226 | Intent intent = new Intent();
227 | intent.setComponent(new ComponentName("com.kakao.talk", "com.kakao.talk.notification.NotificationActionService"));
228 |
229 | intent.putExtra("noti_referer", notiRef);
230 | intent.putExtra("chat_id", chatId);
231 | intent.setAction("com.kakao.talk.notification.REPLY_MESSAGE");
232 |
233 | Bundle results = new Bundle();
234 | results.putCharSequence("reply_message", msg);
235 |
236 | RemoteInput remoteInput = new RemoteInput.Builder("reply_message").build();
237 | RemoteInput[] remoteInputs = new RemoteInput[]{remoteInput};
238 | RemoteInput.addResultsToIntent(remoteInputs, intent, results);
239 | activityManager.startService(
240 | null,
241 | intent,
242 | intent.getType(),
243 | false,
244 | "com.android.shell",
245 | null,
246 | -2
247 | );
248 | }
249 |
250 | private static void SendPhoto(String room, String base64ImageDataString) throws Exception {
251 | byte[] decodedImage = android.util.Base64.decode(base64ImageDataString, android.util.Base64.DEFAULT);
252 | String timestamp = String.valueOf(System.currentTimeMillis());
253 | File picDir = new File("/sdcard/Android/data/com.kakao.talk/files");
254 | if (!picDir.exists()) {
255 | picDir.mkdirs();
256 | }
257 | File imageFile = new File(picDir, timestamp + ".png");
258 | FileOutputStream fos = null;
259 | try {
260 | fos = new FileOutputStream(imageFile);
261 | fos.write(decodedImage);
262 | fos.flush();
263 | } catch (IOException e) {
264 | System.err.println("Error saving image to file: " + e.toString());
265 | throw e;
266 | } finally {
267 | if (fos != null) {
268 | try {
269 | fos.close();
270 | } catch (IOException e) {
271 | System.err.println("Error closing FileOutputStream: " + e.toString());
272 | }
273 | }
274 | }
275 |
276 | Uri imageUri = Uri.fromFile(imageFile);
277 |
278 | Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
279 | mediaScanIntent.setData(imageUri);
280 | try {
281 | activityManager.broadcastIntent(
282 | null,
283 | mediaScanIntent,
284 | null,
285 | null,
286 | 0,
287 | null,
288 | null,
289 | null,
290 | -1,
291 | null,
292 | false,
293 | false,
294 | -2
295 | );
296 | System.out.println("Media scanner broadcast intent sent for: " + imageUri.toString());
297 | } catch (Exception e) {
298 | System.err.println("Error broadcasting media scanner intent: " + e.toString());
299 | throw e;
300 | }
301 |
302 | Intent intent = new Intent();
303 | intent.setAction(Intent.ACTION_SENDTO);
304 | intent.setType("image/png");
305 | intent.putExtra(Intent.EXTRA_STREAM, imageUri);
306 | intent.putExtra("key_id", Long.parseLong(room));
307 | intent.putExtra("key_type", 1);
308 | intent.putExtra("key_from_direct_share", true);
309 | intent.setPackage("com.kakao.talk");
310 |
311 | try {
312 | activityManager.startActivityAsUserWithFeature(
313 | null,
314 | "com.android.shell",
315 | null,
316 | intent,
317 | intent.getType(),
318 | null, null, 0, 0,
319 | null,
320 | null,
321 | -2
322 | );
323 | } catch (Exception e) {
324 | System.err.println("Error starting activity for sending image: " + e.toString());
325 | throw e;
326 | }
327 | }
328 |
329 | // --- Inner Class: KakaoDecrypt ---
330 | static class KakaoDecrypt {
331 | private static final java.util.Map keyCache = new java.util.HashMap<>();
332 | private static long BOT_USER_ID;
333 |
334 | static {
335 | try {
336 | StringBuilder sb = new StringBuilder();
337 | try (BufferedReader reader = new BufferedReader(new FileReader(CONFIG_FILE_PATH))) {
338 | String line;
339 | while ((line = reader.readLine()) != null) {
340 | sb.append(line);
341 | }
342 | }
343 | JSONObject config = new JSONObject(sb.toString());
344 | BOT_USER_ID = config.getLong("bot_id");
345 | } catch (IOException | org.json.JSONException e) {
346 | System.err.println("Error reading config.json or parsing bot_id: " + e.toString());
347 | BOT_USER_ID = 0;
348 | }
349 | }
350 |
351 |
352 | private static String incept(int n) {
353 | String[] dict1 = {"adrp.ldrsh.ldnp", "ldpsw", "umax", "stnp.rsubhn", "sqdmlsl", "uqrshl.csel", "sqshlu", "umin.usubl.umlsl", "cbnz.adds", "tbnz",
354 | "usubl2", "stxr", "sbfx", "strh", "stxrb.adcs", "stxrh", "ands.urhadd", "subs", "sbcs", "fnmadd.ldxrb.saddl",
355 | "stur", "ldrsb", "strb", "prfm", "ubfiz", "ldrsw.madd.msub.sturb.ldursb", "ldrb", "b.eq", "ldur.sbfiz", "extr",
356 | "fmadd", "uqadd", "sshr.uzp1.sttrb", "umlsl2", "rsubhn2.ldrh.uqsub", "uqshl", "uabd", "ursra", "usubw", "uaddl2",
357 | "b.gt", "b.lt", "sqshl", "bics", "smin.ubfx", "smlsl2", "uabdl2", "zip2.ssubw2", "ccmp", "sqdmlal",
358 | "b.al", "smax.ldurh.uhsub", "fcvtxn2", "b.pl"};
359 | String[] dict2 = {"saddl", "urhadd", "ubfiz.sqdmlsl.tbnz.stnp", "smin", "strh", "ccmp", "usubl", "umlsl", "uzp1", "sbfx",
360 | "b.eq", "zip2.prfm.strb", "msub", "b.pl", "csel", "stxrh.ldxrb", "uqrshl.ldrh", "cbnz", "ursra", "sshr.ubfx.ldur.ldnp",
361 | "fcvtxn2", "usubl2", "uaddl2", "b.al", "ssubw2", "umax", "b.lt", "adrp.sturb", "extr", "uqshl",
362 | "smax", "uqsub.sqshlu", "ands", "madd", "umin", "b.gt", "uabdl2", "ldrsb.ldpsw.rsubhn", "uqadd", "sttrb",
363 | "stxr", "adds", "rsubhn2.umlsl2", "sbcs.fmadd", "usubw", "sqshl", "stur.ldrsh.smlsl2", "ldrsw", "fnmadd", "stxrb.sbfiz",
364 | "adcs", "bics.ldrb", "l1ursb", "subs.uhsub", "ldurh", "uabd", "sqdmlal"};
365 | String word1 = dict1[n % dict1.length];
366 | String word2 = dict2[(n + 31) % dict2.length];
367 | return word1 + '.' + word2;
368 | }
369 |
370 | private static byte[] genSalt(long user_id, int encType) {
371 | if (user_id <= 0) {
372 | return new byte[16];
373 | }
374 |
375 | String[] prefixes = {"", "", "12", "24", "18", "30", "36", "12", "48", "7", "35", "40", "17", "23", "29",
376 | "isabel", "kale", "sulli", "van", "merry", "kyle", "james", "maddux",
377 | "tony", "hayden", "paul", "elijah", "dorothy", "sally", "bran",
378 | incept(830819), "veil"};
379 | String saltStr;
380 | try {
381 | saltStr = prefixes[encType] + user_id;
382 | saltStr = saltStr.substring(0, Math.min(saltStr.length(), 16));
383 | } catch (ArrayIndexOutOfBoundsException e) {
384 | throw new IllegalArgumentException("Unsupported encoding type " + encType, e);
385 | }
386 | saltStr = saltStr + "\0".repeat(Math.max(0, 16 - saltStr.length()));
387 | return saltStr.getBytes(StandardCharsets.UTF_8);
388 | }
389 |
390 | private static void pkcs16adjust(byte[] a, int aOff, byte[] b) {
391 | int x = (b[b.length - 1] & 0xff) + (a[aOff + b.length - 1] & 0xff) + 1;
392 | a[aOff + b.length - 1] = (byte) (x % 256);
393 | x = x >> 8;
394 | for (int i = b.length - 2; i >= 0; i--) {
395 | x = x + (b[i] & 0xff) + (a[aOff + i] & 0xff);
396 | a[aOff + i] = (byte) (x % 256);
397 | x = x >> 8;
398 | }
399 | }
400 |
401 | private static byte[] deriveKey(byte[] passwordBytes, byte[] saltBytes, int iterations, int dkeySize) throws Exception {
402 | String password = new String(passwordBytes, StandardCharsets.US_ASCII) + "\0";
403 | byte[] passwordUTF16BE = password.getBytes(StandardCharsets.UTF_16BE);
404 |
405 | MessageDigest hasher = MessageDigest.getInstance("SHA-1");
406 | int digestSize = hasher.getDigestLength();
407 | int blockSize = 64;
408 |
409 | byte[] D = new byte[blockSize];
410 | for (int i = 0; i < blockSize; i++) {
411 | D[i] = 1;
412 | }
413 | byte[] S = new byte[blockSize * ((saltBytes.length + blockSize - 1) / blockSize)];
414 | for (int i = 0; i < S.length; i++) {
415 | S[i] = saltBytes[i % saltBytes.length];
416 | }
417 | byte[] P = new byte[blockSize * ((passwordUTF16BE.length + blockSize - 1) / blockSize)];
418 | for (int i = 0; i < P.length; i++) {
419 | P[i] = passwordUTF16BE[i % passwordUTF16BE.length];
420 | }
421 |
422 | byte[] I = new byte[S.length + P.length];
423 | System.arraycopy(S, 0, I, 0, S.length);
424 | System.arraycopy(P, 0, I, S.length, P.length);
425 |
426 | byte[] B = new byte[blockSize];
427 | int c = (dkeySize + digestSize - 1) / digestSize;
428 |
429 | byte[] dKey = new byte[dkeySize];
430 | for (int i = 1; i <= c; i++) {
431 | hasher = MessageDigest.getInstance("SHA-1");
432 | hasher.update(D);
433 | hasher.update(I);
434 | byte[] A = hasher.digest();
435 |
436 | for (int j = 1; j < iterations; j++) {
437 | hasher = MessageDigest.getInstance("SHA-1");
438 | hasher.update(A);
439 | A = hasher.digest();
440 | }
441 |
442 | for (int j = 0; j < B.length; j++) {
443 | B[j] = A[j % A.length];
444 | }
445 |
446 | for (int j = 0; j < I.length / blockSize; j++) {
447 | pkcs16adjust(I, j * blockSize, B);
448 | }
449 |
450 | int start = (i - 1) * digestSize;
451 | if (i == c) {
452 | System.arraycopy(A, 0, dKey, start, dkeySize - start);
453 | } else {
454 | System.arraycopy(A, 0, dKey, start, A.length);
455 | }
456 | }
457 |
458 | return dKey;
459 | }
460 |
461 | public static String decrypt(int encType, String b64_ciphertext, long user_id) throws Exception {
462 | byte[] keyBytes = new byte[] {
463 | (byte)0x16, (byte)0x08, (byte)0x09, (byte)0x6f, (byte)0x02, (byte)0x17, (byte)0x2b, (byte)0x08,
464 | (byte)0x21, (byte)0x21, (byte)0x0a, (byte)0x10, (byte)0x03, (byte)0x03, (byte)0x07, (byte)0x06
465 | };
466 | byte[] ivBytes = new byte[] {
467 | (byte)0x0f, (byte)0x08, (byte)0x01, (byte)0x00, (byte)0x19, (byte)0x47, (byte)0x25, (byte)0xdc,
468 | (byte)0x15, (byte)0xf5, (byte)0x17, (byte)0xe0, (byte)0xe1, (byte)0x15, (byte)0x0c, (byte)0x35
469 | };
470 |
471 | byte[] salt = genSalt(user_id, encType);
472 | byte[] key;
473 | String saltStr = new String(salt, StandardCharsets.UTF_8);
474 | if (keyCache.containsKey(saltStr)) {
475 | key = keyCache.get(saltStr);
476 | } else {
477 | key = deriveKey(keyBytes, salt, 2, 32);
478 | keyCache.put(saltStr, key);
479 | }
480 |
481 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
482 | IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
483 | Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
484 |
485 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
486 |
487 | byte[] ciphertext = java.util.Base64.getDecoder().decode(b64_ciphertext);
488 | if (ciphertext.length == 0) {
489 | return b64_ciphertext;
490 | }
491 | byte[] padded;
492 | try {
493 | padded = cipher.doFinal(ciphertext);
494 | } catch (javax.crypto.BadPaddingException e) {
495 | System.err.println("BadPaddingException during decryption, possibly due to incorrect key or data. Returning original ciphertext.");
496 | return b64_ciphertext;
497 | }
498 |
499 |
500 | int paddingLength = padded[padded.length - 1];
501 | if (paddingLength <= 0 || paddingLength > cipher.getBlockSize()) {
502 | throw new IllegalArgumentException("Invalid padding length: " + paddingLength);
503 | }
504 |
505 | byte[] plaintextBytes = new byte[padded.length - paddingLength];
506 | System.arraycopy(padded, 0, plaintextBytes, 0, plaintextBytes.length);
507 |
508 |
509 | return new String(plaintextBytes, StandardCharsets.UTF_8);
510 |
511 | }
512 |
513 | public static String encrypt(int encType, String plaintext, long user_id) throws Exception {
514 | byte[] keyBytes = new byte[] {
515 | (byte)0x16, (byte)0x08, (byte)0x09, (byte)0x6f, (byte)0x02, (byte)0x17, (byte)0x2b, (byte)0x08,
516 | (byte)0x21, (byte)0x21, (byte)0x0a, (byte)0x10, (byte)0x03, (byte)0x03, (byte)0x07, (byte)0x06
517 | };
518 | byte[] ivBytes = new byte[] {
519 | (byte)0x0f, (byte)0x08, (byte)0x01, (byte)0x00, (byte)0x19, (byte)0x47, (byte)0x25, (byte)0xdc,
520 | (byte)0x15, (byte)0x5, (byte)0x17, (byte)0xe0, (byte)0xe1, (byte)0x15, (byte)0x0c, (byte)0x35
521 | };
522 |
523 | byte[] salt = genSalt(user_id, encType);
524 | byte[] key;
525 | String saltStr = new String(salt, StandardCharsets.UTF_8);
526 | if (keyCache.containsKey(saltStr)) {
527 | key = keyCache.get(saltStr);
528 | } else {
529 | key = deriveKey(keyBytes, salt, 2, 32);
530 | keyCache.put(saltStr, key);
531 | }
532 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
533 | IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
534 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
535 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
536 | byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
537 | String b64_ciphertext = java.util.Base64.getEncoder().encodeToString(ciphertext);
538 | return b64_ciphertext;
539 | }
540 | }
541 |
542 | // --- Inner Class: KakaoDB ---
543 | static class KakaoDB extends SendMsgDB.KakaoDecrypt {
544 | private JSONObject config;
545 | private String DB_PATH;
546 | private long BOT_ID;
547 | private String BOT_NAME;
548 | private SQLiteDatabase db = null;
549 |
550 | public KakaoDB() {
551 | try {
552 | StringBuilder sb = new StringBuilder();
553 | try (BufferedReader reader = new BufferedReader(new FileReader(CONFIG_FILE_PATH))) {
554 | String line;
555 | while ((line = reader.readLine()) != null) {
556 | sb.append(line);
557 | }
558 | }
559 | config = new JSONObject(sb.toString());
560 | DB_PATH = "/data/data/com.kakao.talk/databases";
561 | BOT_ID = config.getLong("bot_id");
562 | BOT_NAME = config.getString("bot_name");
563 |
564 | db = SQLiteDatabase.openDatabase(DB_PATH + "/KakaoTalk.db", null, SQLiteDatabase.OPEN_READWRITE);
565 |
566 | db.execSQL("ATTACH DATABASE '" + DB_PATH + "/KakaoTalk2.db' AS db2");
567 |
568 |
569 | } catch (SQLiteException e) {
570 | System.err.println("SQLiteException: " + e.getMessage());
571 | System.err.println("You don't have a permission to access KakaoTalk Database.");
572 | System.exit(1);
573 | } catch (IOException e) {
574 | System.err.println("IO Exception reading config.json: " + e.toString());
575 | System.exit(1);
576 | } catch (JSONException e) {
577 | System.err.println("JSON parsing error in config.json: " + e.toString());
578 | System.exit(1);
579 | }
580 | }
581 |
582 | public List getColumnInfo(String table) {
583 | List cols = new ArrayList<>();
584 | Cursor cursor = null;
585 | try {
586 | cursor = db.rawQuery("SELECT * FROM " + table + " LIMIT 1", null);
587 | if (cursor != null && cursor.moveToFirst()) {
588 | String[] columnNames = cursor.getColumnNames();
589 | for (String columnName : columnNames) {
590 | cols.add(columnName);
591 | }
592 | }
593 | } catch (SQLiteException e) {
594 | System.err.println("Error in getColumnInfo for table " + table + ": " + e.getMessage());
595 | return new ArrayList<>();
596 | } finally {
597 | if (cursor != null) {
598 | cursor.close();
599 | }
600 | }
601 | return cols;
602 | }
603 |
604 |
605 | public List getTableInfo() {
606 | List tables = new ArrayList<>();
607 | Cursor cursor = null;
608 | try {
609 | cursor = db.rawQuery("SELECT name FROM sqlite_schema WHERE type='table'", null);
610 | if (cursor != null) {
611 | while (cursor.moveToNext()) {
612 | tables.add(cursor.getString(0));
613 | }
614 | }
615 | } catch (SQLiteException e) {
616 | System.err.println("Error in getTableInfo: " + e.getMessage());
617 | return new ArrayList<>();
618 | } finally {
619 | if (cursor != null) {
620 | cursor.close();
621 | }
622 | }
623 | return tables;
624 | }
625 |
626 | public String getNameOfUserId(long userId) {
627 | String dec_row_name = null;
628 | Cursor cursor = null;
629 | try {
630 | String sql;
631 | String[] stringUserId = {Long.toString(userId)};
632 | if (checkNewDb()) {
633 | sql = "WITH info AS (SELECT ? AS user_id) " +
634 | "SELECT COALESCE(open_chat_member.nickname, friends.name) AS name, " +
635 | "COALESCE(open_chat_member.enc, friends.enc) AS enc " +
636 | "FROM info " +
637 | "LEFT JOIN db2.open_chat_member ON open_chat_member.user_id = info.user_id " +
638 | "LEFT JOIN db2.friends ON friends.id = info.user_id;";
639 | } else {
640 | sql = "SELECT name, enc FROM db2.friends WHERE id = ?";
641 | }
642 | cursor = db.rawQuery(sql, stringUserId);
643 |
644 | if (cursor != null && cursor.moveToNext()) {
645 | String row_name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
646 | String enc = cursor.getString(cursor.getColumnIndexOrThrow("enc"));
647 | dec_row_name = SendMsgDB.KakaoDecrypt.decrypt(Integer.parseInt(enc), row_name, KakaoDecrypt.BOT_USER_ID);
648 | }
649 |
650 | } catch (SQLiteException e) {
651 | System.err.println("Error in getNameOfUserId: " + e.getMessage());
652 | return "";
653 | } catch (Exception e) {
654 | System.err.println("Decryption error in getNameOfUserId: " + e.getMessage());
655 | return "";
656 | } finally {
657 | if (cursor != null) {
658 | cursor.close();
659 | }
660 | }
661 | return dec_row_name;
662 | }
663 |
664 |
665 | public String[] getUserInfo(long chatId, long userId) {
666 | String sender;
667 | if (userId == BOT_ID) {
668 | sender = BOT_NAME;
669 | } else {
670 | sender = getNameOfUserId(userId);
671 | }
672 |
673 | String room = sender;
674 | Cursor cursor = null;
675 | try {
676 | String sql = "SELECT name FROM db2.open_link WHERE id = (SELECT link_id FROM chat_rooms WHERE id = ?)";
677 | String[] selectionArgs = {String.valueOf(chatId)};
678 | cursor = db.rawQuery(sql, selectionArgs);
679 |
680 | if (cursor != null && cursor.moveToNext()) {
681 | room = cursor.getString(0);
682 | }
683 | } catch (SQLiteException e) {
684 | System.err.println("Error in getUserInfo: " + e.getMessage());
685 | } finally {
686 | if (cursor != null) {
687 | cursor.close();
688 | }
689 | }
690 | return new String[]{room, sender};
691 | }
692 |
693 | public Map getRowFromLogId(long logId) {
694 | Map rowMap = new HashMap<>();
695 | Cursor cursor = null;
696 | try {
697 | String sql = "SELECT * FROM chat_logs WHERE id = ?";
698 | String[] selectionArgs = {String.valueOf(logId)};
699 | cursor = db.rawQuery(sql, selectionArgs);
700 | if (cursor != null && cursor.moveToNext()) {
701 | String[] columnNames = cursor.getColumnNames();
702 | for (String columnName : columnNames) {
703 | int columnIndex = cursor.getColumnIndexOrThrow(columnName);
704 | rowMap.put(columnName, cursor.getString(columnIndex));
705 | }
706 | }
707 | } catch (SQLiteException e) {
708 | System.err.println("Error in getRowFromLogId: " + e.getMessage());
709 | return null;
710 | } finally {
711 | if (cursor != null) {
712 | cursor.close();
713 | }
714 | }
715 | return rowMap;
716 | }
717 |
718 |
719 | public Map logToDict(long logId) {
720 | Map dict = new HashMap<>();
721 | Cursor cursor = null;
722 | try {
723 | String sql = "SELECT * FROM chat_logs ORDER BY _id DESC LIMIT 1";
724 | cursor = db.rawQuery(sql, null);
725 | if (cursor != null && cursor.moveToNext()) {
726 | String[] columnNames = cursor.getColumnNames();
727 | for (String columnName : columnNames) {
728 | int columnIndex = cursor.getColumnIndexOrThrow(columnName);
729 | dict.put(columnName, cursor.getString(columnIndex));
730 | }
731 | }
732 | } catch (SQLiteException e) {
733 | System.err.println("Error in logToDict (getLastLog): " + e.getMessage());
734 | return null;
735 | } finally {
736 | if (cursor != null) {
737 | cursor.close();
738 | }
739 | }
740 | return dict;
741 | }
742 |
743 |
744 | public boolean checkNewDb() {
745 | boolean isNewDb = false;
746 | Cursor cursor = null;
747 | try {
748 | cursor = db.rawQuery("SELECT name FROM db2.sqlite_master WHERE type='table' AND name='open_chat_member'", null);
749 | isNewDb = cursor.getCount() > 0;
750 | } catch (SQLiteException e) {
751 | System.err.println("Error in checkNewDb: " + e.getMessage());
752 | return false;
753 | } finally {
754 | if (cursor != null) {
755 | cursor.close();
756 | }
757 | }
758 | return isNewDb;
759 | }
760 |
761 | public void closeConnection() {
762 | if (db != null && db.isOpen()) {
763 | db.close();
764 | System.out.println("Database connection closed.");
765 | }
766 | }
767 |
768 | public SQLiteDatabase getConnection() {
769 | return this.db;
770 | }
771 | }
772 |
773 | // --- Inner Class: ObserverHelper ---
774 | static class ObserverHelper {
775 | private long lastLogId = 0;
776 | private JSONObject config;
777 | private long BOT_ID;
778 | private String BOT_NAME;
779 | private String BOT_IP;
780 | private int BOT_SOCKET_PORT;
781 | private String WEB_SERVER_ENDPOINT; // added web server endpoint
782 |
783 | public ObserverHelper() {
784 | try {
785 | StringBuilder sb = new StringBuilder();
786 | try (BufferedReader reader = new BufferedReader(new FileReader(CONFIG_FILE_PATH))) {
787 | String line;
788 | while ((line = reader.readLine()) != null) {
789 | sb.append(line);
790 | }
791 | }
792 | config = new JSONObject(sb.toString());
793 | BOT_ID = config.getLong("bot_id");
794 | BOT_NAME = config.getString("bot_name");
795 | BOT_IP = config.getString("bot_ip"); // keep bot_ip for possible other usages, as commented in user's config example
796 | BOT_SOCKET_PORT = config.getInt("bot_socket_port");
797 | WEB_SERVER_ENDPOINT = config.getString("web_server_endpoint"); // read web_server_endpoint
798 |
799 | } catch (IOException e) {
800 | System.err.println("IO Exception reading config.json: " + e.toString());
801 | } catch (JSONException e) {
802 | System.err.println("JSON parsing error in config.json: " + e.toString());
803 | }
804 | }
805 |
806 | private String makePostData(String decMsg, String room, String sender, JSONObject js) throws JSONException {
807 | JSONObject data = new JSONObject();
808 | data.put("msg", decMsg);
809 | data.put("room", room);
810 | data.put("sender", sender);
811 | data.put("json", js);
812 | return data.toString();
813 | }
814 |
815 | public void checkChange(SendMsgDB.KakaoDB db, String watchFile) {
816 | if (lastLogId == 0) {
817 | Map lastLog = db.logToDict(0);
818 | if (lastLog != null && lastLog.containsKey("_id")) {
819 | lastLogId = Long.parseLong((String)lastLog.get("_id"));
820 | } else {
821 | lastLogId = 0;
822 | }
823 | System.out.println("Initial lastLogId: " + lastLogId);
824 | return;
825 | }
826 |
827 | String sql = "select * from chat_logs where _id > ? order by _id asc";
828 | Cursor res = null;
829 | try {
830 | String[] selectionArgs = {String.valueOf(lastLogId)};
831 | res = db.getConnection().rawQuery(sql, selectionArgs);
832 | List description = new ArrayList<>();
833 | if (res.getColumnNames() != null) {
834 | for (String columnName : res.getColumnNames()) {
835 | description.add(columnName);
836 | }
837 | }
838 |
839 |
840 | while (res != null && res.moveToNext()) {
841 | long currentLogId = res.getLong(res.getColumnIndexOrThrow("_id"));
842 | if (currentLogId > lastLogId) {
843 | lastLogId = currentLogId;
844 | JSONObject logJson = new JSONObject();
845 | for (String columnName : description) {
846 | try {
847 | logJson.put(columnName, res.getString(res.getColumnIndexOrThrow(columnName)));
848 | } catch (JSONException e) {
849 | System.err.println("JSONException while adding log data to JSON object: " + e.getMessage());
850 | continue;
851 | }
852 | }
853 |
854 |
855 | String enc_msg = res.getString(res.getColumnIndexOrThrow("message"));
856 | long user_id = res.getLong(res.getColumnIndexOrThrow("user_id"));
857 | int encType = 0;
858 | try {
859 | JSONObject v = new JSONObject(res.getString(res.getColumnIndexOrThrow("v")));
860 | encType = v.getInt("enc");
861 | } catch (JSONException e) {
862 | System.err.println("Error parsing 'v' JSON for encType: " + e.getMessage());
863 | encType = 0;
864 | }
865 |
866 |
867 | String dec_msg;
868 | try {
869 | dec_msg = SendMsgDB.KakaoDecrypt.decrypt(encType, enc_msg, user_id);
870 | } catch (Exception e) {
871 | System.err.println("Decryption error for logId " + currentLogId + ": " + e.toString());
872 | dec_msg = "[Decryption Failed]";
873 | }
874 | long chat_id = res.getLong(res.getColumnIndexOrThrow("chat_id"));
875 | String[] userInfo = db.getUserInfo(chat_id, user_id);
876 | String room = userInfo[0];
877 | String sender = userInfo[1];
878 | if (room.equals(BOT_NAME)) {
879 | room = sender;
880 | }
881 | String postData;
882 | try {
883 | postData = makePostData(dec_msg, room, sender, logJson);
884 | // modified to use WEB_SERVER_ENDPOINT
885 | sendPostRequest(WEB_SERVER_ENDPOINT, postData);
886 | } catch (JSONException e) {
887 | System.err.println("JSON error creating post data: " + e.getMessage());
888 | }
889 | System.out.println("New message from " + sender + " in " + room + ": " + dec_msg);
890 | }
891 | }
892 | } catch (SQLiteException e) {
893 | System.err.println("SQL error in checkChange: " + e.getMessage());
894 | } finally {
895 | if (res != null) {
896 | res.close();
897 | }
898 | }
899 | }
900 |
901 | private void sendPostRequest(String urlStr, String jsonData) {
902 | System.out.println("Sending HTTP POST request to: " + urlStr);
903 | System.out.println("JSON Data being sent: " + jsonData);
904 | try {
905 | URL url = new URL(urlStr);
906 | HttpURLConnection con = (HttpURLConnection) url.openConnection();
907 | con.setRequestMethod("POST");
908 |
909 | con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
910 | con.setRequestProperty("Accept", "application/json");
911 |
912 | con.setDoOutput(true);
913 |
914 | String postData = "data=" + URLEncoder.encode(jsonData, StandardCharsets.UTF_8.toString());
915 | try (OutputStream os = con.getOutputStream()) {
916 | byte[] input = postData.getBytes(StandardCharsets.UTF_8);
917 | os.write(input, 0, input.length);
918 | }
919 |
920 | int responseCode = con.getResponseCode();
921 | System.out.println("HTTP Response Code: " + responseCode);
922 |
923 | try (BufferedReader br = new BufferedReader(
924 | new java.io.InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {
925 | StringBuilder response = new StringBuilder();
926 | String responseLine;
927 | while ((responseLine = br.readLine()) != null) {
928 | response.append(responseLine.trim());
929 | }
930 | String responseBody = response.toString();
931 | System.out.println("HTTP Response Body: " + responseBody);
932 | } catch (IOException e) {
933 | System.err.println("Error reading HTTP response body: " + e.getMessage());
934 | }
935 | con.disconnect();
936 |
937 | } catch (IOException e) {
938 | System.err.println("IO error sending POST request: " + e.getMessage());
939 | }
940 | }
941 | }
942 | }
943 |
--------------------------------------------------------------------------------
/SendMsgDB/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "bot_name" : "YOUR_BOT_NAME",
3 | "bot_id" : YOUR_BOT_ID,
4 | "db_path" : "/data/data/com.kakao.talk/databases",
5 | "bot_ip" : "172.17.0.1",
6 | "bot_socket_port" : 3000,
7 | "web_server_endpoint" : "http://172.17.0.1:5001/db"
8 | }
9 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | # coding: utf8
2 | from flask import Flask,request,json
3 | import base64
4 | from chatbot.Response import response
5 | from helper.Replier import Replier
6 | from helper.KakaoDB import KakaoDB
7 | from helper.SharedDict import get_shared_state
8 | import time
9 | import sys
10 |
11 | app = Flask(__name__)
12 | db = KakaoDB()
13 | g = get_shared_state()
14 |
15 | @app.route('/db',methods=['POST'])
16 | def py_exec_db():
17 | r = app.response_class(
18 | response="200",
19 | status=200,
20 | mimetype='text/plain; charset="utf-8"'
21 | )
22 | request_data = json.loads(request.form['data'])
23 | replier = Replier(request_data)
24 | @r.call_on_close
25 | def on_close():
26 | response(request_data["room"],
27 | request_data["msg"],
28 | request_data["sender"],
29 | replier,
30 | request_data["json"],
31 | db,
32 | g
33 | )
34 | sys.stdout.flush()
35 | return r
36 |
37 | if __name__ == "__main__":
38 | SharedState.register('state', SharedState._get_shared_state, NamespaceProxy)
39 | app.run(host='0.0.0.0', port=5000)
40 |
--------------------------------------------------------------------------------
/chatbot.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=chatbot www service
3 |
4 | [Service]
5 | User=YOUR_USERNAME
6 | WorkingDirectory=YOUR_PYKAKAODBBOT_HOME
7 | ExecStart=/YOUR_PYKAKAODBBOT_HOME/venv/bin/gunicorn -b 0.0.0.0:5000 -w 9 app:app -t 100
8 | Restart=on-failure
9 | RestartSec=1s
10 |
11 | [Install]
12 | WantedBy=multi-user.target
13 |
--------------------------------------------------------------------------------
/chatbot/Response.py:
--------------------------------------------------------------------------------
1 | def response(room, msg, sender, replier, msg_json, db, g):
2 | if msg == "!hi":
3 | replier.reply("hello")
4 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "bot_name" : "봇 이름을 입력합니다",
3 | "bot_id" : 봇의 user_id를 입력합니다,
4 | "db_path" : "/home/리눅스 username을 입력합니다/data/data/com.kakao.talk/databases",
5 | "bot_ip" : "127.0.0.1",
6 | "bot_socket_port" : 3000
7 | }
8 |
--------------------------------------------------------------------------------
/dbobserver.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=DB Observer for chatbot
3 |
4 | [Service]
5 | User=YOUR_USERNAME
6 | WorkingDirectory=YOUR_PYKAKAODBBOT_HOME
7 | ExecStart=/YOUR_PYKAKAODBBOT_HOME/venv/bin/python /YOUR_PYKAKAODBBOT_HOME/observer.py
8 | Restart=on-failure
9 | RestartSec=1s
10 |
11 | [Install]
12 | WantedBy=multi-user.target
13 |
--------------------------------------------------------------------------------
/guess_user_id.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 | db = sqlite3.connect('../data/data/com.kakao.talk/databases/KakaoTalk.db')
3 | cur = db.cursor()
4 | cur.execute('SELECT user_id FROM chat_logs WHERE v LIKE \'%\"isMine\":true%\' LIMIT 1;')
5 | print(cur.fetchall()[0][0])
6 |
--------------------------------------------------------------------------------
/helper/KakaoDB.py:
--------------------------------------------------------------------------------
1 | from helper.KakaoDecrypt import KakaoDecrypt
2 | from helper.ObserverHelper import get_config
3 | import sqlite3
4 | import time
5 | import sys
6 |
7 |
8 | class KakaoDB(KakaoDecrypt):
9 | def __init__(self):
10 | self.config = get_config()
11 | self.DB_PATH = self.config["db_path"]
12 | self.BOT_ID = self.config["bot_id"]
13 | self.BOT_NAME = self.config["bot_name"]
14 |
15 | try:
16 | self.con = sqlite3.connect(f"{self.DB_PATH}/KakaoTalk.db")
17 | except Exception:
18 | print("You don't have a permission to access KakaoTalk Database.")
19 | sys.exit(1)
20 |
21 | self.cur = self.con.cursor()
22 | self.cur.execute(f"ATTACH DATABASE '{self.DB_PATH}/KakaoTalk2.db' AS db2")
23 |
24 | def get_column_info(self, table):
25 | try:
26 | self.cur.execute("SELECT * FROM ? LIMIT 1", [table])
27 | cols = [description[0] for description in self.cur.description]
28 | return cols
29 | except Exception:
30 | return []
31 |
32 | def get_table_info(self):
33 | self.cur.execute("SELECT name FROM sqlite_schema WHERE type='table';")
34 | tables = [table[0] for table in self.cur.fetchall()]
35 | return tables
36 |
37 | def get_name_of_user_id(self, user_id):
38 | if self.check_new_db():
39 | self.cur.execute(
40 | """
41 | WITH info AS (
42 | SELECT ? AS user_id
43 | )
44 | SELECT
45 | COALESCE(open_chat_member.nickname, friends.name) AS name,
46 | COALESCE(open_chat_member.enc, friends.enc) AS enc
47 | FROM info
48 | LEFT JOIN db2.open_chat_member
49 | ON open_chat_member.user_id = info.user_id
50 | LEFT JOIN db2.friends
51 | ON friends.id = info.user_id;
52 | """,
53 | [user_id],
54 | )
55 | else:
56 | self.cur.execute("SELECT name, enc FROM db2.friends WHERE id = ?", [user_id])
57 |
58 | res = self.cur.fetchall()
59 | for row in res:
60 | row_name = row[0]
61 | enc = row[1]
62 | dec_row_name = self.decrypt(enc, row_name)
63 | return dec_row_name
64 |
65 | def get_user_info(self, chat_id, user_id):
66 | if user_id == self.BOT_ID:
67 | sender = self.BOT_NAME
68 | else:
69 | sender = self.get_name_of_user_id(user_id)
70 |
71 | self.cur.execute(
72 | "SELECT name FROM db2.open_link WHERE id = (SELECT link_id FROM chat_rooms WHERE id = ?)",
73 | [chat_id],
74 | )
75 |
76 | res = self.cur.fetchall()
77 | if res == []:
78 | room = sender
79 | else:
80 | room = res[0][0]
81 | return (room, sender)
82 |
83 | def get_row_from_log_id(self, log_id):
84 | self.cur.execute("SELECT * FROM chat_logs WHERE id = ?", [log_id])
85 | res = self.cur.fetchall()
86 | return res[0]
87 |
88 | def clean_chat_logs(self, days):
89 | try:
90 | days = float(days)
91 | now = time.time()
92 | days_before_now = round(now - days * 24 * 60 * 60)
93 | sql = "delete from chat_logs where created_at < ?"
94 | self.cur.execute(sql, [days_before_now])
95 | self.con.commit()
96 | res = f"{days:g}일 이상 지난 데이터가 삭제되었습니다."
97 | except Exception:
98 | res = "요청이 잘못되었거나 에러가 발생하였습니다."
99 | return res
100 |
101 | def log_to_dict(self, log_id):
102 | sql = "select * from chat_logs where id = ?"
103 | self.cur.execute(sql, [log_id])
104 | descriptions = [d[0] for d in self.cur.description]
105 | rows = self.cur.fetchall()[0]
106 | return {descriptions[i]: rows[i] for i in range(len(descriptions))}
107 |
108 | def check_new_db(self):
109 | sql = "SELECT name FROM db2.sqlite_master WHERE type='table' AND name='open_chat_member'"
110 | self.cur.execute(sql)
111 | return self.cur.fetchone() is not None
112 |
--------------------------------------------------------------------------------
/helper/KakaoDecrypt.py:
--------------------------------------------------------------------------------
1 | from Crypto.Cipher import AES
2 | import hashlib
3 | import base64
4 | import argparse
5 | from Crypto.Util.Padding import pad
6 | import json
7 |
8 | #https://github.com/jiru/kakaodecrypt
9 | #removed unnecessary codes and added encrypt
10 |
11 | class KakaoDecrypt:
12 | key_cache = {}
13 | with open('config.json', 'r') as fo:
14 | BOT_USER_ID = json.loads(fo.read())["bot_id"]
15 |
16 | # Reimplementation of com.kakao.talk.dream.Projector.incept() from libdream.so
17 | @staticmethod
18 | def incept(n):
19 | dict1 = ['adrp.ldrsh.ldnp', 'ldpsw', 'umax', 'stnp.rsubhn', 'sqdmlsl', 'uqrshl.csel', 'sqshlu', 'umin.usubl.umlsl', 'cbnz.adds', 'tbnz',
20 | 'usubl2', 'stxr', 'sbfx', 'strh', 'stxrb.adcs', 'stxrh', 'ands.urhadd', 'subs', 'sbcs', 'fnmadd.ldxrb.saddl',
21 | 'stur', 'ldrsb', 'strb', 'prfm', 'ubfiz', 'ldrsw.madd.msub.sturb.ldursb', 'ldrb', 'b.eq', 'ldur.sbfiz', 'extr',
22 | 'fmadd', 'uqadd', 'sshr.uzp1.sttrb', 'umlsl2', 'rsubhn2.ldrh.uqsub', 'uqshl', 'uabd', 'ursra', 'usubw', 'uaddl2',
23 | 'b.gt', 'b.lt', 'sqshl', 'bics', 'smin.ubfx', 'smlsl2', 'uabdl2', 'zip2.ssubw2', 'ccmp', 'sqdmlal',
24 | 'b.al', 'smax.ldurh.uhsub', 'fcvtxn2', 'b.pl']
25 | dict2 = ['saddl', 'urhadd', 'ubfiz.sqdmlsl.tbnz.stnp', 'smin', 'strh', 'ccmp', 'usubl', 'umlsl', 'uzp1', 'sbfx',
26 | 'b.eq', 'zip2.prfm.strb', 'msub', 'b.pl', 'csel', 'stxrh.ldxrb', 'uqrshl.ldrh', 'cbnz', 'ursra', 'sshr.ubfx.ldur.ldnp',
27 | 'fcvtxn2', 'usubl2', 'uaddl2', 'b.al', 'ssubw2', 'umax', 'b.lt', 'adrp.sturb', 'extr', 'uqshl',
28 | 'smax', 'uqsub.sqshlu', 'ands', 'madd', 'umin', 'b.gt', 'uabdl2', 'ldrsb.ldpsw.rsubhn', 'uqadd', 'sttrb',
29 | 'stxr', 'adds', 'rsubhn2.umlsl2', 'sbcs.fmadd', 'usubw', 'sqshl', 'stur.ldrsh.smlsl2', 'ldrsw', 'fnmadd', 'stxrb.sbfiz',
30 | 'adcs', 'bics.ldrb', 'l1ursb', 'subs.uhsub', 'ldurh', 'uabd', 'sqdmlal']
31 | word1 = dict1[ n % len(dict1) ]
32 | word2 = dict2[ (n+31) % len(dict2) ]
33 | return word1 + '.' + word2
34 |
35 | @staticmethod
36 | def genSalt(user_id, encType):
37 | if user_id <= 0:
38 | return b'\0'*16
39 |
40 | prefixes = ['','','12','24','18','30','36','12','48','7','35','40','17','23','29',
41 | 'isabel','kale','sulli','van','merry','kyle','james', 'maddux',
42 | 'tony', 'hayden', 'paul', 'elijah', 'dorothy', 'sally', 'bran',
43 | KakaoDecrypt.incept(830819), 'veil']
44 | try:
45 | salt = prefixes[encType] + str(user_id)
46 | salt = salt[0:16]
47 | except IndexError:
48 | raise ValueError('Unsupported encoding type %i' % encType)
49 | salt = salt + '\0' * (16 - len(salt))
50 | return salt.encode('UTF-8')
51 |
52 | @staticmethod
53 | def pkcs16adjust(a, aOff, b):
54 | x = (b[len(b) - 1] & 0xff) + (a[aOff + len(b) - 1] & 0xff) + 1
55 | a[aOff + len(b) - 1] = x % 256
56 | x = x >> 8;
57 | for i in range(len(b)-2, -1, -1):
58 | x = x + (b[i] & 0xff) + (a[aOff + i] & 0xff)
59 | a[aOff + i] = x % 256
60 | x = x >> 8
61 |
62 | # PKCS12 key derivation as implemented in Bouncy Castle (using SHA1).
63 | # See org/bouncycastle/crypto/generators/PKCS12ParametersGenerator.java.
64 | @staticmethod
65 | def deriveKey(password, salt, iterations, dkeySize):
66 | password = (password + b'\0').decode('ascii').encode('utf-16-be')
67 |
68 | hasher = hashlib.sha1()
69 | v = hasher.block_size
70 | u = hasher.digest_size
71 |
72 | D = [ 1 ] * v
73 | S = [ 0 ] * v * int((len(salt) + v - 1) / v)
74 | for i in range(0, len(S)):
75 | S[i] = salt[i % len(salt)]
76 | P = [ 0 ] * v * int((len(password) + v - 1) / v)
77 | for i in range(0, len(P)):
78 | P[i] = password[i % len(password)]
79 |
80 | I = S + P
81 |
82 | B = [ 0 ] * v
83 | c = int((dkeySize + u - 1) / u)
84 |
85 | dKey = [0] * dkeySize
86 | for i in range(1, c+1):
87 | hasher = hashlib.sha1()
88 | hasher.update(bytes(D))
89 | hasher.update(bytes(I))
90 | A = hasher.digest()
91 |
92 | for j in range(1, iterations):
93 | hasher = hashlib.sha1()
94 | hasher.update(A)
95 | A = hasher.digest()
96 |
97 | A = list(A)
98 | for j in range(0, len(B)):
99 | B[j] = A[j % len(A)]
100 |
101 | for j in range(0, int(len(I)/v)):
102 | KakaoDecrypt.pkcs16adjust(I, j * v, B)
103 |
104 | start = (i - 1) * u
105 | if i == c:
106 | dKey[start : dkeySize] = A[0 : dkeySize-start]
107 | else:
108 | dKey[start : start+len(A)] = A[0 : len(A)]
109 |
110 | return bytes(dKey)
111 |
112 | @staticmethod
113 | def decrypt(encType, b64_ciphertext, user_id=BOT_USER_ID):
114 | key = b'\x16\x08\x09\x6f\x02\x17\x2b\x08\x21\x21\x0a\x10\x03\x03\x07\x06'
115 | iv = b'\x0f\x08\x01\x00\x19\x47\x25\xdc\x15\xf5\x17\xe0\xe1\x15\x0c\x35'
116 |
117 | salt = KakaoDecrypt.genSalt(user_id, encType)
118 | if salt in KakaoDecrypt.key_cache:
119 | key = KakaoDecrypt.key_cache[salt]
120 | else:
121 | key = KakaoDecrypt.deriveKey(key, salt, 2, 32)
122 | KakaoDecrypt.key_cache[salt] = key
123 | encoder = AES.new(key, AES.MODE_CBC, iv)
124 |
125 | ciphertext = base64.b64decode(b64_ciphertext)
126 | if len(ciphertext) == 0:
127 | return b64_ciphertext
128 | padded = encoder.decrypt(ciphertext)
129 | try:
130 | plaintext = padded[:-padded[-1]]
131 | except IndexError:
132 | raise ValueError('Unable to decrypt data', ciphertext)
133 | try:
134 | return plaintext.decode('UTF-8')
135 | except UnicodeDecodeError:
136 | return plaintext
137 |
138 | @staticmethod
139 | def encrypt(encType, plaintext, user_id=BOT_USER_ID):
140 | key = b'\x16\x08\x09\x6f\x02\x17\x2b\x08\x21\x21\x0a\x10\x03\x03\x07\x06'
141 | iv = b'\x0f\x08\x01\x00\x19\x47\x25\xdc\x15\xf5\x17\xe0\xe1\x15\x0c\x35'
142 | salt = KakaoDecrypt.genSalt(user_id, encType)
143 | if salt in KakaoDecrypt.key_cache:
144 | key = KakaoDecrypt.key_cache[salt]
145 | else:
146 | key = KakaoDecrypt.deriveKey(key, salt, 2, 32)
147 | KakaoDecrypt.key_cache[salt] = key
148 | encoder = AES.new(key, AES.MODE_CBC, iv)
149 | ciphertext = encoder.encrypt(pad(plaintext.encode('utf-8'),encoder.block_size))
150 | b64_ciphertext = base64.b64encode(ciphertext)
151 | return b64_ciphertext
152 |
--------------------------------------------------------------------------------
/helper/ObserverHelper.py:
--------------------------------------------------------------------------------
1 | import json
2 | import requests
3 | import sys
4 | import subprocess
5 | import psutil
6 | import time
7 |
8 | class ObserverHelper:
9 | def __init__(self,config):
10 | self.last_log_id = 0
11 | self.config = get_config()
12 | self.BOT_ID = self.config["bot_id"]
13 | self.BOT_NAME = self.config["bot_name"]
14 |
15 | def make_post_data(self, dec_msg, room, sender, js):
16 | data = {"msg" : dec_msg,
17 | "room" : room,
18 | "sender" : sender,
19 | "json" : js
20 | }
21 | return json.dumps(data)
22 |
23 | def check_change(self, db):
24 | if self.last_log_id == 0:
25 | db.cur.execute(f'select _id from chat_logs order by _id desc limit 1')
26 | self.last_log_id = db.cur.fetchall()[0][0]
27 | return
28 | db.cur.execute(f'select * from chat_logs where _id > ? order by _id asc',[self.last_log_id])
29 | description = [desc[0] for desc in db.cur.description]
30 | res = db.cur.fetchall()
31 |
32 | for row in res:
33 | if row[0] > self.last_log_id:
34 | if not self.check_sendmsg():
35 | self.run_sendmsg()
36 |
37 | self.last_log_id = row[0]
38 | v = json.loads(row[13])
39 | enc = v["enc"]
40 | origin = v["origin"]
41 | enc_msg = row[5]
42 | user_id = row[4]
43 | dec_msg = db.decrypt(enc,enc_msg,user_id)
44 | chat_id = row[3]
45 | user_info = db.get_user_info(chat_id,user_id)
46 | room = user_info[0]
47 | sender = user_info[1]
48 | if room == self.BOT_NAME:
49 | room = sender
50 | post_data = self.make_post_data(dec_msg, room, sender, {description[i]:row[i] for i in range(len(row))})
51 | try:
52 | requests.post("http://127.0.0.1:5000/db",data={"data":post_data})
53 | except:
54 | print("Flask server is not running.")
55 | sys.stdout.flush()
56 |
57 | def check_sendmsg(self):
58 | for process in psutil.process_iter():
59 | if "SendMsg" in process.cmdline():
60 | return True
61 | return False
62 |
63 | def run_sendmsg(self):
64 | subprocess.Popen(["adb","shell","su root sh -c 'CLASSPATH=/data/local/tmp/SendMsg.dex app_process / SendMsg' &"],
65 | stdin=subprocess.PIPE,
66 | stdout=subprocess.PIPE,
67 | stderr=subprocess.PIPE,
68 | text=True)
69 | time.sleep(1)
70 |
71 | def get_config():
72 | with open('config.json','r') as fo:
73 | config = json.loads(fo.read())
74 | return config
75 |
--------------------------------------------------------------------------------
/helper/Replier.py:
--------------------------------------------------------------------------------
1 | from socket import *
2 | from helper.ObserverHelper import get_config
3 | import json
4 | import base64
5 | import time
6 | import threading
7 | from PIL import Image
8 | import io
9 |
10 | class Replier:
11 | def __init__(self, request_data):
12 | self.config = get_config()
13 | self.ip = self.config["bot_ip"]
14 | self.port = self.config["bot_socket_port"]
15 | self.json = request_data["json"]
16 | self.room = str(self.json["chat_id"])
17 | self.queue = []
18 | self.last_sent_time = time.time()
19 |
20 | def send_socket(self, is_success, type, data, room, msg_json):
21 | clientSocket = socket(AF_INET, SOCK_STREAM)
22 | clientSocket.connect((self.ip,self.port))
23 |
24 | res = { "isSuccess":is_success,
25 | "type":type,
26 | "data":base64.b64encode(data.encode()).decode(),
27 | "room":base64.b64encode(room.encode()).decode(),
28 | "msgJson":base64.b64encode(json.dumps(msg_json).encode()).decode()
29 | }
30 | clientSocket.send(json.dumps(res).encode("utf-8"))
31 | clientSocket.close()
32 |
33 | def reply(self, msg, room=None):
34 | if room == None:
35 | room = self.room
36 | self.__queue_message(True,"normal",str(msg),room,self.json)
37 |
38 |
39 | def reply_image_from_file(self, room, filepath):
40 | img = Image.open(filepath)
41 | self.reply_image_from_image(room,img)
42 |
43 | def reply_image_from_image(self, room, img):
44 | buffered = io.BytesIO()
45 | img.save(buffered, format="PNG")
46 | png_bytes = buffered.getvalue()
47 | base64_bytes = base64.b64encode(png_bytes)
48 | base64_string = base64_bytes.decode('utf-8')
49 | if room == None:
50 | room = self.room
51 | self.__queue_message(True,"image",base64_string,str(room),{})
52 |
53 | def __queue_message(self, is_success, type, data, room, msg_json):
54 | self.queue.append((is_success, type, data, room, msg_json))
55 | if len(self.queue) == 1:
56 | self.__send_message()
57 |
58 | def __send_message(self):
59 | next_message = self.queue[0]
60 | current_time = time.time()
61 | if current_time-self.last_sent_time >= 0.1:
62 | self.send_socket(next_message[0],next_message[1],next_message[2],next_message[3],next_message[4])
63 | self.queue.pop(0)
64 | self.last_sent_time = current_time
65 | if len(self.queue) > 0:
66 | timer = threading.Timer(0.1, self.__send_message)
67 | timer.start()
68 |
--------------------------------------------------------------------------------
/helper/SharedDict.py:
--------------------------------------------------------------------------------
1 | from multiprocessing.managers import AcquirerProxy, BaseManager, DictProxy
2 |
3 |
4 | #https://stackoverflow.com/questions/57734298/how-can-i-provide-shared-state-to-my-flask-app-with-multiple-workers-without-dep
5 | def get_shared_state():
6 | shared_dict = {}
7 | manager = BaseManager(("127.0.0.1", 35791), b"pykakao")
8 | manager.register("get_dict", lambda: shared_dict, DictProxy)
9 | try:
10 | manager.get_server()
11 | manager.start()
12 | except OSError: # Address already in use
13 | manager.connect()
14 | return manager.get_dict()
--------------------------------------------------------------------------------
/kill_sendmsg.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Find the process ID (PID) of the SendMsg process.
4 | pid=$(ps aux | grep "SendMsg" | grep "app_process" | grep -v "sh -c" | awk '{print $2}')
5 |
6 | # Check if the PID is found.
7 | if [ -n "$pid" ]; then
8 | # Kill the process.
9 | sudo kill -9 "$pid"
10 | # Check if the kill command was successful.
11 | if [ $? -eq 0 ]; then
12 | echo "SendMsg(PID: $pid) is killed..."
13 | else
14 | echo "Failed to kill SendMsg(PID: $pid)."
15 | fi
16 | else
17 | echo "SendMsg is not running..."
18 | fi
19 |
--------------------------------------------------------------------------------
/observer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import time
4 | import json
5 | import base64
6 | import os
7 | from helper.ObserverHelper import ObserverHelper, get_config
8 | from helper.KakaoDB import KakaoDB
9 | import subprocess
10 |
11 | class Watcher(object):
12 | running = True
13 | refresh_delay_secs = 0.01
14 |
15 | def __init__(self, config,db):
16 | self._cached_stamp = 0
17 | self.db = db
18 | self.config = config
19 | self.watchfile = config["db_path"] + '/KakaoTalk.db-wal'
20 | self.helper = ObserverHelper(config)
21 |
22 | def look(self):
23 | stamp = os.stat(self.watchfile).st_mtime
24 | if stamp != self._cached_stamp:
25 | self._cached_stamp = stamp
26 | self.helper.check_change(self.db)
27 |
28 | def watch(self):
29 | while self.running:
30 | time.sleep(self.refresh_delay_secs)
31 | self.look()
32 |
33 | def main():
34 | db = KakaoDB()
35 | config = get_config()
36 | watcher = Watcher(config,db)
37 | watcher.watch()
38 |
39 | if __name__ == "__main__":
40 | main()
41 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
2 | pycryptodome
3 | requests
4 | gunicorn
5 | pillow
6 | psutil
7 |
--------------------------------------------------------------------------------
/setup1.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 1. Check and Install Docker
4 | if command -v docker &> /dev/null
5 | then
6 | echo "Docker already installed. Skipping docker install."
7 | else
8 | echo "Installing Docker..."
9 | sudo apt-get update
10 | sudo apt-get install ca-certificates curl -y
11 | sudo install -m 0755 -d /etc/apt/keyrings
12 | sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
13 | sudo chmod a+r /etc/apt/keyrings/docker.asc
14 |
15 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
16 | $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
17 | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
18 | sudo apt-get update
19 | sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
20 | echo "Docker installed successfully."
21 | fi
22 |
23 | # 2. Copy service files and set up services
24 | echo "Setting up chatbot and dbobserver services"
25 |
26 | # Define current user and working directory
27 | current_user=$(whoami)
28 | current_dir=$(pwd)
29 |
30 | # dbobserver.service
31 | cat < dbobserver.service
32 | [Unit]
33 | Description=DB Observer for chatbot
34 |
35 | [Service]
36 | User=$current_user
37 | WorkingDirectory=$current_dir
38 | ExecStart=$current_dir/venv/bin/python $current_dir/observer.py
39 | Restart=on-failure
40 | RestartSec=1s
41 |
42 | [Install]
43 | WantedBy=multi-user.target
44 | EOF
45 |
46 | sudo cp dbobserver.service /etc/systemd/system/dbobserver.service
47 | rm dbobserver.service
48 |
49 | # chatbot.service
50 | cat < chatbot.service
51 | [Unit]
52 | Description=chatbot www service
53 |
54 | [Service]
55 | User=$current_user
56 | WorkingDirectory=$current_dir
57 | ExecStart=$current_dir/venv/bin/gunicorn -b 0.0.0.0:5000 -w 9 app:app -t 100
58 | Restart=on-failure
59 | RestartSec=1s
60 |
61 | [Install]
62 | WantedBy=multi-user.target
63 | EOF
64 |
65 | sudo cp chatbot.service /etc/systemd/system/chatbot.service
66 | rm chatbot.service
67 |
68 |
69 | # Enable chatbot and dbobserver services (but do not start yet)
70 | sudo systemctl enable dbobserver.service
71 | sudo systemctl enable chatbot.service
72 |
73 | # 4. Set up binder drivers
74 | echo "Setting up binder drivers."
75 |
76 | # binder.service
77 | cat < binder.service
78 | [Unit]
79 | Description=Auto load binder
80 | After=network-online.target
81 |
82 | [Service]
83 | Type=oneshot
84 | ExecStart=/sbin/modprobe binder_linux devices="binder,hwbinder,vndbinder"
85 |
86 | [Install]
87 | WantedBy=multi-user.target
88 | EOF
89 |
90 | sudo cp binder.service /etc/systemd/system/binder.service
91 | rm binder.service
92 |
93 | sudo systemctl enable binder.service
94 | sudo systemctl start binder.service
95 |
96 | # 5. Create redoid docker container
97 | echo "Creating redoid docker container... It will take a few minutes."
98 | sudo docker run -itd --privileged --name redroid \
99 | -v ~/data:/data \
100 | -p 5555:5555 \
101 | -p 3000:3000 \
102 | redroid/redroid:11.0.0-latest \
103 | ro.product.model=SM-T970 \
104 | ro.product.brand=Samsung
105 |
106 | # 6. Final message
107 | echo "Redroid install finished. Install KakaoTalk inside, and then, run ./setup2.sh"
108 |
--------------------------------------------------------------------------------
/setup2.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 1. Install packages
4 | echo "Installing python3-venv, adb, sqlite3 package."
5 | sudo apt-get update
6 | sudo apt-get install python3-venv python3-pip adb sqlite3 -y
7 |
8 | # 2. Guess User ID
9 | echo "Guessing user_id of your bot."
10 | CURRENT_USERNAME=$(whoami)
11 | sudo chmod -R -c 777 ~/data/data/.
12 |
13 | echo "Trying to get BOT_ID from KakaoTalk2.db..."
14 | SQLITE_BOT_ID_OUTPUT=$(python3 guess_user_id.py)
15 | SQLITE_BOT_ID=$(echo "$SQLITE_BOT_ID_OUTPUT" | grep -oP '^\s*\K\d+' | head -n 1)
16 |
17 | if [ -z "$BOT_ID" ]; then
18 | echo "Error: Could not automatically guess BOT_ID. Please check guess_user_id.py output and set BOT_ID manually."
19 | $BOT_ID="YOUR_BOT_ID"
20 | else
21 | echo "Your bot's id seems $BOT_ID."
22 | fi
23 |
24 | # 3. Set Bot Config
25 | echo "Setting bot config..."
26 | CONFIG_JSON=$(cat < config.json
38 |
39 | # 4. Install requirements
40 | echo "Installing requirements."
41 | python3 -m venv venv
42 | venv/bin/python -m pip install pip --upgrade
43 | venv/bin/python -m pip install -r requirements.txt
44 | adb devices
45 | adb push SendMsg/SendMsg.dex /data/local/tmp/.
46 |
47 | # 5. Crontab job
48 | echo "Creating a crontab permission job"
49 | CRONTAB_LINE="* * * * * /bin/chmod -R -c 777 /home/$CURRENT_USERNAME/data/data/."
50 | (crontab -l 2>/dev/null; echo "$CRONTAB_LINE") | sudo crontab -
51 |
52 | # 6. Start services
53 | echo "Now starting PyKakaoDBBot..."
54 | sudo systemctl start chatbot
55 | sudo systemctl start dbobserver
56 |
57 | # 7. Check service status and finish
58 | sleep 5
59 | CHATBOT_STATUS=$(systemctl is-active chatbot)
60 | DBOBSERVER_STATUS=$(systemctl is-active dbobserver)
61 |
62 | if [ "$CHATBOT_STATUS" = "active" ] && [ "$DBOBSERVER_STATUS" = "active" ]; then
63 | echo "PyKakaoDBBot is running!"
64 | echo "Modify chatbot/Response.py for your codes, and restart chatbot service (sudo systemctl restart chatbot) to apply new codes"
65 | else
66 | echo "Error: PyKakaoDBBot services are not running. Please check service status using:"
67 | echo "sudo systemctl status chatbot"
68 | echo "sudo systemctl status dbobserver"
69 | fi
70 |
71 | exit 0
72 |
--------------------------------------------------------------------------------