Skip to content

Commit b47fc6e

Browse files
committed
Fix error handling, work on native backend issues.
Share encryption packet handling between JS and native backends.
1 parent 031c008 commit b47fc6e

File tree

7 files changed

+116
-145
lines changed

7 files changed

+116
-145
lines changed

TODO

-5
This file was deleted.

android/app/src/main/java/com/enderchat/modules/connection/ConnectionModule.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ class ConnectionModule(reactContext: ReactApplicationContext)
186186
buffer.write(bytes, packet.totalLength!!, bytes.size - packet.totalLength)
187187

188188
// We can handle Keep Alive, Login Success and Set Compression.
189-
// TODO: Maybe handle Keep Alive in JS. The overhead is minimal and would take away the disconnect timer from here.
190189
if (packet.id.value == keepAliveClientBoundId) {
191190
directlyWriteToConnection(keepAliveServerBoundId, packet.data)
192191
continue
@@ -239,11 +238,15 @@ class ConnectionModule(reactContext: ReactApplicationContext)
239238
.emit(eventName, params)
240239
}
241240

242-
@ReactMethod fun addListener(/* eventName: String */) {
241+
@ReactMethod
242+
@Suppress("UNUSED_PARAMETER")
243+
fun addListener(eventName: String) {
243244
// Set up any upstream listeners or background tasks as necessary
244245
}
245246

246-
@ReactMethod fun removeListeners(/* count: Int */) {
247+
@ReactMethod
248+
@Suppress("UNUSED_PARAMETER")
249+
fun removeListeners(count: Int) {
247250
// Remove upstream listeners, stop unnecessary background tasks
248251
}
249252
}

src/minecraft/connection/javascript.ts

+15-63
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,19 @@ import {
66
Cipher,
77
createCipheriv,
88
createDecipheriv,
9-
createHash,
10-
Decipher,
11-
publicEncrypt
9+
Decipher
1210
} from 'react-native-crypto'
1311
import {
1412
concatPacketData,
1513
makeBaseCompressedPacket,
1614
makeBasePacket,
1715
Packet,
18-
PacketDataTypes,
1916
parseCompressedPacket,
2017
parsePacket
2118
} from '../packet'
22-
import {
23-
readVarInt,
24-
writeVarInt,
25-
resolveHostname,
26-
mcHexDigest,
27-
protocolMap,
28-
getRandomBytes
29-
} from '../utils'
30-
import { joinMinecraftSession } from '../api/mojang'
3119
import { ServerConnection, ConnectionOptions } from '.'
32-
import { getLoginPacket, parseEncryptionRequestPacket } from './shared'
20+
import { getLoginPacket, handleEncryptionRequest } from './shared'
21+
import { readVarInt, writeVarInt, resolveHostname, protocolMap } from '../utils'
3322

3423
export declare interface JavaScriptServerConnection {
3524
on: ((event: 'packet', listener: (packet: Packet) => void) => this) &
@@ -200,56 +189,19 @@ const initiateJavaScriptConnection = async (opts: ConnectionOptions) => {
200189
conn.close()
201190
continue
202191
}
203-
// https://wiki.vg/Protocol_Encryption
204-
const [serverId, publicKey, verifyToken] =
205-
parseEncryptionRequestPacket(packet)
206-
;(async () => {
207-
const secret = await getRandomBytes(16) // Generate random 16-byte shared secret.
208-
// Generate hash.
209-
const sha1 = createHash('sha1')
210-
sha1.update(serverId) // ASCII encoding of the server id string from Encryption Request
211-
sha1.update(secret)
212-
sha1.update(publicKey) // Server's encoded public key from Encryption Request
213-
const hash = mcHexDigest(sha1.digest())
214-
// Send hash to Mojang servers.
215-
const req = await joinMinecraftSession(
216-
accessToken,
217-
selectedProfile,
218-
hash
219-
)
220-
if (!req.ok) {
221-
throw new Error('Mojang online mode network request failed')
222-
}
223-
// Encrypt shared secret and verify token with public key.
224-
const pk =
225-
'-----BEGIN PUBLIC KEY-----\n' +
226-
publicKey.toString('base64') +
227-
'\n-----END PUBLIC KEY-----'
228-
const ePrms = { key: pk, padding: 1 } // RSA_PKCS1_PADDING
229-
const encryptedSharedSecret = publicEncrypt(ePrms, secret)
230-
const encryptedVerifyToken = publicEncrypt(ePrms, verifyToken)
231-
// Send encryption response packet.
232-
// From this point forward, everything is encrypted, including the Login Success packet.
233-
const response: PacketDataTypes[] = [
234-
writeVarInt(encryptedSharedSecret.byteLength),
235-
encryptedSharedSecret,
236-
writeVarInt(encryptedVerifyToken.byteLength),
237-
encryptedVerifyToken
238-
]
239-
if (is119) {
240-
conn.msgSalt = await getRandomBytes(8)
241-
response.splice(2, 0, true)
192+
handleEncryptionRequest(
193+
packet,
194+
accessToken,
195+
selectedProfile,
196+
conn,
197+
is119,
198+
async (secret: Buffer, response: Buffer) => {
199+
const AES_ALG = 'aes-128-cfb8'
200+
conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret)
201+
await conn.writePacket(0x01, response)
202+
conn.aesCipher = createCipheriv(AES_ALG, secret, secret)
242203
}
243-
const AES_ALG = 'aes-128-cfb8'
244-
conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret)
245-
await conn.writePacket(0x01, concatPacketData(response))
246-
conn.aesCipher = createCipheriv(AES_ALG, secret, secret)
247-
})().catch(e => {
248-
console.error(e)
249-
conn.disconnectReason =
250-
'{"text":"Failed to authenticate with Mojang servers!"}'
251-
conn.close()
252-
})
204+
)
253205
}
254206
conn.emit('packet', packet)
255207
} else break

src/minecraft/connection/native.ts

+24-67
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,10 @@ import {
44
NativeModules
55
} from 'react-native'
66
import events from 'events'
7-
import { createHash, publicEncrypt } from 'react-native-crypto'
8-
import { concatPacketData, Packet, PacketDataTypes } from '../packet'
9-
import {
10-
readVarInt,
11-
writeVarInt,
12-
resolveHostname,
13-
mcHexDigest,
14-
protocolMap,
15-
getRandomBytes
16-
} from '../utils'
17-
import { joinMinecraftSession } from '../api/mojang'
187
import { ServerConnection, ConnectionOptions } from '.'
19-
import { getLoginPacket, parseEncryptionRequestPacket } from './shared'
8+
import { concatPacketData, Packet } from '../packet'
9+
import { getLoginPacket, handleEncryptionRequest } from './shared'
10+
import { readVarInt, writeVarInt, resolveHostname, protocolMap } from '../utils'
2011

2112
const { ConnectionModule } = NativeModules
2213

@@ -65,6 +56,10 @@ export class NativeServerConnection
6556
this.eventEmitter.addListener('packet', (event: NativePacketEvent) => {
6657
console.log(event)
6758
if (event.connectionId !== this.id) return
59+
// Handle timeout after 20 seconds of no data. (TODO: Handle this natively.)
60+
if (this.disconnectTimer) clearTimeout(this.disconnectTimer)
61+
this.disconnectTimer = setTimeout(() => this.close(), 20000)
62+
// Run after interactions to improve user experience.
6863
InteractionManager.runAfterInteractions(() => {
6964
const packet: Packet = {
7065
id: event.id,
@@ -75,16 +70,15 @@ export class NativeServerConnection
7570
lengthLength: event.lengthLength
7671
}
7772

78-
// Internally handle login packets.
79-
// We aren't handling these in native for improved code sharing.
80-
// TODO: Actually share code with the JavaScript back-end.
73+
// Internally handle login packets. We aren't handling these in native to share code.
8174
const is1164 = options.protocolVersion >= protocolMap['1.16.4']
8275
const is117 = options.protocolVersion >= protocolMap[1.17]
8376
const is119 = options.protocolVersion >= protocolMap[1.19]
8477
const is1191 = options.protocolVersion >= protocolMap['1.19.1']
8578
// Set Compression and Keep Alive are handled in native for now.
86-
if (packet.id === 0x02 && !this.loggedIn) {
87-
this.loggedIn = true // Login Success
79+
// When modifying this code, apply the same changes to the JavaScript back-end.
80+
if (packet.id === 0x02 && !this.loggedIn /* Login Success */) {
81+
this.loggedIn = true
8882
} else if (
8983
// Disconnect (login) or Disconnect (play)
9084
(packet.id === 0x00 && !this.loggedIn) ||
@@ -111,56 +105,19 @@ export class NativeServerConnection
111105
this.close()
112106
return
113107
}
114-
// https://wiki.vg/Protocol_Encryption
115-
const [serverId, publicKey, verifyToken] =
116-
parseEncryptionRequestPacket(packet)
117-
;(async () => {
118-
const secret = await getRandomBytes(16) // Generate random 16-byte shared secret.
119-
// Generate hash.
120-
const sha1 = createHash('sha1')
121-
sha1.update(serverId) // ASCII encoding of the server id string from Encryption Request
122-
sha1.update(secret)
123-
sha1.update(publicKey) // Server's encoded public key from Encryption Request
124-
const hash = mcHexDigest(sha1.digest())
125-
// Send hash to Mojang servers.
126-
const req = await joinMinecraftSession(
127-
accessToken,
128-
selectedProfile,
129-
hash
130-
)
131-
if (!req.ok) {
132-
throw new Error('Mojang online mode network request failed')
133-
}
134-
// Encrypt shared secret and verify token with public key.
135-
const pk =
136-
'-----BEGIN PUBLIC KEY-----\n' +
137-
publicKey.toString('base64') +
138-
'\n-----END PUBLIC KEY-----'
139-
const ePrms = { key: pk, padding: 1 } // RSA_PKCS1_PADDING
140-
const encryptedSharedSecret = publicEncrypt(ePrms, secret)
141-
const encryptedVerifyToken = publicEncrypt(ePrms, verifyToken)
142-
// Send encryption response packet.
143-
// From this point forward, everything is encrypted, including the Login Success packet.
144-
const response: PacketDataTypes[] = [
145-
writeVarInt(encryptedSharedSecret.byteLength),
146-
encryptedSharedSecret,
147-
writeVarInt(encryptedVerifyToken.byteLength),
148-
encryptedVerifyToken
149-
]
150-
if (is119) {
151-
this.msgSalt = await getRandomBytes(8)
152-
response.splice(2, 0, true)
108+
handleEncryptionRequest(
109+
packet,
110+
accessToken,
111+
selectedProfile,
112+
this,
113+
is119,
114+
async (secret: Buffer, response: Buffer) => {
115+
// const AES_ALG = 'aes-128-cfb8'
116+
// conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret)
117+
await this.writePacket(0x01, response)
118+
// conn.aesCipher = createCipheriv(AES_ALG, secret, secret)
153119
}
154-
// const AES_ALG = 'aes-128-cfb8'
155-
// this.aesDecipher = createDecipheriv(AES_ALG, secret, secret)
156-
await this.writePacket(0x01, concatPacketData(response))
157-
// this.aesCipher = createCipheriv(AES_ALG, secret, secret)
158-
})().catch(e => {
159-
console.error(e)
160-
this.disconnectReason =
161-
'{"text":"Failed to authenticate with Mojang servers!"}'
162-
this.close()
163-
})
120+
)
164121
}
165122

166123
this.emit('packet', packet)
@@ -200,7 +157,7 @@ export class NativeServerConnection
200157

201158
const initiateNativeConnection = async (opts: ConnectionOptions) => {
202159
const [host, port] = await resolveHostname(opts.host, opts.port)
203-
const id = await ConnectionModule.createConnection({
160+
const id = await ConnectionModule.openConnection({
204161
loginPacket: getLoginPacket(opts).toString('base64'),
205162
...opts,
206163
host,

src/minecraft/connection/shared.ts

+64-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import { ConnectionOptions } from '.'
1+
import { createHash, publicEncrypt } from 'react-native-crypto'
2+
import { ConnectionOptions, ServerConnection } from '.'
3+
import { joinMinecraftSession } from '../api/mojang'
24
import { concatPacketData, Packet, PacketDataTypes } from '../packet'
3-
import { protocolMap, readVarInt, writeVarInt } from '../utils'
5+
import {
6+
getRandomBytes,
7+
mcHexDigest,
8+
protocolMap,
9+
readVarInt,
10+
writeVarInt
11+
} from '../utils'
412

513
export const parseEncryptionRequestPacket = (packet: Packet) => {
614
// ASCII encoding of the server id string
@@ -49,3 +57,57 @@ export const getLoginPacket = (opts: ConnectionOptions) => {
4957
}
5058
return concatPacketData(data)
5159
}
60+
61+
export const handleEncryptionRequest = (
62+
packet: Packet,
63+
accessToken: string,
64+
selectedProfile: string,
65+
connection: ServerConnection,
66+
is119: boolean,
67+
callback: (secret: Buffer, response: Buffer) => Promise<void>
68+
) => {
69+
// https://wiki.vg/Protocol_Encryption
70+
const [serverId, publicKey, verifyToken] =
71+
parseEncryptionRequestPacket(packet)
72+
;(async () => {
73+
const secret = await getRandomBytes(16) // Generate random 16-byte shared secret.
74+
// Generate hash.
75+
const sha1 = createHash('sha1')
76+
sha1.update(serverId) // ASCII encoding of the server id string from Encryption Request
77+
sha1.update(secret)
78+
sha1.update(publicKey) // Server's encoded public key from Encryption Request
79+
const hash = mcHexDigest(sha1.digest())
80+
// Send hash to Mojang servers.
81+
const req = await joinMinecraftSession(accessToken, selectedProfile, hash)
82+
if (!req.ok) {
83+
throw new Error('Mojang online mode network request failed')
84+
}
85+
// Encrypt shared secret and verify token with public key.
86+
const pk =
87+
'-----BEGIN PUBLIC KEY-----\n' +
88+
publicKey.toString('base64') +
89+
'\n-----END PUBLIC KEY-----'
90+
const ePrms = { key: pk, padding: 1 } // RSA_PKCS1_PADDING
91+
const encryptedSharedSecret = publicEncrypt(ePrms, secret)
92+
const encryptedVerifyToken = publicEncrypt(ePrms, verifyToken)
93+
// Send encryption response packet.
94+
// From this point forward, everything is encrypted, including the Login Success packet.
95+
const response: PacketDataTypes[] = [
96+
writeVarInt(encryptedSharedSecret.byteLength),
97+
encryptedSharedSecret,
98+
writeVarInt(encryptedVerifyToken.byteLength),
99+
encryptedVerifyToken
100+
]
101+
if (is119) {
102+
connection.msgSalt = await getRandomBytes(8)
103+
response.splice(2, 0, true)
104+
}
105+
// This callback will send the response and enable the ciphers.
106+
await callback(secret, concatPacketData(response))
107+
})().catch(e => {
108+
console.error(e)
109+
connection.disconnectReason =
110+
'{"text":"Failed to authenticate with Mojang servers!"}'
111+
connection.close()
112+
})
113+
}

src/screens/chat/ChatScreen.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,14 @@ const ChatScreen = ({ navigation, route }: Props) => {
159159
.then(conn => {
160160
if (statusRef.current !== 'CLOSED') {
161161
if (isConnection(conn)) setConnection(conn)
162-
else setDisconnectReason(conn)
162+
else {
163+
closeChatScreen()
164+
setDisconnectReason(conn)
165+
}
163166
} else if (isConnection(conn)) conn.connection.close() // No memory leaky
164167
})
165-
.catch(() => {
168+
.catch(e => {
169+
console.error(e)
166170
closeChatScreen()
167171
setDisconnectReason({
168172
server: route.params.serverName,

src/screens/chat/sessionBuilder.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export const createConnection = async (
3333
const [host, port] = await resolveHostname(hostname, portNumber)
3434
const activeAccount = Object.keys(accounts).find(e => accounts[e].active)
3535
if (!activeAccount) {
36-
closeChatScreen()
3736
return {
3837
server,
3938
reason:
@@ -86,7 +85,6 @@ export const createConnection = async (
8685
}
8786
setSession(activeAccount, session)
8887
} catch (e) {
89-
closeChatScreen()
9088
const reason =
9189
'Failed to create session! You may need to re-login with your Microsoft Account in the Accounts tab.'
9290
return { server, reason }
@@ -116,7 +114,7 @@ export const createConnection = async (
116114
newConn.on('error', onCloseOrError)
117115
return { serverName: server, connection: newConn }
118116
} catch (e) {
119-
closeChatScreen()
117+
console.error(e)
120118
return { server, reason: 'Failed to connect to server!' }
121119
}
122120
}

0 commit comments

Comments
 (0)