├── .jshintrc ├── LICENSE ├── README.md ├── enums └── emsg.js ├── index.js ├── lib ├── client │ └── crypto.js ├── crypto.js ├── definitions.js ├── nonce.js ├── packetreceiver.js └── server │ └── crypto.js ├── package.json ├── settings.json └── util ├── bytebuffer-sc.js └── usage.js /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "node": true, 4 | "esversion": 6 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | COC Proxy with the latest encryption! Cheers 2 | 3 | * Required patched apk using SCDevTeam patcher: https://github.com/SC-DevTeam/coc-patcher 4 | * Additional instructions on usage can be found here: https://github.com/royale-proxy/node-cr-proxy 5 | 6 | # TODO 7 | * Actually it logs decrypted payloads in hexadecimal. Would be cool to add the messages definitions into the project tree 8 | -------------------------------------------------------------------------------- /enums/emsg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @enum EMsg 3 | */ 4 | 5 | module.exports = { 6 | // Client Messages 7 | 'ClientHello': 10100, 8 | 'Login': 10101, 9 | 'ClientCapabilities': 10107, 10 | 'KeepAlive': 10108, 11 | 'AuthenticationCheck': 10112, 12 | 'SetDeviceToken': 10113, 13 | 'ResetAccount': 10116, 14 | 'ReportUser': 10117, 15 | 'AccountSwitched': 10118, 16 | 'UnlockAccount': 10121, 17 | 'AppleBillingRequest': 10150, 18 | 'GoogleBillingRequest': 10151, 19 | 'KunlunBillingRequest': 10159, 20 | 'ChangeAvatarName': 10212, 21 | 'AskForPlayingGamecenterFriends': 10512, 22 | 'AskForPlayingFacebookFriends': 10513, 23 | 'InboxOpened': 10905, 24 | 'UnbindFacebookAccount': 12211, 25 | 'RequestSectorState': 12903, 26 | 'SectorCommand': 12904, 27 | 'GetCurrentBattleReplayData': 12905, 28 | 'SendBattleEvent': 12951, 29 | 'GoHome': 14101, 30 | 'EndClientTurn': 14102, 31 | 'StartMission': 14104, 32 | 'HomeLogicStopped': 14105, 33 | 'CancelMatchmake': 14107, 34 | 'ChangeHomeName': 14108, 35 | 'VisitHome': 14113, 36 | 'HomeBattleReplay': 14114, 37 | 'HomeBattleReplayViewed': 14117, 38 | 'AcceptChallenge': 14120, 39 | 'CancelChallengeMessage': 14123, 40 | 'BindFacebookAccount': 14201, 41 | 'BindGamecenterAccount': 14212, 42 | 'BindGoogleServiceAccount': 14262, 43 | 'CreateAlliance': 14301, 44 | 'AskForAllianceData': 14302, 45 | 'AskForJoinableAlliancesList': 14303, 46 | 'AskForAllianceStream': 14304, 47 | 'JoinAlliance': 14305, 48 | 'ChangeAllianceMemberRole': 14306, 49 | 'KickAllianceMember': 14307, 50 | 'LeaveAlliance': 14308, 51 | 'DonateAllianceUnit': 14310, 52 | 'ChatToAllianceStream': 14315, 53 | 'ChangeAllianceSettings': 14316, 54 | 'RequestJoinAlliance': 14317, 55 | 'SelectSpellsFromCoOpen': 14318, 56 | 'OfferChestForCoOpen': 14319, 57 | 'RespondToAllianceJoinRequest': 14321, 58 | 'SendAllianceInvitation': 14322, 59 | 'JoinAllianceUsingInvitation': 14323, 60 | 'SearchAlliances': 14324, 61 | 'SendAllianceMail': 14330, 62 | 'AskForAllianceRankingList': 14401, 63 | 'AskForTVContent': 14402, 64 | 'AskForAvatarRankingList': 14403, 65 | 'AskForAvatarLocalRanking': 14404, 66 | 'AskForAvatarStream': 14405, 67 | 'AskForBattleReplayStream': 14406, 68 | 'AskForLastAvatarTournamentResults': 14408, 69 | 'RemoveAvatarStreamEntry': 14418, 70 | 'AvatarNameCheckRequest': 14600, 71 | 'LogicDeviceLinkCodeStatus': 16000, 72 | 'AskForJoinableTournaments': 16103, 73 | 'SearchTournaments': 16113, 74 | 75 | // Server Messages 76 | 'ServerHello': 20100, 77 | 'LoginFailed': 20103, 78 | 'LoginOk': 20104, 79 | 'FriendList': 20105, 80 | 'KeepAliveOk': 20108, 81 | 'ChatAccountBanStatus': 20118, 82 | 'BillingRequestFailed': 20121, 83 | 'UnlockAccountOk': 20132, 84 | 'UnlockAccountFailed': 20133, 85 | 'AppleBillingProcessedByServer': 20151, 86 | 'GoogleBillingProcessedByServer': 20152, 87 | 'KunlunBillingProcessedByServer': 20156, 88 | 'ShutdownStarted': 20161, 89 | 'AvatarNameChangeFailed': 20205, 90 | 'AvatarInGameStatusUpdated': 20206, 91 | 'AllianceOnlineStatusUpdated': 20207, 92 | 'BattleResult': 20225, 93 | 'AvatarNameCheckResponse': 20300, 94 | 'OpponentLeftMatchNotification': 20801, 95 | 'OpponentRejoinsMatchNotification': 20802, 96 | 'SectorHearbeat': 21902, 97 | 'SectorState': 21903, 98 | 'BattleEvent': 22952, 99 | 'PvpMatchmakeNotification': 22957, 100 | 'OwnHomeData': 24101, 101 | 'OwnAvatarData': 24102, 102 | 'OutOfSync': 24104, 103 | 'StopHomeLogic': 24106, 104 | 'MatchmakeInfo': 24107, 105 | 'MatchmakeFailed': 24108, 106 | 'AvailableServerCommand': 24111, 107 | 'UdpConnectionInfo': 24112, 108 | 'VisitedHomeData': 24113, 109 | 'HomeBattleReplayData': 24114, 110 | 'ServerError': 24115, 111 | 'HomeBattleReplayFailed': 24116, 112 | 'ChallengeFailed': 24121, 113 | 'CancelChallengeDone': 24124, 114 | 'CancelMatchmakeDone': 24125, 115 | 'FacebookAccountBound': 24201, 116 | 'FacebookAccountAlreadyBound': 24202, 117 | 'GamecenterAccountAlreadyBound': 24212, 118 | 'FacebookAccountUnbound': 24213, 119 | 'GoogleServiceAccountBound': 24261, 120 | 'GoogleServiceAccountAlreadyBound': 24262, 121 | 'AllianceData': 24301, 122 | 'AllianceJoinFailed': 24302, 123 | 'AllianceJoinOk': 24303, 124 | 'JoinableAllianceList': 24304, 125 | 'AllianceLeaveOk': 24305, 126 | 'ChangeAllianceMemberRoleOk': 24306, 127 | 'KickAllianceMemberOk': 24307, 128 | 'AllianceMember': 24308, 129 | 'AllianceMemberRemoved': 24309, 130 | 'AllianceList': 24310, 131 | 'AllianceStream': 24311, 132 | 'AllianceStreamEntry': 24312, 133 | 'AllianceStreamEntryRemoved': 24318, 134 | 'AllianceJoinRequestOk': 24319, 135 | 'AllianceJoinRequestFailed': 24320, 136 | 'AllianceInvitationSendFailed': 24321, 137 | 'AllianceInvitationSentOk': 24322, 138 | 'AllianceFullEntryUpdate': 24324, 139 | 'AllianceCreateFailed': 24332, 140 | 'AllianceChangeFailed': 24333, 141 | 'AllianceRankingList': 24401, 142 | 'AllianceLocalRankingList': 24402, 143 | 'AvatarRankingList': 24403, 144 | 'AvatarLocalRankingList': 24404, 145 | 'RoyalTVContent': 24405, 146 | 'LastAvatarTournamentResults': 24407, 147 | 'AvatarStream': 24411, 148 | 'AvatarStreamEntry': 24412, 149 | 'BattleReportStream': 24413, 150 | 'AvatarStreamEntryRemoved': 24418, 151 | 'InboxList': 24445, 152 | 'InboxGlobal': 24446, 153 | 'InboxCount': 24447, 154 | 'Disconnected': 25892, 155 | 'LogicDeviceLinkCodeResponse': 26002, 156 | 'LogicDeviceLinkNewDeviceLinked': 26003, 157 | 'LogicDeviceLinkCodeDeactivated': 26004, 158 | 'LogicDeviceLinkResponse': 26005, 159 | 'LogicDeviceLinkDone': 26007, 160 | 'LogicDeviceLinkError': 26008, 161 | 162 | // Value-to-name mapping for convenience 163 | 164 | // Client Messages 165 | '10100': 'ClientHello', 166 | '10101': 'Login', 167 | '10107': 'ClientCapabilities', 168 | '10108': 'KeepAlive', 169 | '10112': 'AuthenticationCheck', 170 | '10113': 'SetDeviceToken', 171 | '10116': 'ResetAccount', 172 | '10117': 'ReportUser', 173 | '10118': 'AccountSwitched', 174 | '10121': 'UnlockAccount', 175 | '10150': 'AppleBillingRequest', 176 | '10151': 'GoogleBillingRequest', 177 | '10159': 'KunlunBillingRequest', 178 | '10212': 'ChangeAvatarName', 179 | '10512': 'AskForPlayingGamecenterFriends', 180 | '10513': 'AskForPlayingFacebookFriends', 181 | '10905': 'InboxOpened', 182 | '12211': 'UnbindFacebookAccount', 183 | '12903': 'RequestSectorState', 184 | '12904': 'SectorCommand', 185 | '12905': 'GetCurrentBattleReplayData', 186 | '12951': 'SendBattleEvent', 187 | '14101': 'GoHome', 188 | '14102': 'EndClientTurn', 189 | '14104': 'StartMission', 190 | '14105': 'HomeLogicStopped', 191 | '14107': 'CancelMatchmake', 192 | '14108': 'ChangeHomeName', 193 | '14113': 'VisitHome', 194 | '14114': 'HomeBattleReplay', 195 | '14117': 'HomeBattleReplayViewed', 196 | '14120': 'AcceptChallenge', 197 | '14123': 'CancelChallengeMessage', 198 | '14201': 'BindFacebookAccount', 199 | '14212': 'BindGamecenterAccount', 200 | '14262': 'BindGoogleServiceAccount', 201 | '14301': 'CreateAlliance', 202 | '14302': 'AskForAllianceData', 203 | '14303': 'AskForJoinableAlliancesList', 204 | '14304': 'AskForAllianceStream', 205 | '14305': 'JoinAlliance', 206 | '14306': 'ChangeAllianceMemberRole', 207 | '14307': 'KickAllianceMember', 208 | '14308': 'LeaveAlliance', 209 | '14310': 'DonateAllianceUnit', 210 | '14315': 'ChatToAllianceStream', 211 | '14316': 'ChangeAllianceSettings', 212 | '14317': 'RequestJoinAlliance', 213 | '14318': 'SelectSpellsFromCoOpen', 214 | '14319': 'OfferChestForCoOpen', 215 | '14321': 'RespondToAllianceJoinRequest', 216 | '14322': 'SendAllianceInvitation', 217 | '14323': 'JoinAllianceUsingInvitation', 218 | '14324': 'SearchAlliances', 219 | '14330': 'SendAllianceMail', 220 | '14401': 'AskForAllianceRankingList', 221 | '14402': 'AskForTVContent', 222 | '14403': 'AskForAvatarRankingList', 223 | '14404': 'AskForAvatarLocalRanking', 224 | '14405': 'AskForAvatarStream', 225 | '14406': 'AskForBattleReplayStream', 226 | '14408': 'AskForLastAvatarTournamentResults', 227 | '14418': 'RemoveAvatarStreamEntry', 228 | '14600': 'AvatarNameCheckRequest', 229 | '16000': 'LogicDeviceLinkCodeStatus', 230 | '16103': 'AskForJoinableTournaments', 231 | '16113': 'SearchTournaments', 232 | 233 | // Server Messages 234 | '20100': 'ServerHello', 235 | '20103': 'LoginFailed', 236 | '20104': 'LoginOk', 237 | '20105': 'FriendList', 238 | '20108': 'KeepAliveServer', 239 | '20118': 'ChatAccountBanStatus', 240 | '20121': 'BillingRequestFailed', 241 | '20132': 'UnlockAccountOk', 242 | '20133': 'UnlockAccountFailed', 243 | '20151': 'AppleBillingProcessedByServer', 244 | '20152': 'GoogleBillingProcessedByServer', 245 | '20156': 'KunlunBillingProcessedByServer', 246 | '20161': 'ShutdownStarted', 247 | '20205': 'AvatarNameChangeFailed', 248 | '20206': 'AvatarInGameStatusUpdated', 249 | '20207': 'AllianceOnlineStatusUpdated', 250 | '20225': 'BattleResult', 251 | '20300': 'AvatarNameCheckResponse', 252 | '20801': 'OpponentLeftMatchNotification', 253 | '20802': 'OpponentRejoinsMatchNotification', 254 | '21902': 'SectorHearbeat', 255 | '21903': 'SectorState', 256 | '22952': 'BattleEvent', 257 | '22957': 'PvpMatchmakeNotification', 258 | '24101': 'OwnHomeData', 259 | '24102': 'OwnAvatarData', 260 | '24104': 'OutOfSync', 261 | '24106': 'StopHomeLogic', 262 | '24107': 'MatchmakeInfo', 263 | '24108': 'MatchmakeFailed', 264 | '24111': 'AvailableServerCommand', 265 | '24112': 'UdpConnectionInfo', 266 | '24113': 'VisitedHomeData', 267 | '24114': 'HomeBattleReplay', 268 | '24115': 'ServerError', 269 | '24116': 'HomeBattleReplayFailed', 270 | '24121': 'ChallengeFailed', 271 | '24124': 'CancelChallengeDone', 272 | '24125': 'CancelMatchmakeDone', 273 | '24201': 'FacebookAccountBound', 274 | '24202': 'FacebookAccountAlreadyBound', 275 | '24212': 'GamecenterAccountAlreadyBound', 276 | '24213': 'FacebookAccountUnbound', 277 | '24261': 'GoogleServiceAccountBound', 278 | '24262': 'GoogleServiceAccountAlreadyBound', 279 | '24301': 'AllianceData', 280 | '24302': 'AllianceJoinFailed', 281 | '24303': 'AllianceJoinOk', 282 | '24304': 'JoinableAllianceList', 283 | '24305': 'AllianceLeaveOk', 284 | '24306': 'ChangeAllianceMemberRoleOk', 285 | '24307': 'KickAllianceMemberOk', 286 | '24308': 'AllianceMember', 287 | '24309': 'AllianceMemberRemoved', 288 | '24310': 'AllianceList', 289 | '24311': 'AllianceStream', 290 | '24312': 'AllianceStreamEntry', 291 | '24318': 'AllianceStreamEntryRemoved', 292 | '24319': 'AllianceJoinRequestOk', 293 | '24320': 'AllianceJoinRequestFailed', 294 | '24321': 'AllianceInvitationSendFailed', 295 | '24322': 'AllianceInvitationSentOk', 296 | '24324': 'AllianceFullEntryUpdate', 297 | '24332': 'AllianceCreateFailed', 298 | '24333': 'AllianceChangeFailed', 299 | '24401': 'AllianceRankingList', 300 | '24402': 'AllianceLocalRankingList', 301 | '24403': 'AvatarRankingList', 302 | '24404': 'AvatarLocalRankingList', 303 | '24405': 'RoyalTVContent', 304 | '24407': 'LastAvatarTournamentResults', 305 | '24411': 'AvatarStream', 306 | '24412': 'AvatarStreamEntry', 307 | '24413': 'BattleReportStream', 308 | '24418': 'AvatarStreamEntryRemoved', 309 | '24445': 'InboxList', 310 | '24446': 'InboxGlobal', 311 | '24447': 'InboxCount', 312 | '25892': 'Disconnected', 313 | '26002': 'LogicDeviceLinkCodeResponse', 314 | '26003': 'LogicDeviceLinkNewDeviceLinked', 315 | '26004': 'LogicDeviceLinkCodeDeactivated', 316 | '26005': 'LogicDeviceLinkResponse', 317 | '26007': 'LogicDeviceLinkDone', 318 | '26008': 'LogicDeviceLinkError' 319 | }; 320 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var net = require('net'); 4 | var fs = require('fs'); 5 | var util = require('util'); 6 | var path = require('path'); 7 | var jsome = require('jsome'); 8 | var options = require('./util/usage').options; 9 | var settings = require('./settings.json'); 10 | 11 | require('console-stamp')(console, 'yyyy-mm-dd HH:MM:ss'); 12 | 13 | var PacketReceiver = require('./lib/packetreceiver'); 14 | var ClientCrypto = require('./lib/client/crypto'); 15 | var ServerCrypto = require('./lib/server/crypto'); 16 | var Definitions = require('./lib/definitions'); 17 | var EMsg = require('./enums/emsg'); 18 | 19 | var definitions = new Definitions(options); 20 | var clients = {}; 21 | 22 | const banner = new Buffer("0d0a205f5f5f5f5f205f5f5f5f5f5f5f5f5f5f5f2020202020202020205f5f5f5f5f20202020202020202020202020202020202020200d0a2f20205f5f5f2f20205f5f205c20205f20205c202020202020207c5f2020205f7c202020202020202020202020202020202020200d0a5c20602d2d2e7c202f20205c2f207c207c207c5f5f5f5f5f2020205f7c207c205f5f5f20205f5f205f205f205f5f205f5f5f20200d0a20602d2d2e205c207c2020207c207c207c202f205f205c205c202f202f207c2f205f205c2f205f60207c20275f2060205f205c200d0a2f5c5f5f2f202f205c5f5f2f5c207c2f202f20205f5f2f5c2056202f7c207c20205f5f2f20285f7c207c207c207c207c207c207c0d0a5c5f5f5f5f2f205c5f5f5f5f2f5f5f5f2f205c5f5f5f7c205c5f2f205c5f2f5c5f5f5f7c5c5f5f2c5f7c5f7c207c5f7c207c5f7c0d0a202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200d0a202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200d0a202020205f5f5f5f5f205f5f5f5f5f205f5f5f5f5f205f5f5f5f5f5f2020202020202020202020202020202020202020202020200d0a2020202f20205f5f205c20205f20202f20205f5f205c7c205f5f5f205c20202020202020202020202020202020202020202020200d0a2020207c202f20205c2f207c207c207c202f20205c2f7c207c5f2f202f205f5f205f5f5f5f5f20205f5f5f2020205f20202020200d0a2020207c207c2020207c207c207c207c207c202020207c20205f5f2f20275f5f2f205f205c205c2f202f207c207c207c202020200d0a2020207c205c5f5f2f5c205c5f2f202f205c5f5f2f5c7c207c20207c207c207c20285f29203e20203c7c207c5f7c207c202020200d0a202020205c5f5f5f5f2f5c5f5f5f2f205c5f5f5f5f2f5c5f7c20207c5f7c20205c5f5f5f2f5f2f5c5f5c5c5f5f2c207c202020200d0a202020202020202020202020202020202020202020202020202020202020202020202020202020202020205f5f2f207c202020200d0a2020202020202020202020202020202020202020202020202020202020202020202020202020202020207c5f5f5f2f2020202020", "hex"); 23 | 24 | if(options.replay) { 25 | fs.readFile(options.replay.filename, {encoding: "binary"}, function(err, contents) { 26 | if(err) { 27 | return console.error(err); 28 | } 29 | var message = { 30 | messageType: parseInt(path.basename(options.replay.filename, ".bin")), 31 | decrypted: contents 32 | }; 33 | 34 | definitions.decode(message); 35 | if(message.decoded) { 36 | jsome(message.decoded); 37 | } 38 | }); 39 | } else { 40 | var server = net.createServer(); 41 | 42 | server.on('error', function(err) { 43 | if (err.code == 'EADDRINUSE') { 44 | console.log('Address in use, exiting...'); 45 | } else { 46 | console.log('Unknown error setting up proxy: ' + err); 47 | } 48 | 49 | process.exit(1); 50 | }); 51 | 52 | server.on('listening', function() { 53 | console.log('listening on ' + server.address().address + ':' + server.address().port); 54 | console.log(banner.toString("utf8")); 55 | }); 56 | 57 | server.on('connection', function(socket) { 58 | var gameserver = new net.Socket(); 59 | socket.key = socket.remoteAddress + ":" + socket.remotePort; 60 | clients[socket.key] = socket; 61 | 62 | var clientPacketReceiver = new PacketReceiver(); 63 | var serverPacketReceiver = new PacketReceiver(); 64 | 65 | var clientCrypto = new ClientCrypto(settings); 66 | var serverCrypto = new ServerCrypto(settings); 67 | 68 | clientCrypto.setServer(serverCrypto); 69 | serverCrypto.setClient(clientCrypto); 70 | 71 | console.log('new client ' + socket.key + ' connected, establishing connection to game server'); 72 | 73 | gameserver.connect(9339, "gamea.clashofclans.com", function() { 74 | console.log('Connected to game server on ' + gameserver.remoteAddress + ':' + gameserver.remotePort); 75 | }); 76 | 77 | gameserver.on("data", function(chunk) { 78 | serverPacketReceiver.packetize(chunk, function(packet) { 79 | var message = { 80 | 'messageType': packet.readUInt16BE(0), 81 | 'length': packet.readUIntBE(2, 3), 82 | 'version': packet.readUInt16BE(5), 83 | 'payload': packet.slice(7, packet.length) 84 | }; 85 | 86 | console.log('[SERVER] ' + (EMsg[message.messageType] ? EMsg[message.messageType] + ' [' + message.messageType + ']' : message.messageType)); 87 | 88 | clientCrypto.decryptPacket(message); 89 | 90 | console.log("[SERVER DECRYPTED]: " + new Buffer(message.decrypted).toString('hex')); 91 | 92 | if(options.dump) { 93 | fs.writeFile(options.dump.filename + "/" + message.messageType + ".bin", Buffer.from(message.decrypted), {encoding: "binary"}, function(err) { 94 | if(err) { 95 | console.error(err); 96 | } 97 | }); 98 | } 99 | 100 | definitions.decode(message); 101 | 102 | if(options.verbose && message.decoded && Object.keys(message.decoded).length) { 103 | jsome(message.decoded); 104 | } 105 | 106 | serverCrypto.encryptPacket(message); 107 | 108 | var header = Buffer.alloc(7); 109 | 110 | header.writeUInt16BE(message.messageType, 0); 111 | header.writeUIntBE(message.encrypted.length, 2, 3); 112 | header.writeUInt16BE(message.version, 5); 113 | 114 | clients[socket.key].write(Buffer.concat([header, Buffer.from(message.encrypted)])); 115 | }); 116 | }); 117 | 118 | gameserver.on("end", function() { 119 | console.log('Disconnected from game server'); 120 | }); 121 | 122 | clients[socket.key].on('data', function(chunk) { 123 | clientPacketReceiver.packetize(chunk, function(packet) { 124 | var message = { 125 | 'messageType': packet.readUInt16BE(0), 126 | 'length': packet.readUIntBE(2, 3), 127 | 'version': packet.readUInt16BE(5), 128 | 'payload': packet.slice(7, packet.length) 129 | }; 130 | 131 | console.log('[CLIENT] ' + (EMsg[message.messageType] ? EMsg[message.messageType] + ' [' + message.messageType + ']' : message.messageType)); 132 | 133 | serverCrypto.decryptPacket(message); 134 | 135 | if(options.dump) { 136 | fs.writeFile(options.dump.filename + "/" + message.messageType + ".bin", Buffer.from(message.decrypted), {encoding: "binary"}, function(err) { 137 | if(err) { 138 | return console.log(err); 139 | } 140 | }); 141 | } 142 | 143 | //definitions.decode(message); 144 | 145 | if(options.verbose && message.decoded && Object.keys(message.decoded).length) { 146 | //jsome(message.decoded); 147 | } 148 | 149 | console.log('[CLIENT DECRYPTED]: ' + new Buffer(message.decrypted).toString('hex')); 150 | 151 | clientCrypto.encryptPacket(message); 152 | 153 | var header = Buffer.alloc(7); 154 | 155 | header.writeUInt16BE(message.messageType, 0); 156 | header.writeUIntBE(message.encrypted.length, 2, 3); 157 | header.writeUInt16BE(message.version, 5); 158 | 159 | gameserver.write(Buffer.concat([header, Buffer.from(message.encrypted)])); 160 | }); 161 | }); 162 | 163 | clients[socket.key].on('end', function() { 164 | console.log('Client ' + socket.key + ' disconnected from proxy.'); 165 | delete clients[socket.key]; 166 | gameserver.end(); 167 | }); 168 | }); 169 | 170 | server.listen({ host: '0.0.0.0', port: 9339, exclusive: true }, function(err) { 171 | if (err) { 172 | console.log(err); 173 | } 174 | }); 175 | } 176 | -------------------------------------------------------------------------------- /lib/client/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var nacl = require("tweetnacl"); 4 | var blake2 = require("blake2"); 5 | var ByteBuffer = require("../../util/bytebuffer-sc"); 6 | var EMsg = require('../../enums/emsg'); 7 | const Crypto = require('../crypto.js'); 8 | const Nonce = require('../nonce.js'); 9 | 10 | class ClientCrypto extends Crypto { 11 | 12 | constructor(settings) { 13 | super(); 14 | var kp = nacl.box.keyPair(); 15 | 16 | this.privateKey = Buffer.from(kp.secretKey); 17 | this.clientKey = Buffer.from(kp.publicKey); 18 | this.serverKey = new Buffer(settings.serverKey, "hex"); 19 | this.beforeNm(this.serverKey); 20 | this.setEncryptNonce(); 21 | } 22 | 23 | setServer(server) { 24 | this.server = server; 25 | } 26 | 27 | decryptPacket(message) { 28 | if (message.messageType == EMsg.ServerHello || (message.messageType == EMsg.LoginFailed && !this.getSessionKey())) { 29 | var len = message.payload.readInt32BE(); 30 | this.setSessionKey(message.payload.slice(4, 4 + len)); 31 | message.decrypted = message.payload; 32 | } else if (message.messageType == EMsg.LoginOk || message.messageType == EMsg.LoginFailed) { 33 | var decrypted; 34 | var nonce = new Nonce({ clientKey: this.clientKey, serverKey: this.serverKey, nonce: this.encryptNonce }); 35 | 36 | message.decrypted = this.decrypt(message.payload, nonce); 37 | 38 | if (message.decrypted) { 39 | this.setDecryptNonce(Buffer.from(message.decrypted.slice(0, 24))); 40 | this.server.setEncryptNonce(Buffer.from(message.decrypted.slice(0, 24))); 41 | this.setSharedKey(Buffer.from(message.decrypted.slice(24, 56))); 42 | 43 | message.decrypted = message.decrypted.slice(56); 44 | } 45 | } else { 46 | message.decrypted = this.decrypt(message.payload); 47 | } 48 | } 49 | 50 | encryptPacket(message) { 51 | if (message.messageType == EMsg.ClientHello) { 52 | message.encrypted = message.decrypted; 53 | } else if (message.messageType == EMsg.Login) { 54 | var nonce = new Nonce({ clientKey: this.clientKey, serverKey: this.serverKey }); 55 | var toEncrypt = Buffer.concat([this.getSessionKey(), this.encryptNonce.getBuffer(), Buffer.from(message.decrypted)]); 56 | 57 | message.encrypted = Buffer.concat([this.clientKey, Buffer.from(this.encrypt(toEncrypt, nonce))]); 58 | } else { 59 | message.encrypted = this.encrypt(message.decrypted); 60 | } 61 | } 62 | } 63 | 64 | module.exports = ClientCrypto; 65 | -------------------------------------------------------------------------------- /lib/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var nacl = require("tweetnacl"); 4 | var blake2 = require("blake2"); 5 | var ByteBuffer = require("../util/bytebuffer-sc"); 6 | const Nonce = require("./nonce"); 7 | 8 | class Crypto { 9 | constructor(settings) { 10 | this.privateKey = null; 11 | this.serverKey = null; 12 | this.clientKey = null; 13 | this.sharedKey = null; 14 | this.decryptNonce = null; 15 | this.encryptNonce = null; 16 | this.sessionKey = null; 17 | } 18 | 19 | getSharedKey() { 20 | return this.sharedKey; 21 | } 22 | 23 | setSharedKey(sharedKey) { 24 | this.sharedKey = sharedKey; 25 | } 26 | 27 | getEncryptNonce() { 28 | return this.encryptNonce; 29 | } 30 | 31 | setEncryptNonce(nonce) { 32 | this.encryptNonce = new Nonce({ nonce: nonce }); 33 | } 34 | 35 | getDecryptNonce() { 36 | return this.decryptNonce; 37 | } 38 | 39 | setDecryptNonce(nonce) { 40 | this.decryptNonce = new Nonce({ nonce: nonce }); 41 | } 42 | 43 | setSessionKey(sessionKey) { 44 | this.sessionKey = sessionKey; 45 | } 46 | 47 | getSessionKey() { 48 | return this.sessionKey; 49 | } 50 | 51 | beforeNm(publicKey) { 52 | this.sharedKey = new Buffer(nacl.box.before(publicKey, this.privateKey)); 53 | } 54 | 55 | encrypt(message, nonce) { 56 | if (!nonce) { 57 | this.encryptNonce.increment(); 58 | nonce = this.encryptNonce; 59 | } 60 | 61 | return nacl.box.after(message, nonce.getBuffer(), this.sharedKey); 62 | } 63 | 64 | decrypt(cipherText, nonce) { 65 | var decrypted; 66 | if (!nonce) { 67 | this.decryptNonce.increment(); 68 | nonce = this.decryptNonce; 69 | } 70 | 71 | decrypted = nacl.box.open.after(cipherText, nonce.getBuffer(), this.sharedKey); 72 | 73 | if (decrypted) { 74 | return decrypted; 75 | } else { 76 | console.log('unable to decrypt message. exiting.'); 77 | process.exit(1); 78 | } 79 | } 80 | } 81 | 82 | module.exports = Crypto; 83 | -------------------------------------------------------------------------------- /lib/definitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const zlib = require('zlib'); 5 | 6 | var Long = require("long"); 7 | var ByteBuffer = require("../util/bytebuffer-sc"); 8 | var EMsg = require('../enums/emsg'); 9 | 10 | class Definitions { 11 | 12 | constructor(options) { 13 | var self = this; 14 | 15 | self.definitions = []; 16 | self.components = []; 17 | self.options = options; 18 | 19 | ['client', 'server', 'component'].forEach(function(folder) { 20 | fs.readdir('./node_modules/coc-messages/' + folder, (err, files) => { 21 | if (err) { 22 | console.log('error opening node-modules/coc-messages/' + folder + ': ' + err); 23 | process.exit(1); 24 | } 25 | 26 | files.forEach(file => { 27 | var json = JSON.parse(fs.readFileSync('./node_modules/coc-messages/' + folder + '/' + file, 'utf8')); 28 | 29 | if (json.id) { 30 | self.definitions[json.id] = json; 31 | } else { 32 | self.components[json.name] = json; 33 | 34 | if (json.extensions) { 35 | var extensions = []; 36 | 37 | for (var key in json.extensions) { 38 | extensions[json.extensions[key].id] = json.extensions[key]; 39 | } 40 | 41 | self.components[json.name].extensions = extensions; 42 | } 43 | } 44 | }); 45 | }); 46 | }); 47 | } 48 | 49 | decode_fields(reader, fields) { 50 | var unknown = 0; 51 | var decoded = {}; 52 | 53 | fields.forEach((field, index) => { 54 | var fieldType = field.type.substring(0); // creates a clone without reference 55 | 56 | if (!field.name) { 57 | field.name = "unknown_" + index; 58 | } 59 | 60 | if (fieldType.includes('?')) { 61 | if (Boolean(reader.readByte())) { 62 | fieldType = fieldType.substring(1); 63 | } else { 64 | reader.offset--; // we only peeked, multiple bools can be mixed together 65 | decoded[field.name] = false; 66 | return; 67 | } 68 | } 69 | 70 | if (fieldType.includes('[')) { 71 | var n = fieldType.substring(fieldType.indexOf('[') + 1, fieldType.indexOf(']')); 72 | fieldType = fieldType.substring(0, fieldType.indexOf('[')); 73 | 74 | // if n is specified, then we use it, otherwise we need to read how big the array is 75 | // may need to implement lenghtType, but seems unecessary, they are all RRSINT32 afaik 76 | if (n === '') { 77 | if(field.lengthType && field.lengthType == 'INT') { 78 | n = reader.readInt32(); 79 | } else { 80 | n = reader.readRrsInt32(); 81 | } 82 | } else { 83 | n = parseInt(n); 84 | } 85 | 86 | decoded[field.name] = []; 87 | 88 | for (var i = 0; i < n; i++) { 89 | decoded[field.name][i] = this.decode_field(reader, fieldType, field); 90 | } 91 | } else { 92 | decoded[field.name] = this.decode_field(reader, fieldType, field); 93 | } 94 | }); 95 | 96 | return decoded; 97 | } 98 | 99 | decode_field(reader, fieldType, field) { 100 | var decoded; 101 | 102 | if (fieldType == 'BYTE') { 103 | decoded = reader.readByte(); 104 | } else if (fieldType == 'SHORT') { 105 | decoded = reader.readInt16(); 106 | } else if (fieldType == 'BOOLEAN'){ 107 | decoded = Boolean(reader.readByte()); 108 | } else if (fieldType == 'INT') { 109 | decoded = reader.readInt32(); 110 | } else if (fieldType == 'INT32') { 111 | decoded = reader.readVarint32(); 112 | } else if (fieldType == 'RRSINT32') { 113 | decoded = reader.readRrsInt32(); 114 | } else if (fieldType == 'RRSLONG') { 115 | decoded = Long.fromValue({high: reader.readRrsInt32(), low: reader.readRrsInt32(), unsigned: false}); 116 | } else if (fieldType == 'LONG') { 117 | decoded = reader.readInt64(); 118 | } else if (fieldType == 'STRING') { 119 | decoded = reader.readIString(); 120 | } else if (fieldType == 'BITSET') { 121 | var bits = reader.readByte(); 122 | 123 | decoded = [ 124 | !!(bits & 0x01), 125 | !!(bits & 0x02), 126 | !!(bits & 0x04), 127 | !!(bits & 0x08), 128 | !!(bits & 0x10), 129 | !!(bits & 0x20), 130 | !!(bits & 0x40), 131 | !!(bits & 0x80) 132 | ]; 133 | 134 | if(field.bit) { 135 | decoded = decoded[field.bit]; 136 | } 137 | 138 | if(field.peek === true) { 139 | reader.offset--; 140 | } 141 | } else if (fieldType == 'SCID') { 142 | var hi = reader.readRrsInt32(); 143 | var lo; 144 | if(hi) { 145 | lo = reader.readRrsInt32(); 146 | decoded = hi * 1000000 + lo; 147 | } else { 148 | decoded = 0; 149 | } 150 | } else if (fieldType == 'ZIP_STRING') { 151 | var len = reader.readInt32() - 4; // it's prefixed with a INT32 of the unzipped length 152 | 153 | reader.LE(); // switch to little endian 154 | var zlength = reader.readInt32(); 155 | reader.BE(); // switch back to big endian 156 | 157 | if(reader.remaining() >= len) { 158 | decoded = zlib.unzipSync(reader.slice(reader.offset, reader.offset + len).toBuffer()).toString(); 159 | reader.offset = reader.offset + len; 160 | } else { 161 | decoded = false; 162 | console.log('Insufficient data to unzip field.'); 163 | } 164 | } else if (fieldType == 'IGNORE') { 165 | decoded = reader.remaining() + ' bytes have been ignored.'; 166 | reader.offset = reader.limit; 167 | } else if (this.components[fieldType]) { 168 | decoded = this.decode_fields(reader, this.components[fieldType].fields); 169 | if (this.components[fieldType].extensions !== undefined) { 170 | 171 | if (decoded.id !== undefined) { 172 | var extensionDef = this.components[fieldType].extensions.find(function(extension) { 173 | if (extension) { 174 | return extension.id == decoded.id; 175 | } else { 176 | return 0; 177 | } 178 | }); 179 | 180 | if (extensionDef) { 181 | decoded.payload = this.decode_fields(reader, extensionDef.fields); 182 | } else { 183 | console.warn('Error: Extensions of field type ' + fieldType + ' with id ' + decoded.id + ' is missing. (' + field.name + ').'); 184 | return false; 185 | } 186 | } else { 187 | console.warn('Warning: missing id for component ' + fieldType + ' (' + field.name + ').'); 188 | return false; 189 | } 190 | } 191 | } else { 192 | console.error('Error: field type ' + fieldType + ' does not exist. (' + field.name + '). Exiting.'); 193 | process.exit(1); 194 | } 195 | 196 | return decoded; 197 | } 198 | 199 | decode(message) { 200 | var reader = ByteBuffer.fromBinary(message.decrypted); 201 | 202 | if (this.definitions[message.messageType]) { 203 | message.decoded = {}; 204 | 205 | if (this.definitions[message.messageType].fields && this.definitions[message.messageType].fields.length) { 206 | message.decoded = this.decode_fields(reader, this.definitions[message.messageType].fields); 207 | } 208 | 209 | if (reader.remaining() && this.options.verbose) { 210 | console.warn(reader.remaining() + ' bytes remaining...'); 211 | //reader.printDebug(); 212 | } 213 | } else { 214 | console.warn('Missing definition for ' + (EMsg[message.messageType] ? EMsg[message.messageType] : message.messageType)); 215 | if(this.options.verbose) { 216 | //reader.printDebug(); 217 | } 218 | } 219 | } 220 | } 221 | 222 | module.exports = Definitions; 223 | -------------------------------------------------------------------------------- /lib/nonce.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var blake2 = require("blake2"); 4 | var nacl = require("tweetnacl"); 5 | 6 | class Nonce { 7 | constructor(arg) { 8 | if (!arg.clientKey) { 9 | if (arg.nonce) { 10 | this.buffer = arg.nonce; 11 | } else { 12 | this.buffer = new Buffer(nacl.randomBytes(nacl.box.nonceLength)); 13 | } 14 | } else { 15 | var b2 = blake2.createHash('blake2b', { digestLength: 24 }); 16 | if (arg.nonce) { 17 | b2.update(arg.nonce.getBuffer()); 18 | } 19 | 20 | b2.update(arg.clientKey); 21 | b2.update(arg.serverKey); 22 | 23 | this.buffer = b2.digest(); 24 | } 25 | } 26 | 27 | increment() { 28 | var integer; 29 | integer = this.buffer.readInt16LE(0); 30 | this.buffer.writeInt16LE(integer + 2, 0); 31 | } 32 | 33 | getBuffer() { 34 | return this.buffer; 35 | } 36 | } 37 | 38 | module.exports = Nonce; 39 | -------------------------------------------------------------------------------- /lib/packetreceiver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class PacketReceiver { 4 | constructor() { 5 | this._buffer = null; 6 | this._packet = null; 7 | } 8 | 9 | packetize(data, callback) { 10 | var messageId, offset, payloadLength, ref, ref1, results; 11 | 12 | if (this._buffer) { 13 | this._buffer = Buffer.concat([this._buffer, data]); 14 | } else { 15 | this._buffer = data; 16 | } 17 | 18 | while (this._buffer && this._buffer.length) { 19 | if (this._packet && this._packet.length) { 20 | payloadLength = this._packet.readUIntBE(2, 3); 21 | 22 | if (this._buffer.length >= payloadLength) { 23 | if (this._packet) { 24 | this._packet = Buffer.concat([this._packet, this._buffer.slice(0, payloadLength)]); 25 | } else { 26 | this._packet = this._buffer.slice(0, payloadLength); 27 | } 28 | 29 | callback(this._packet); 30 | this._packet = null; 31 | 32 | this._buffer = this._buffer.slice(payloadLength); 33 | } else { 34 | break; 35 | } 36 | } else if (this._buffer.length >= 7) { 37 | this._packet = this._buffer.slice(0, 7); 38 | this._buffer = this._buffer.slice(7); 39 | } else { 40 | // we'll be coming back here soon, but looks like we went through current buffer without a full header yet 41 | break; 42 | } 43 | } 44 | } 45 | } 46 | 47 | module.exports = PacketReceiver; 48 | -------------------------------------------------------------------------------- /lib/server/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var nacl = require("tweetnacl"); 4 | var blake2 = require("blake2"); 5 | var ByteBuffer = require("../../util/bytebuffer-sc"); 6 | var EMsg = require('../../enums/emsg'); 7 | const Crypto = require('../crypto.js'); 8 | const Nonce = require('../nonce.js'); 9 | 10 | class ServerCrypto extends Crypto { 11 | constructor(settings) { 12 | super(); 13 | this.publicServerKey = new Buffer(settings.serverKey, "hex"); 14 | this.magicKey = new Buffer("56b577a8996f52c388981942811c6e9b8d39dc3e281f1ce0c047a3d8e55cb222", "hex"); 15 | } 16 | 17 | setClient(client) { 18 | this.client = client; 19 | } 20 | 21 | decryptPacket(message) { 22 | if (message.messageType == EMsg.ClientHello) { 23 | message.decrypted = message.payload; 24 | } else if (message.messageType == EMsg.Login) { 25 | this.clientKey = message.payload.slice(0, 32); 26 | var cipherText = message.payload.slice(32); 27 | 28 | this.setSharedKey(this.magicKey) 29 | var nonce = new Nonce({ clientKey: this.clientKey, serverKey: this.publicServerKey }); 30 | 31 | message.decrypted = this.decrypt(cipherText, nonce); 32 | 33 | if (message.decrypted) { 34 | this.setSessionKey(Buffer.from(message.decrypted.slice(0, 24))); 35 | this.setDecryptNonce(Buffer.from(message.decrypted.slice(24, 48))); 36 | this.client.setEncryptNonce(Buffer.from(message.decrypted.slice(24, 48))); 37 | 38 | message.decrypted = message.decrypted.slice(48); 39 | } 40 | } else { 41 | message.decrypted = this.decrypt(message.payload); 42 | } 43 | } 44 | 45 | encryptPacket(message) { 46 | if (message.messageType == EMsg.ServerHello || (message.messageType == EMsg.LoginFailed && !this.getSessionKey())) { 47 | message.encrypted = message.decrypted; 48 | } else if (message.messageType == EMsg.LoginOk || message.messageType == EMsg.LoginFailed) { 49 | var nonce = new Nonce({ clientKey: this.clientKey, serverKey: this.publicServerKey, nonce: this.decryptNonce }); 50 | 51 | this.setSharedKey(this.magicKey) 52 | 53 | var toEncrypt = Buffer.concat([this.encryptNonce.getBuffer(), this.getSharedKey(), Buffer.from(message.decrypted)]); 54 | var cipherText = this.encrypt(toEncrypt, nonce); 55 | 56 | message.encrypted = cipherText; 57 | } else { 58 | message.encrypted = this.encrypt(message.decrypted); 59 | } 60 | } 61 | } 62 | 63 | module.exports = ServerCrypto; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-coc-proxy", 3 | "version": "1.0.0", 4 | "description": "Clash of Clans Proxy", 5 | "homepage": "https://github.com/SC-DevTeam/node-coc-proxy", 6 | "bugs": { 7 | "url": "https://github.com/SC-DevTeam/node-coc-proxy/issues" 8 | }, 9 | "main": "index.js", 10 | "author": "Steve Rabouin", 11 | "license": "GNU GPLv3", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://bitbucket.org/royaletools/node-cr-proxy.git" 15 | }, 16 | "engines": { 17 | "node": ">=6.8.0" 18 | }, 19 | "dependencies": { 20 | "blake2": "^1.4.0", 21 | "bytebuffer": "^5.0.1", 22 | "coc-messages": "github:SC-DevTeam/coc-messages", 23 | "command-line-args": "^4.0.2", 24 | "command-line-usage": "^4.0.0", 25 | "console-stamp": "^0.2.5", 26 | "jsome": "^2.3.26", 27 | "tweetnacl": "^0.14.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverKey": "7eb15f65bdd576619abbd0b0650c45db1020f5ec969fcf48a828424f1bc8d809" 3 | } 4 | -------------------------------------------------------------------------------- /util/bytebuffer-sc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ByteBuffer = module.exports = require('bytebuffer'); 4 | 5 | /** 6 | * Reads a 32bit base 128 variable-length integer using supercell magic. 7 | * @param {number=} offset Offset to read from. Will use and increase {@link ByteBuffer#offset} by the number of bytes 8 | * written if omitted. 9 | * @returns {number|!{value: number, length: number}} The value read if offset is omitted, else the value read 10 | * and the actual number of bytes read. 11 | * @throws {Error} If it's not a valid varint. Has a property `truncated = true` if there is not enough data available 12 | * to fully decode the varint. 13 | * @expose 14 | */ 15 | ByteBuffer.prototype.readRrsInt32 = function(offset) { 16 | var relative = typeof offset === 'undefined'; 17 | if (relative) offset = this.offset; 18 | if (!this.noAssert) { 19 | if (typeof offset !== 'number' || offset % 1 !== 0) 20 | throw TypeError("Illegal offset: " + offset + " (not an integer)"); 21 | offset >>>= 0; 22 | if (offset < 0 || offset + 1 > this.buffer.length) 23 | throw RangeError("Illegal offset: 0 <= " + offset + " (+" + 1 + ") <= " + this.buffer.length); 24 | } 25 | var c = 0, 26 | value = 0 >>> 0, 27 | seventh, 28 | msb, 29 | b; 30 | do { 31 | if (!this.noAssert && offset > this.limit) { 32 | var err = Error("Truncated"); 33 | err.truncated = true; 34 | throw err; 35 | } 36 | b = this.buffer[offset++]; 37 | 38 | if (c === 0) { 39 | seventh = (b & 0x40) >> 6; // save 7th bit 40 | msb = (b & 0x80) >> 7; // save msb 41 | b = b << 1; // rotate to the left 42 | b = b & ~(0x181); // clear 8th and 1st bit and 9th if any 43 | b = b | (msb << 7) | (seventh); // insert msb and 6th back in 44 | } 45 | 46 | value |= (b & 0x7f) << (7 * c); 47 | ++c; 48 | } while ((b & 0x80) !== 0); 49 | 50 | value = ((value >>> 1) ^ -(value & 1)) | 0; 51 | 52 | if (relative) { 53 | this.offset = offset; 54 | return value; 55 | } 56 | 57 | return { 58 | "value": value, 59 | "length": c 60 | }; 61 | }; 62 | 63 | /** 64 | * Writes a 32bit base 128 variable-length integer using supercell magic. 65 | * @param {number} value Value to write 66 | * @param {number=} offset Offset to write to. Will use and increase {@link ByteBuffer#offset} by the number of bytes 67 | * written if omitted. 68 | * @returns {!ByteBuffer|number} this if `offset` is omitted, else the actual number of bytes written 69 | * @expose 70 | */ 71 | ByteBuffer.prototype.writeRrsInt32 = function(value, offset) { 72 | var relative = typeof offset === 'undefined'; 73 | if (relative) offset = this.offset; 74 | if (!this.noAssert) { 75 | if (typeof value !== 'number' || value % 1 !== 0) 76 | throw TypeError("Illegal value: " + value + " (not an integer)"); 77 | value |= 0; 78 | if (typeof offset !== 'number' || offset % 1 !== 0) 79 | throw TypeError("Illegal offset: " + offset + " (not an integer)"); 80 | offset >>>= 0; 81 | if (offset < 0 || offset + 0 > this.buffer.length) 82 | throw RangeError("Illegal offset: 0 <= " + offset + " (+" + 0 + ") <= " + this.buffer.length); 83 | } 84 | var size = ByteBuffer.calculateVarint32(value), 85 | rotate = true, 86 | b; 87 | offset += size; 88 | var capacity10 = this.buffer.length; 89 | if (offset > capacity10) 90 | this.resize((capacity10 *= 2) > offset ? capacity10 : offset); 91 | offset -= size; 92 | 93 | value = (value << 1) ^ (value >> 31); 94 | 95 | value >>>= 0; 96 | while (value) { 97 | b = (value & 0x7f); 98 | if (value >= 0x80) 99 | b |= 0x80; 100 | if (rotate) { 101 | rotate = false; 102 | var lsb = b & 0x1; 103 | var msb = (b & 0x80) >> 7; 104 | b = b >> 1; // rotate to the right 105 | b = b & ~(0xC0); // clear 7th and 6th bit 106 | b = b | (msb << 7) | (lsb << 6); // insert msb and lsb back in 107 | } 108 | this.buffer[offset++] = b; 109 | value >>>= 7; 110 | } 111 | if (relative) { 112 | this.offset = offset; 113 | return this; 114 | } 115 | return size; 116 | }; 117 | 118 | /** 119 | * Reads a length as uint32 prefixed UTF8 encoded string. Supercell also has FF FF FF FF for len when string is empty so 120 | * we override it 121 | * @param {number=} offset Offset to read from. Will use and increase {@link ByteBuffer#offset} by the number of bytes 122 | * read if omitted. 123 | * @returns {string|!{string: string, length: number}} The string read if offset is omitted, else the string 124 | * read and the actual number of bytes read. 125 | * @expose 126 | * @see ByteBuffer#readVarint32 127 | */ 128 | ByteBuffer.prototype.readIString = function(offset) { 129 | var relative = typeof offset === 'undefined'; 130 | if (relative) offset = this.offset; 131 | if (!this.noAssert) { 132 | if (typeof offset !== 'number' || offset % 1 !== 0) 133 | throw TypeError("Illegal offset: " + offset + " (not an integer)"); 134 | offset >>>= 0; 135 | if (offset < 0 || offset + 4 > this.buffer.length) 136 | throw RangeError("Illegal offset: 0 <= " + offset + " (+" + 4 + ") <= " + this.buffer.length); 137 | } 138 | var start = offset; 139 | var len = this.readUint32(offset); 140 | 141 | if (len == Math.pow(2, 32) - 1) { 142 | this.offset += 4; 143 | return ''; 144 | } else { 145 | var str = this.readUTF8String(len, ByteBuffer.METRICS_BYTES, offset += 4); 146 | offset += str.length; 147 | if (relative) { 148 | this.offset = offset; 149 | return str.string; 150 | } else { 151 | return { 152 | 'string': str.string, 153 | 'length': offset - start 154 | }; 155 | } 156 | } 157 | }; 158 | 159 | /** 160 | * Writes a length as uint32 prefixed UTF8 encoded string -- supercell wants FFFFFFFF instead of 00000000 when empty. 161 | * @param {string} str String to write 162 | * @param {number=} offset Offset to write to. Will use and increase {@link ByteBuffer#offset} by the number of bytes 163 | * written if omitted. 164 | * @returns {!ByteBuffer|number} `this` if `offset` is omitted, else the actual number of bytes written 165 | * @expose 166 | * @see ByteBuffer#writeVarint32 167 | */ 168 | ByteBuffer.prototype.writeIString = function(str, offset) { 169 | var relative = typeof offset === 'undefined'; 170 | if (relative) offset = this.offset; 171 | if (!this.noAssert) { 172 | if (typeof str !== 'string') 173 | throw TypeError("Illegal str: Not a string"); 174 | if (typeof offset !== 'number' || offset % 1 !== 0) 175 | throw TypeError("Illegal offset: " + offset + " (not an integer)"); 176 | offset >>>= 0; 177 | if (offset < 0 || offset + 0 > this.buffer.length) 178 | throw RangeError("Illegal offset: 0 <= " + offset + " (+" + 0 + ") <= " + this.buffer.length); 179 | } 180 | var start = offset, 181 | k; 182 | k = Buffer.byteLength(str, "utf8"); 183 | offset += 4 + k; 184 | var capacity13 = this.buffer.length; 185 | if (offset > capacity13) 186 | this.resize((capacity13 *= 2) > offset ? capacity13 : offset); 187 | offset -= 4 + k; 188 | if(k === 0) { // supercell wants FF ^_^ 189 | this.buffer[offset + 3] = 0xFF; 190 | this.buffer[offset + 2] = 0xFF; 191 | this.buffer[offset + 1] = 0xFF; 192 | this.buffer[offset] = 0xFF; 193 | } else if (this.littleEndian) { 194 | this.buffer[offset + 3] = (k >>> 24) & 0xFF; 195 | this.buffer[offset + 2] = (k >>> 16) & 0xFF; 196 | this.buffer[offset + 1] = (k >>> 8) & 0xFF; 197 | this.buffer[offset] = k & 0xFF; 198 | } else { 199 | this.buffer[offset] = (k >>> 24) & 0xFF; 200 | this.buffer[offset + 1] = (k >>> 16) & 0xFF; 201 | this.buffer[offset + 2] = (k >>> 8) & 0xFF; 202 | this.buffer[offset + 3] = k & 0xFF; 203 | } 204 | offset += 4; 205 | offset += this.buffer.write(str, offset, k, "utf8"); 206 | if (relative) { 207 | this.offset = offset; 208 | return this; 209 | } 210 | return offset - start; 211 | }; 212 | -------------------------------------------------------------------------------- /util/usage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commandLineArgs = require('command-line-args'); 4 | var getUsage = require('command-line-usage'); 5 | var fs = require('fs'); 6 | 7 | function FileDetails(filename){ 8 | if (!(this instanceof FileDetails)) return new FileDetails(filename); 9 | this.filename = filename; 10 | this.exists = fs.existsSync(filename); 11 | } 12 | 13 | var optionDefinitions = [ 14 | { 15 | name: 'verbose', 16 | alias: 'v', 17 | description: 'Show debug log.', 18 | type: Boolean 19 | }, 20 | { 21 | name: 'dump', 22 | alias: 'd', 23 | typeLabel: '[underline]{folder}', 24 | description: 'Dump decrypted packets in specified folder', 25 | type: FileDetails 26 | }, 27 | { 28 | name: 'replay', 29 | alias: 'r', 30 | typeLabel: '[underline]{file}', 31 | description: 'Replay a dumped packet.', 32 | type: FileDetails, 33 | }, 34 | { 35 | name: 'help', 36 | alias: 'h', 37 | description: 'Print this usage guide.', 38 | type: Boolean 39 | } 40 | ]; 41 | 42 | var sections = [ 43 | { 44 | header: 'Royale Proxy', 45 | content: 'A simple Clash Royale proxy.' 46 | }, 47 | { 48 | header: 'Synopsis', 49 | content: [ 50 | '$ node index [[bold]{--verbose}] [[bold]{--dump} [underline]{./packets/}]', 51 | '$ node index --help' 52 | ] 53 | }, 54 | { 55 | header: 'Options', 56 | optionList: optionDefinitions 57 | }, 58 | { 59 | content: [ 60 | 'Visit us at [underline]{http://github.com/royale-proxy}', 61 | '', 62 | ' _______ __ _______ ', 63 | '| _ .-----.--.--.---.-| .-----| _ .----.-----.--.--.--.--.', 64 | '|. l | _ | | | _ | | -__|. 1 | _| _ |_ _| | |', 65 | '|. _ |_____|___ |___._|__|_____|. ____|__| |_____|__.__|___ |', 66 | '|: | | |_____| |: | |_____|', 67 | '|::.|:. | |::.| ', 68 | '`--- ---\' `---\' ' 69 | ], 70 | raw: true 71 | } 72 | ]; 73 | 74 | var options = commandLineArgs(optionDefinitions); 75 | var usage = getUsage(sections); 76 | 77 | if(options.help) { 78 | console.log(usage); 79 | process.exit(0); 80 | } 81 | 82 | if(options.dump === null || (options.dump && !options.dump.exists)) { 83 | console.error('Error: Specified path does not exist. Please check the path and try again.'); 84 | console.log(usage); 85 | process.exit(1); 86 | } 87 | 88 | if(options.replay === null || (options.replay && !options.replay.exists)) { 89 | console.error('Error: Specified filename does not exist. Please check the filename and try again.'); 90 | console.log(usage); 91 | process.exit(1); 92 | } 93 | 94 | module.exports.options = options; 95 | --------------------------------------------------------------------------------