├── 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 | --------------------------------------------------------------------------------