Xcr3tChat: 一个比较安全的聊天工具

最近准备换机器,在整理硬盘的时候翻出来了一套加密聊天系统的源代码,是大三的时候的大作业。这套系统使用一次性密钥对来进行端对端的聊天加密,希望能给大家在加密传输方面带来一些新的思路。完整代码在这里:https://github.com/KavelCortex/Xcr3TChat-server

Xcr3tChat的特点

全程RSA加密

无论是对服务器进行身份校验,还是端对端进行聊天,所有内容都将使用非对称加密方式进行加密后传输。就算被中间人攻击,获取到的不过是一串184个字符的BASE64编码,在没有获取到接收端私钥的情况下根本无从破译。

对于发送聊天信息的客户端,每次发送消息前将向对方发送一个握手包请求对方发送公钥。将聊天内容使用公钥加密后直接传送至对方客户端。而接收端在每次收到握手请求后将生成一组新的密钥对。密钥对使用一次后将被销毁,即使有一条截获的消息被破解,那么对于其他消息还能保持加密。

端对端通信

客户端对于服务器的通信也是使用RSA加密的方式进行。服务器使用的是固定密钥对,所以整套系统安全性的突破口在于服务器密钥的保管。由于服务器端是整套系统安全性最低的部分,所以服务器承担的工作是最不敏感的,仅为验证身份信息以及承担寻呼的任务,除此以外完全不干涉客户端之间的通信。

protocol

项目功能概述

  • 全程加密通讯
  • 服务器通信相关操作(如注册用户、登入登出操作等)
  • 客户端通信相关操作(如聊天、交换公钥等)
  • 用于跨平台通信的通信协议
  • 用于快速开发的客户端适配器

实现方法

全程加密通讯

客户端与服务器端在发送数据时都需要获取对方的公钥,然后通过加密类CyptorUtil进行加密操作再进行发送。

发送方处理流程

原始数据->RSA加密->BASE64编码->发送

接收端处理流程

接收->BASE64解码->RSA 解密->原始数据

代码实现

加密工具类 CryptorUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class CryptorUtil {


public static String getSaltedMD5(String src, byte salt) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(src.getBytes());
byte[] md5 = md.digest();
md5[0] = salt;

return new BigInteger(1, md5).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}

public static String getRandomSaltedMD5(String src) {
byte salt = (byte) Math.abs(new SecureRandom().nextInt(256));
src += salt;
return getSaltedMD5(src, salt);
}

public static boolean equalsSaltedMD5(String src, String md5) {
//byte salt = md5.getBytes()[0];
String saltHex = md5.substring(0, 2);
byte salt = (byte) Integer.parseInt(saltHex, 16);
String srcMD5 = getSaltedMD5(src + salt, salt);
return srcMD5.equals(md5);
}


public static String encryptBASE64(byte[] data) throws IOException {
return (new BASE64Encoder()).encodeBuffer(data);
}

public static byte[] decryptBASE64(String data) throws IOException {
return (new BASE64Decoder()).decodeBuffer(data);
}

public static String pack(String BASE64PubKey, String rawData) throws
IOException {
byte[] encryptedData = cryptData(BASE64PubKey, rawData.getBytes(),
Cipher.ENCRYPT_MODE);
return encryptBASE64(encryptedData);
}

public static String unpack(String BASE64PriKey, String BASE64Data)
throws IOException {
byte[] data = decryptBASE64(BASE64Data);
byte[] decryptedData = cryptData(BASE64PriKey, data, Cipher
.DECRYPT_MODE);
return new String(decryptedData, "UTF-8");
}


public static byte[] cryptData(String BASE64Key, byte[] data, int
cryptMode) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec keySpec;
Key key;
switch (cryptMode) {
case Cipher.ENCRYPT_MODE:
keySpec = new X509EncodedKeySpec(decryptBASE64(BASE64Key));
key = keyFactory.generatePublic(keySpec);
break;
case Cipher.DECRYPT_MODE:
keySpec = new PKCS8EncodedKeySpec(decryptBASE64(BASE64Key));
key = keyFactory.generatePrivate(keySpec);
break;
default:
return null;
}
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(cryptMode, key);
return cipher.doFinal(data);

} catch (Exception e) {
throw new IllegalStateException("Crypt Failed");
}
}

}

服务器通信相关操作

客户端开启一个socket连接至ServerSocket,通过自定义通信协议发送加密信息。服务器接收到信息后解密并解析指令,进行相应的操作并发送返回的加密信息给客户端。

功能截图

注册用户

添加用户前数据库情况

UI界面进行注册操作1

UI界面进行注册操作2

添加用户后数据库情况

登录操作

命令行进行登录

登录成功提示

查找并连接用户

对方不在线

对方在线

登出操作

命令行输入-o进行登出

登出成功提示

代码实现

服务器端生成类Xcr3TServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Xcr3TServer{
private int mPort;
protected static Connection dbConn;
protected final static String databaseURL="jdbc:mariadb://example.com:3300/xcr3tdb?user=xcr3tserver&password=**********";
protected final static String SERVER_PRIVATE_KEY =
"MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJQLwdmayTZ90cFhNq5y6qRI3YR5\n" +
"4pSZyeqs8yD1FfGvdBpjzHCx4/rbl4xLvSt+BrP/QuYAd6ebqu8qRaUYTUzd2vpHA5NeU0BsRbRz\n" +
"V+l8ypj113o83DmOfsnGMitVqSxw754NNGxGrU5f0sdb6qSzCO3ZGRIij19+9Mv3qfdJAgMBAAEC\n" +
"gYAfC4QgDKxrJ+FHiwo7dM+tmbYSJLkV7lYARzpIy/xJDUDsk8b4TuV+4nOaMPu/VhMzxbCSqMBu\n" +
"vl8O/i9SmpEC3pOHoa2fYX1OZwUWa89VuiumDMftwIFjnRIzhQf++7GKMcVzRVSSuHlcIG7AcG34\n" +
"u5Gg8XtrI/vHOpornERZMQJBANIyT6h6lZzKiVy7DDuYowEU1A7LG3Ers7vt4W19VAdjGr+xO792\n" +
"Svv+2DdaiDWPM1P4dG6d+Wmr6JLqw9kS/QUCQQC0Tmlc2WfgLrCI8pdclB+nXcSYs2UHILcHOtr8\n" +
"c94SySw2XRTkJTbvqkvgjPHEYvvp8gu3Ls3/yXTUIeiOW0R1AkAhgoPQiDpx1JgxgGBi3+KcuYVV\n" +
"Fmw5jo4I19OocOKEivgot0ifLWym3+n4aSZt43Z7XJCzUdwBTLa3NVYjtTNBAkA3M/6cN8/O2lyg\n" +
"QS3IYW1jj5jea6ZVzVVcOE/NlSf7tm374vm/dAlizU/X2y82QlwAX2Po3MKjOqmzPQJ3e0f1AkEA\n" +
"rQNV91xRhLOKQ3uSfzrAjSv3+mg5vQ3B4VG+hoNmoVmh9V/PJcP2pAv3Zx6yCZCjhv4RuWQSFDro\n" +
"PEvmPCQgPQ==";

public Xcr3TServer(int port){
System.out.println("Initializing Xcr3TServer...");
mPort=port;
try {
dbConn = DriverManager.getConnection(Xcr3TServer.databaseURL);
} catch (SQLException e) {
e.printStackTrace();
}
new Thread(new ServerSocketRunnable(mPort)).start();
}
}
服务器操作类 SocketHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
public class SocketHandler implements Runnable {

private static ConcurrentHashMap<UUID, Socket> clientSocketList = new
ConcurrentHashMap<>();

private UUID mUUID;
private List<Socket> mSockets = new ArrayList<>();

SocketHandler(List<Socket> sockets) {
mSockets = sockets;
}

public static Socket getClientSocket(UUID clientUUID) {
return clientSocketList.get(clientUUID);
}

static int getClientServingCount() {
if (clientSocketList != null)
return clientSocketList.size();
else
return 0;
}

int getSocketCount() {
return mSockets.size();
}

public UUID getUUID() {
return mUUID;
}

@Override
public void run() {
for (Socket socket : mSockets) {
try {
try {
mUUID = UUID.randomUUID();
clientSocketList.put(mUUID, socket);
System.out.println("\nADD:" + getUUID() + "/ " +
getClientServingCount() + " serving");
BufferedReader bufferedReader = new BufferedReader(new
InputStreamReader(socket.getInputStream()));
String line;
StringBuilder identity = new StringBuilder();
//Identity
while ((line = bufferedReader.readLine()) != null) {
identity.append(line);
identity.append("\r\n");
if (line.isEmpty())
break;
}
RequestParser requestParser;
requestParser = new RequestParser(identity.toString());
boolean doneFlag = false;


JSONObject clientJSON = requestParser.getJSON();

if (!doneFlag && requestParser.isProtocolHeader
(Xcr3TProtocol.REQUEST_ADD)) {
try {

System.out.println(clientJSON.toString());
String uid = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("uid"));
String pswMD5 = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("identity"));
//TODO: 添加用户资料进数据库
Statement statement = Xcr3TServer.dbConn
.createStatement();
ResultSet rs = statement.executeQuery("SELECT * " +
"FROM ClientInfo WHERE `uid`='" + uid +
"';");
if (rs.next())
throw new IllegalStateException("username is " +
"already exist");


int affected = statement.executeUpdate("INSERT " +
"INTO ClientInfo (uid,identity,status) " +
"values('" + uid + "','" + pswMD5 + "'," +
"'OFFLINE');");
if (affected == 0)
throw new IllegalStateException("Cannot add " +
"user.");
rs = statement.executeQuery("SELECT * FROM " +
"ClientInfo WHERE `uid`='" + uid + "' AND" +
" `identity`='" + pswMD5 + "';");
if (!rs.next())
throw new IllegalStateException("Cannot read " +
"user.");

Response response = new Response.Builder
(Xcr3TProtocol.RESPONSE_200_OK)
.setDestinationPublicKey(clientJSON
.getString("publicKey"))
.put("status", "OK")
.put("id", rs.getString("id"))
.put("uid", rs.getString("uid"))
.build();

doneFlag = sendResponse(socket, response);


} catch (JSONException e) {
throw new IllegalStateException("JSON Error");
} catch (SQLException e) {
e.printStackTrace();
throw new IllegalStateException("SQL Server " +
"Unavailable");
}
}

if (!doneFlag && requestParser.isProtocolHeader
(Xcr3TProtocol.REQUEST_HANDSHAKE)) {
try {

System.out.println(clientJSON.toString());
String uid = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("uid"));
String psw = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("identity"));
String port = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("port"));

//TODO: 查找数据库匹配身份并返回TOKEN
Statement statement = Xcr3TServer.dbConn
.createStatement();
String clientQueryStr = "WHERE `uid` = '" + uid +
"'";

ResultSet rs = statement.executeQuery("SELECT * " +
"FROM ClientInfo " + clientQueryStr + ";");
if (!rs.next())
throw new IllegalStateException("Unknown " +
"identity");

if (rs.getString("status").equals("ONLINE"))
throw new IllegalStateException("You have " +
"logged in. If it isn't you, please " +
"contact the server.");

String md5DB = rs.getString("identity");

if (!CryptorUtil.equalsSaltedMD5(psw, md5DB))
throw new IllegalStateException("Unknown " +
"identity");


statement.execute("UPDATE ClientInfo SET " +
"`status`='ONLINE', `token`= '" + getUUID
() + "' " +
", `location`= '" + socket.getInetAddress
() + "', `port`= '" + port + "' " +
clientQueryStr + ";");

Response response = new Response.Builder
(Xcr3TProtocol.RESPONSE_200_OK)
.setDestinationPublicKey(clientJSON
.getString("publicKey"))
.put("status", "OK")
.put("token", getUUID().toString())
.build();

doneFlag = sendResponse(socket, response);


} catch (JSONException e) {
throw new IllegalStateException("JSON Error");
} catch (SQLException e) {
e.printStackTrace();
throw new IllegalStateException("SQL Error");
}
}
if (!doneFlag && requestParser.isProtocolHeader
(Xcr3TProtocol.REQUEST_FIND)) {
try {

System.out.println(clientJSON.toString());
String destUID = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("destUID"));
String token = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("token"));

//TODO: 服务器比较token
Statement statement = Xcr3TServer.dbConn
.createStatement();
ResultSet rs = statement.executeQuery("SELECT " +
"`uid` FROM ClientInfo WHERE `token`='" +
token + "';");
if (!rs.next())
throw new IllegalStateException("Bad token");
if (rs.getString("uid").equals(destUID))
throw new IllegalStateException("Please don't" +
" find yourself :(");

rs = statement.executeQuery("SELECT * FROM " +
"ClientInfo WHERE `uid`='" + destUID +
"';");
if (!rs.next())
throw new IllegalStateException("Invalid user");

//TODO:服务器连接对方并获取ready值
boolean isReady;
String status = rs.getString("status");
if (isReady = status.equals("ONLINE")) {
Response response = new Response.Builder
(Xcr3TProtocol.RESPONSE_200_OK)
.setDestinationPublicKey(clientJSON
.getString("publicKey"))
.put("status", "OK")
.put("valid", "true")
.put("ready", String.valueOf(isReady))
.put("ip", rs.getString("location")
.substring(1))
.put("port", rs.getString("port"))
.build();
doneFlag = sendResponse(socket, response);
} else {
if (status.equals("OFFLINE"))
throw new IllegalStateException(destUID +
" is OFFLINE.");
throw new IllegalStateException(destUID + "is" +
" UNKNOWN.");
}


} catch (JSONException e) {
throw new IllegalStateException("JSON Error");
} catch (SQLException e) {
e.printStackTrace();
throw new IllegalStateException("SQL Error");
}
}
if (!doneFlag && requestParser.isProtocolHeader
(Xcr3TProtocol.REQUEST_GOODBYE)) {
try {
System.out.println(clientJSON.toString());
String uid = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("uid"));
String token = CryptorUtil.unpack(Xcr3TServer
.SERVER_PRIVATE_KEY, clientJSON.getString
("token"));
String clientQueryStr = "WHERE `uid` = '" + uid +
"' AND `token`='" + token + "'";
//TODO: 更改服务器上的用户状态
Statement statement = Xcr3TServer.dbConn
.createStatement();
statement.execute("UPDATE ClientInfo SET " +
"`location`= null, `port`= null, " +
"`status`='OFFLINE', `token`= '' " +
clientQueryStr + ";");

Response response = new Response.Builder
(Xcr3TProtocol.RESPONSE_200_OK)
.setDestinationPublicKey(clientJSON
.getString("publicKey"))
.put("status", "OK")
.put("goodbye", "true")
.build();

doneFlag = sendResponse(socket, response);

} catch (JSONException e) {
throw new IllegalStateException("JSON Error");
} catch (SQLException e) {
e.printStackTrace();
throw new IllegalStateException("SQL Error");
}
}
if (!doneFlag) {
throw new IllegalStateException("Unsupported Function");
}
bufferedReader.close();
closeSocket(socket);

} catch (IllegalStateException e) {
String err = "Received a bad/empty request: " + e
.getMessage();
Response response = new Response.Builder(Xcr3TProtocol
.RESPONSE_400_BAD_REQUEST)
.put("status", "error")
.put("error", e.getMessage())
.build();

sendResponse(socket, response);
System.out.println(err);
closeSocket(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

private boolean sendResponse(Socket socket, Response response) throws
IOException {
BufferedWriter bufferedWriter = new BufferedWriter(new
OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(response.toString());
bufferedWriter.flush();
bufferedWriter.close();
return true;
}

private void closeSocket(Socket socket) throws IOException {
socket.close();
clientSocketList.remove(mUUID);
//System.out.println("DEL:" + getUUID() + "/ " + getClientServingCount() + " left");
}
}

客户端通信相关操作

客户端在登录后将开启一个SocketServer,并提交该SocketServer的IP和端口号给服务器记录。当有其他客户端向服务器请求与该客户端进行通信时,服务器将返回该客户端的IP和端口号。其他客户端将直接对该地址创建一个socket进行连接。

功能截图

连接用户

控制台-c命令连接用户

开始聊天

开始聊天

关闭会话

控制台-d关闭会话

提示对话已关闭

代码实现

客户端生成类 Xcr3TClient.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
public class Xcr3TClient {

public static final String CLIENT_NAME = "Kavel's Xcr3Tchat Client/0.1";
public final static String SERVERNAME = "localhost";
public final static int SERVERPORT = 54213;
private final static String SERVER_PUBLIC_KEY =
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCUC8HZmsk2fdHBYTaucuqkSN2EeeKUmcnqrPMg\n" +
"9RXxr3QaY8xwseP625eMS70rfgaz/0LmAHenm6rvKkWlGE1M3dr6RwOTXlNAbEW0c1fpfMqY9dd6\n" +
"PNw5jn7JxjIrVakscO+eDTRsRq1OX9LHW" +
"+qkswjt2RkSIo9ffvTL96n3SQIDAQAB";
protected static KeyPairGenerator mKeyPairGenerator;
private String mUID;
private String mPassword;
private ServerSocket mServerSocket;
private Chattable mChatter;
private ChatHandler mChatHandler;
private int mPort;
private String mPrivateKey; //己方PrK
private String mToken = "";

public Xcr3TClient(String uid, String psw, Chattable chatter) {
mUID = uid;
mPassword = psw;
mChatter = chatter;
try {
mKeyPairGenerator = KeyPairGenerator.getInstance("RSA");
mKeyPairGenerator.initialize(1024, new SecureRandom());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}

protected static void send(Socket socket, Object request) throws
IOException {

System.out.println(request.toString());
BufferedWriter bufferedWriter = new BufferedWriter(new
OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(request.toString());
bufferedWriter.flush();
}

protected static ResponseParser parse(Socket socket, String BASE64PriKey)
throws IOException {
BufferedReader bufferedReader = new BufferedReader(new
InputStreamReader(socket.getInputStream()));
String line;
StringBuilder header = new StringBuilder();

while ((line = bufferedReader.readLine()) != null) {
header.append(line);
header.append("\r\n");
if (line.isEmpty())
break;
}
StringBuilder msg = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
msg.append(line);
msg.append("\r\n");
if (line.isEmpty())
break;
}

ResponseParser parser = new ResponseParser(header.toString(), msg
.toString(), BASE64PriKey);

System.out.println(parser.getJSON().toString());
return parser;
}

public void setChatter(Chattable chatter) {
mChatter = chatter;
}

public String getUsername() {
return mUID;
}

public ChatHandler getChatHandler() {
return mChatHandler;
}

private String generatePublicKey() throws IOException {
KeyPair keyPair = mKeyPairGenerator.generateKeyPair();
mPrivateKey = CryptorUtil.encryptBASE64(keyPair.getPrivate()
.getEncoded()); //每次生成新KeyPair后将覆盖旧PrK
return CryptorUtil.encryptBASE64(keyPair.getPublic().getEncoded());
//己方PuK本地不保存,使用一次后交由GC处理
}

/**
* 添加当前用户
*
* @return 服务器返回添加结果
* @throws IOException
*/
public boolean register() throws IOException {

String pswMD5 = CryptorUtil.getRandomSaltedMD5(mPassword);

Request request = new Request.Builder(Xcr3TProtocol.REQUEST_ADD)
.setDestinationPublicKey(SERVER_PUBLIC_KEY)
.put("uid", mUID)
.put("identity", pswMD5)
.putSelfPublicKey(generatePublicKey())
.build();

ResponseParser parser = sendToServerAndParse(request);

if (parser.isStatusOK()) {
mChatter.printLog("You have successfully registered as: " +
getUsername());
return true;
} else {
if (parser.getJSON().has("error"))
mChatter.printLog(parser.getJSON().getString("error"));
return false;

}

}

/**
* 登录操作
*
* @return 服务器返回握手结果
* @throws IOException
*/
public boolean login() throws IOException {
mServerSocket = new ServerSocket(0);
mPort = mServerSocket.getLocalPort();
Request request = new Request.Builder(Xcr3TProtocol.REQUEST_HANDSHAKE)
.setDestinationPublicKey(SERVER_PUBLIC_KEY)
.put("uid", mUID)
.put("identity", mPassword)
.put("port", "" + mPort)
.putSelfPublicKey(generatePublicKey())
.build();

ResponseParser parser = sendToServerAndParse(request);

if (parser.isStatusOK()) {
mToken = parser.getJSON().getString("token");
new Thread(new ChatListener(mServerSocket, mChatter, getUsername
())).start();
mChatter.printLog("You have logged in as: " + getUsername());
return true;
} else {
if (parser.getJSON().has("error"))
mChatter.printLog(parser.getJSON().getString("error"));
return false;
}
}

/**
* 查询某用户在线状态,接通以后将使用Chattable.incoming()回调进行通知
*
* @param uid 要查询的用户ID
* @return 查询结果
* @throws IllegalStateException
* @throws IOException
*/
public boolean find(String uid) throws IllegalStateException, IOException {
if (mToken.isEmpty())
throw new IllegalStateException("Please login first!");

Request request = new Request.Builder(Xcr3TProtocol.REQUEST_FIND)
.setDestinationPublicKey(SERVER_PUBLIC_KEY)
.put("destUID", uid)
.put("token", mToken)
.putSelfPublicKey(generatePublicKey())
.build();

ResponseParser parser = sendToServerAndParse(request);
if (parser.isStatusERROR())
throw new IllegalStateException(parser.getJSON().getString
("error"));
if (parser.isStatusOK() && parser.getJSON().getString("ready").equals
("true")) {
String host = parser.getJSON().getString("ip");
int port = Integer.parseInt(parser.getJSON().getString("port"));
Socket s = new Socket(host, port);
new ChatHandler(s, mChatter, getUsername());
return true;
} else
return false;
}

public void logout() throws IOException {
Request request = new Request.Builder(Xcr3TProtocol.REQUEST_GOODBYE)
.setDestinationPublicKey(SERVER_PUBLIC_KEY)
.put("uid", mUID)
.put("token", mToken)
.putSelfPublicKey(generatePublicKey())
.build();
ResponseParser parser = sendToServerAndParse(request);
if (parser.isStatusOK()) {
mToken = "";
mServerSocket.close();
mPort = 0;

mChatter.printLog("You (" + getUsername() + ") have logged out.");
}
}

/**
* 向服务器发送请求并返回结果
*
* @param request 经Request类包装后的请求对象
* @return 经ResponseParser类包装后的返回结果对象
* @throws IOException
*/
private ResponseParser sendToServerAndParse(Request request) throws
IOException {

Socket socket = new Socket(SERVERNAME, SERVERPORT);
send(socket, request);
ResponseParser parser = parse(socket, mPrivateKey);
mPrivateKey = null; //使用一次后清除该私钥
socket.close();
return parser;
}
}
会话操作器 ChatHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
public class ChatHandler {

private Socket mSocket;
private Chattable mChatter;
private Map<String, String> mPrivateKeyChain;
private Map<String, String> mPublicKeyChain;
private Stack<String> mKeyIDList;
private String mSelfUsername;
private String mOppositeUsername;
private Object keyListLock = new Object();

public ChatHandler(Socket s, Chattable chatter, String selfUsername)
throws IOException {
mSelfUsername = selfUsername;
mSocket = s;
mChatter = chatter;
mPrivateKeyChain = new HashMap<>();
mPublicKeyChain = new HashMap<>();
mKeyIDList = new Stack<>();
new Thread(new CharReceiver()).start();
sendHandshakeMsg();

}

public String getSelfUsername() {
return mSelfUsername;
}

public String getOppositeUsername() {
return mOppositeUsername;
}

public String getLink() {
return mSelfUsername + " -> " + mOppositeUsername;
}

public void sendHandshakeMsg() throws IOException {
String selfKeyID = UUID.randomUUID().toString();
String selfKey = generateSelfPublicKey(selfKeyID);
Request request = new Request.Builder(Xcr3TProtocol.REQUEST_HANDSHAKE)
.put("uid", mSelfUsername)
.putEncryptID(selfKeyID)
.putSelfPublicKey(selfKey)
.build();
Xcr3TClient.send(mSocket, request);
}

public void sendChat(String chat) throws IOException {

//STEP1: Request key
Request keyRequest = new Request.Builder(Xcr3TProtocol.REQUEST_GET_KEY)
.build();
Xcr3TClient.send(mSocket, keyRequest);

//STEP2: Check keyList
synchronized (keyListLock) {

while (mKeyIDList.empty()) {
try {
keyListLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//STEP3: Send message using receivedKey
String destKeyID = mKeyIDList.pop();
String destKey = mPublicKeyChain.remove(destKeyID);


Request request = new Request.Builder(Xcr3TProtocol.REQUEST_CHAT)
.setDestinationPublicKey(destKey)
.put("chat", chat)
.putDecryptID(destKeyID)
.build();
Xcr3TClient.send(mSocket, request);
}

public void disconnect() throws IOException {
Request request = new Request.Builder(Xcr3TProtocol.REQUEST_GOODBYE)
.build();
Xcr3TClient.send(mSocket, request);
}


private String generateSelfPublicKey(String keyID) throws IOException {
KeyPair keyPair = Xcr3TClient.mKeyPairGenerator.generateKeyPair();
mPrivateKeyChain.put(keyID, CryptorUtil.encryptBASE64(keyPair
.getPrivate().getEncoded()));
return CryptorUtil.encryptBASE64(keyPair.getPublic().getEncoded());
//己方PuK本地不保存,使用一次后交由GC处理
}

private class CharReceiver implements Runnable {

@Override
public void run() {
try {
while (mSocket.isConnected()) {
RequestParser parser = parseChat(mSocket);
JSONObject chatJSON = parser.getJSON();
//System.out.println(parser.getProtocolHeader() + "\r\n"
// + chatJSON.toString());

if (!parser.isChat())
throw new IllegalStateException("It's not a chat");

if (parser.isProtocolHeader(Xcr3TProtocol
.REQUEST_GET_KEY)) {
String selfKeyID = UUID.randomUUID().toString();
String selfKey = generateSelfPublicKey(selfKeyID);
Response keyResponse = new Response.Builder
(Xcr3TProtocol.RESPONSE_200_OK)
.setResponseName(Xcr3TClient.CLIENT_NAME)
.put("encryptID", selfKeyID)
.put("publicKey", selfKey)
.build();
Xcr3TClient.send(mSocket, keyResponse);
}

if (parser.isProtocolHeader(Xcr3TProtocol.REQUEST_CHAT)) {
String decryptID = chatJSON.getString("decryptID");
String decryptKey = mPrivateKeyChain.remove(decryptID);
String chatText = CryptorUtil.unpack(decryptKey,
chatJSON.getString("chat"));
//TODO: 输出内容
mChatter.showChat(getSelfUsername() + " <- " +
getOppositeUsername() + ": " + chatText);
}

if (parser.isProtocolHeader(Xcr3TProtocol
.RESPONSE_200_OK) && chatJSON.has("publicKey")) {
String destKey = chatJSON.getString("publicKey");
String destKeyID = chatJSON.getString("encryptID");
synchronized (keyListLock) {
mKeyIDList.push(destKeyID);
mPublicKeyChain.put(destKeyID, destKey);
keyListLock.notify();
}
}

if (parser.isProtocolHeader(Xcr3TProtocol
.REQUEST_HANDSHAKE)) {
mOppositeUsername = chatJSON.getString("uid");
mChatter.incoming(ChatHandler.this);
}

if (parser.isProtocolHeader(Xcr3TProtocol
.REQUEST_GOODBYE)) {
Response keyResponse = new Response.Builder
(Xcr3TProtocol.RESPONSE_200_OK)
.setResponseName(Xcr3TClient.CLIENT_NAME)
.put("goodbye", "true")
.build();
Xcr3TClient.send(mSocket, keyResponse);
mChatter.disconnecting(ChatHandler.this);
mSocket.close();
}
if (parser.isProtocolHeader(Xcr3TProtocol
.RESPONSE_200_OK) && chatJSON.has("goodbye")) {
if (chatJSON.getString("goodbye").equals("true"))
mChatter.disconnecting(ChatHandler.this);
mSocket.close();

}
}
} catch (IOException e) {
mChatter.printLog("Connection Closed: " + getLink());
mChatter.disconnecting(ChatHandler.this);
}
}

private RequestParser parseChat(Socket socket) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new
InputStreamReader(socket.getInputStream()));
String line;
StringBuilder chatContent = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
chatContent.append(line);
chatContent.append("\r\n");
if (line.isEmpty())
break;
}

if (chatContent.toString().startsWith(Xcr3TProtocol
.RESPONSE_200_OK)) {
while ((line = bufferedReader.readLine()) != null) {
chatContent.append(line);
chatContent.append("\r\n");
if (line.isEmpty())
break;
}
}

return new RequestParser(chatContent.toString());
}
}

}

用于跨平台通信的通信协议

为了方便后续进行跨平台通信,制定了一套 RESTful API 作为通信规范。发送方使用Request.Builder()Response.Builder()生成符合规范的请求头,使用RequestParserResponseParser解析符合规范的请求。

API内容

用户操作 发送至 协议头 附加数据 对方返回数据
注册用户 服务器 POST /register HTTP/1.1 {uid,identity,publicKey} {status,uid,id}
登录 服务器 POST /handshake HTTP/1.1 {uid,identity,port,publicKey} {status,token}
寻找用户 服务器 POST /find HTTP/1.1 {destUID,token} {status,valid,ready,ip,port}
[聊天]握手 对方客户端 POST /handshake HTTP/1.1 {uid,encryptID,publicKey} -
[聊天]获取公钥 对方客户端 GET /key HTTP/1.1 - {encryptID,publicKey}
[聊天]发送信息 对方客户端 POST /chat HTTP/1.1 {chat,decryptID} -
[聊天]关闭会话 对方客户端 POST /goodbye HTTP/1.1 - -
登出 服务器 POST /goodbye HTTP/1.1 {uid,token,publicKey} {status,goodbye}

代码实现

协议类 Xcr3TProtocol.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Xcr3TProtocol {

public static final String REQUEST_ADD="POST /register HTTP/1.1";
public static final String REQUEST_HANDSHAKE ="POST /handshake HTTP/1.1";
public static final String REQUEST_GET_KEY="GET /key HTTP/1.1";
public static final String REQUEST_FIND="POST /find HTTP/1.1";
public static final String REQUEST_GOODBYE ="POST /goodbye HTTP/1.1";

public static final String REQUEST_CHAT="POST /chat HTTP/1.1";

public static final String RESPONSE_200_OK="HTTP/1.1 200 OK";
public static final String RESPONSE_400_BAD_REQUEST="HTTP/1.1 400 Bad Request";

}
请求生成类 Request.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class Request {
private String mProtocolHeader;
private JSONObject mJSON;

private Request() {
}

private Request(Builder builder) {
mProtocolHeader = builder.mProtocolHeader;
mJSON = builder.mJSON;
}

@Override
public String toString() {
StringBuilder request = new StringBuilder();
request.append(mProtocolHeader);
request.append("\r\n");
request.append(mJSON.length() > 0 ? mJSON.toString() : "");
request.append("\r\n\r\n");
return request.toString();
}

public static class Builder {
private String mProtocolHeader;
private String mDestPublicKey;
private JSONObject mJSON;
private boolean needEncrypt;

public Builder(String protocolHeader) {
mProtocolHeader = protocolHeader;
mJSON = new JSONObject();
}

public Builder setDestinationPublicKey(String BASE64PubKey) {
needEncrypt = true;
mDestPublicKey = BASE64PubKey;
return this;
}

public Builder put(String key, String value) throws IOException {
String encryptedValue;
if (needEncrypt)
encryptedValue = CryptorUtil.pack(mDestPublicKey, value);
else
encryptedValue = value;
mJSON.put(key, encryptedValue);
return this;
}

public Builder putEncryptID(String id) {
mJSON.put("encryptID", id);
return this;
}

public Builder putDecryptID(String id) {
mJSON.put("decryptID", id);
return this;
}

public Builder putSelfPublicKey(String BASE64PubKey) {
mJSON.put("publicKey", BASE64PubKey);
return this;
}

public Request build() {
return new Request(this);
}
}

}
回应生成类 Response.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class Response {

private static final String SERVER_NAME = "Kavel's Xcr3Tchat Server/0.1";
private String mResponseName;
private String mProtocolHeader;
private int mContentLength = 0;
private JSONObject mJSON;
private String mEncryptedResponse;

private Response() {
}

private Response(Builder builder) {
mResponseName = builder.mResponseName;
mProtocolHeader = builder.mProtocolHeader;
mEncryptedResponse = builder.mEncryptedResponse;
mContentLength = mEncryptedResponse.length();
mJSON = builder.mJSON;
}

public String getServerName() {
return mResponseName;
}

public String getProtocolHeader() {
return mProtocolHeader;
}

public JSONObject getJSON() {
return mJSON;
}

public int getContentLength() {
return mContentLength;
}

@Override
public String toString() {
StringBuilder response = new StringBuilder();
response.append(mProtocolHeader + "\r\n");
response.append("Server: " + mResponseName + "\r\n");
response.append("Content-Length: " + mContentLength + "\r\n");
response.append("\r\n");
response.append(mEncryptedResponse);
response.append("\r\n\r\n");
return response.toString();
}

public static class Builder {

private String mResponseName;
private String mProtocolHeader;
private String mDestPublicKey;
private JSONObject mJSON;
private String mEncryptedResponse;
private boolean needEncrypt;

public Builder(String protocolHeader) {
mProtocolHeader = protocolHeader;
mJSON = new JSONObject();
mResponseName = SERVER_NAME;
}

public Builder setResponseName(String name) {
mResponseName = name;
return this;
}

public Builder setDestinationPublicKey(String BASE64PubKey) {
needEncrypt = true;
mDestPublicKey = BASE64PubKey;
return this;
}

public Builder put(String key, String value) throws IOException {
mJSON.put(key, value);
return this;
}

public Response build() throws IOException {
if (needEncrypt)
mEncryptedResponse = CryptorUtil.pack(mDestPublicKey, mJSON
.toString());
else
mEncryptedResponse = mJSON.toString();
return new Response(this);
}

}

}
请求解析类 RequestParser.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class RequestParser {
private final String mRawRequest;
private String mMethod;
private String mProtocolHeader;
private JSONObject mJSON;
private HashMap<String, String> mQueryMap = new HashMap<>();
private boolean isBadRequest;

public RequestParser(String rawRequest) {
mRawRequest = rawRequest;
if (mRawRequest.isEmpty()) {
isBadRequest = true;
throw new IllegalStateException("Empty Request");
}
parseRequest();
}

private RequestParser(String rawRequest, String method, String func) {
mRawRequest = rawRequest;
mMethod = method;
}

public String getRawRequest() {
return mRawRequest;
}

public String getRequestMethod() {
return mMethod;
}

public boolean isRequestMethod(String methodType) {
return mMethod.equals(methodType);
}

public String getProtocolHeader() {
return mProtocolHeader;
}

public boolean isProtocolHeader(String protocolHeader) {
return mProtocolHeader.startsWith(protocolHeader);
}

public boolean isChat() {
return isProtocolHeader(Xcr3TProtocol.REQUEST_CHAT)
|| isProtocolHeader(Xcr3TProtocol.REQUEST_HANDSHAKE)
|| isProtocolHeader(Xcr3TProtocol.REQUEST_GET_KEY)
|| isProtocolHeader(Xcr3TProtocol.REQUEST_GOODBYE)
|| isProtocolHeader(Xcr3TProtocol.RESPONSE_200_OK);
}

public JSONObject getJSON() {
return mJSON;
}

public HashMap<String, String> getQueryMap() {
return mQueryMap;
}

public String getQueryValue(String key) {
return mQueryMap.get(key);
}

public boolean isBadRequest() {
return isBadRequest;
}

public static RequestParser generateBadRequest() {
return new RequestParser("", "", "/bad.request");
}

private void parseRequest() {

mProtocolHeader = mRawRequest.substring(0, mRawRequest.indexOf
("\r\n")).trim();
mMethod = mProtocolHeader.substring(0, mProtocolHeader.indexOf("/"))
.trim();

try {
if (mRawRequest.contains("{")) {
String jsonRaw = mRawRequest.substring(mRawRequest.indexOf
("{"), mRawRequest.indexOf("}") + 1);
mJSON = new JSONObject(jsonRaw);
}
} catch (Exception e) {
throw new IllegalStateException("No JSON Found");
}

}

private void fillQueryMap(String queryString) {
for (String querySet : queryString.split("&")) {
String queryKey = querySet.split("=")[0];
String queryValue = "";
try {
queryValue = querySet.split("=")[1];
} catch (ArrayIndexOutOfBoundsException e) {
queryValue = "";
}
mQueryMap.put(queryKey, queryValue);
}
}
}
回应解析类 ResponseParser.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class ResponseParser {
private String mHeader;
private String mMsg;
private JSONObject mJSON;
private String mBASE64PriKey;
private boolean isOK;

public ResponseParser(String header, String msg, String BASE64PriKey) {
mHeader = header.split("\n")[0].trim();
mMsg = msg;
mBASE64PriKey = BASE64PriKey;
if (mHeader.isEmpty())
throw new IllegalStateException("Empty Response");
parseResponse();
}

public JSONObject getJSON() {
return mJSON;
}

public boolean isResponse(String protocolHeader) {
return mHeader.equals(protocolHeader);
}

public boolean isResponseOK() {
return isOK;
}

public boolean isStatusOK() {
return isOK && mJSON.get("status").equals("OK");
}

public boolean isStatusERROR() {
return mJSON.has("status") && mJSON.get("status").equals("error");
}

private void parseResponse() {
String decryptedMsg = "";

if (mHeader.equals(Xcr3TProtocol.RESPONSE_200_OK)) {
isOK = true;
try {
byte[] data = CryptorUtil.cryptData(mBASE64PriKey,
CryptorUtil.decryptBASE64(mMsg), Cipher.DECRYPT_MODE);
decryptedMsg = new String(data, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
} else if (mHeader.equals(Xcr3TProtocol.RESPONSE_400_BAD_REQUEST)) {
isOK = false;
decryptedMsg = mMsg;

}
mJSON = new JSONObject(decryptedMsg);
}
}

用于快速开发的客户端适配器

为了能够进行客户端的快速开发,创建了一套Java API,通过调用相关方法即可快速开发出一个符合规范的Java客户端。

API分为三部分,分别是:

  • Chattable接口:提供信息的回调方法。
  • Xcr3TClient类:客户端主类,可调用其公有方法完成上述所有用户操作。
  • Xcr3TAdapter类:客户端适配器类,支持多客户端、命令行操作,传入Chattable类后能够支持信息回显。

代码实现

信息回调接口 Chattable.java
1
2
3
4
5
6
7
public interface Chattable {
void incoming(ChatHandler handler);
void disconnecting(ChatHandler handler);
void showChat(String chat);
void printLog(String message);
void updateUI();
}
客户端适配器类 Xcr3TAdapter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
public class Xcr3TAdapter {

Chattable mChatter;
private Xcr3TClient mCurrentIdentity;
private ChatHandler mCurrentChatHandler;
private Map<String, Xcr3TClient> mIdentityMap = new HashMap<>();
private Map<String, Map<String, ChatHandler>> mChatHandlerMap = new
HashMap<>();
private Options mCliOptions;
private HelpFormatter mHelpFormatter;

public Xcr3TAdapter(Chattable chatter) {

mChatter = chatter;
initCli();

}

public Xcr3TClient getCurrentIdentity() {
return mCurrentIdentity;
}

public ChatHandler getCurrentChatHandler() {
return mCurrentChatHandler;
}

public Map<String, Xcr3TClient> getIdentityMap() {
return mIdentityMap;
}

public Map<String, ChatHandler> getChatHandlerMap(String identity) {
if (!mChatHandlerMap.containsKey(identity))
mChatHandlerMap.put(identity, new HashMap<>());
return mChatHandlerMap.get(identity);
}

public void login(String uid, String psw) {
if (mIdentityMap.containsKey(uid))
throw new IllegalStateException("already logged in.");
addIdentity(uid, psw);
}

public void logout() {
if (mCurrentIdentity == null)
throw new IllegalStateException("please select an identity to log" +
" out.");
logout(mCurrentIdentity.getUsername());
}

public void logout(String identity) throws IllegalStateException {
if (!mIdentityMap.containsKey(identity))
throw new IllegalStateException("Invalid identity");
Xcr3TClient client = mIdentityMap.get(identity);
try {
rmIdentity(client);
} catch (IOException e) {
e.printStackTrace();
}
}

public void register(String uid, String psw1, String psw2) {
if (!psw1.equals(psw2))
throw new IllegalStateException("password must be the same.");
Xcr3TClient client = new Xcr3TClient(uid, psw2, mChatter);
try {
if (client.register())
addIdentity(client);
} catch (IOException e) {
throw new IllegalStateException("Couldn't connect to server.");
}
}

public void switchIdentity(String identity) {
if (mIdentityMap.containsKey(identity)) {
setCurrentClient(identity);
} else
throw new IllegalStateException("Invalid Identity.");
}

public void connect(String uid) {
try {
mCurrentIdentity.find(uid);
} catch (IOException e) {
throw new IllegalStateException("Couldn't connect to server.");
}
}

public void disconnect() {
if (mCurrentChatHandler == null)
throw new IllegalStateException("please select an contact to " +
"disconnect.");
disconnect(mCurrentChatHandler.getLink());
}

public void disconnect(String link) {
String identity = link.split("->")[0].trim();
String contact = link.split("->")[1].trim();

if (!getIdentityMap().containsKey(identity)
|| !getChatHandlerMap(identity).containsKey(contact))
throw new IllegalStateException("Invalid link");
ChatHandler handler = getChatHandlerMap(identity).get(contact);
try {
handler.disconnect();
} catch (IOException e) {
throw new IllegalStateException("Couldn't connect to server.");
}

}

public void forward(String contact) {
String identity = mCurrentIdentity.getUsername();
String link = identity + " -> " + contact;
if (mChatHandlerMap.containsKey(identity) && mChatHandlerMap.get
(identity).containsKey(contact)) {
mCurrentChatHandler = mChatHandlerMap.get(identity).get(contact);
setCurrentHandler(link);
} else
throw new IllegalStateException("Invalid forward");
}

public void cmd(String[] args) {
try {
CommandLineParser parser = new DefaultParser();
CommandLine line = parser.parse(mCliOptions, args);

if (line.hasOption("h")) {
StringWriter sw = new StringWriter();
mHelpFormatter.printHelp(new PrintWriter(sw), 100, "[command]" +
" [message]",
"", mCliOptions, 0, 0, "");
mChatter.printLog(sw.toString());
}
if (line.hasOption("l")) {
String uid = line.getOptionValues("l")[0];
String psw = line.getOptionValues("l")[1];
login(uid, psw);
}
if (line.hasOption("o")) {
String identity = line.getOptionValue("o");
if (identity == null)
logout();
else
logout(identity);
}
if (line.hasOption("r")) {
String uid = line.getOptionValues("r")[0];
String psw1 = line.getOptionValues("r")[1];
String psw2 = line.getOptionValues("r")[2];
register(uid, psw1, psw2);
}
if (line.hasOption("s")) {
String identity = line.getOptionValue("s");
switchIdentity(identity);

}
if (line.hasOption("c")) {
String uid = line.getOptionValue("c");
connect(uid);
}
if (line.hasOption("d")) {
String link = line.getOptionValue("d");
if (link == null)
disconnect();
else
disconnect(link);
}
if (line.hasOption("f")) {
String contact = line.getOptionValue("f");
forward(contact);
}

String chatText = String.join(" ", line.getArgs());

if (chatText.isEmpty()) {
mChatter.updateUI();
return;
}

if (mCurrentChatHandler == null)
throw new IllegalStateException("Oops! we don't know who to " +
"be sent to! Please select a contact!");

if (mCurrentIdentity == null)
throw new IllegalStateException("Hi stranger! Please log in " +
"or select your identity!");

mCurrentChatHandler.sendChat(chatText);
mChatter.showChat(mCurrentChatHandler.getSelfUsername() + " -> "
+ mCurrentChatHandler.getOppositeUsername() + ": " +
chatText);
mChatter.updateUI();


} catch (ParseException e) {
StringWriter sw = new StringWriter();
mHelpFormatter.printHelp(new PrintWriter(sw), 100, "[command] " +
"[message]",
"", mCliOptions, 0, 0, "");
mChatter.printLog(sw.toString());
} catch (IllegalStateException e) {
mChatter.printLog(e.getMessage());
} catch (IOException e) {
e.printStackTrace();
}
}

private void initCli() {
mHelpFormatter = new HelpFormatter();
mCliOptions = new Options();
mCliOptions.addOption("h", "help", false, "print help for the command" +
".");
mCliOptions.addOption(
Option.builder("l").longOpt("login")
.numberOfArgs(2)
.valueSeparator(' ')
.argName("uid password")
.desc("login using uid and password")
.build());
mCliOptions.addOption(
Option.builder("o").longOpt("logout")
.hasArg()
.optionalArg(true)
.argName("identity")
.desc("logout current identity, or specified ideneity")
.build());
mCliOptions.addOption(
Option.builder("r").longOpt("register")
.numberOfArgs(3)
.argName("uid password re-enter-password")
.desc("register using uid and password")
.build());
mCliOptions.addOption(
Option.builder("s").longOpt("switch")
.numberOfArgs(1)
.argName("identity")
.desc("switch to available identity")
.build());
mCliOptions.addOption(
Option.builder("c").longOpt("connect")
.numberOfArgs(1)
.argName("uid")
.desc("connect to a contact")
.build());
mCliOptions.addOption(
Option.builder("d").longOpt("disconnect")
.hasArg()
.optionalArg(true)
.argName("link")
.desc("disconnect current link, or specified link")
.build());
mCliOptions.addOption(
Option.builder("f").longOpt("forward")
.numberOfArgs(1)
.argName("uid")
.desc("send chat to specified contact")
.build());
}

private void addIdentity(String uid, String psw) {
Xcr3TClient client = new Xcr3TClient(uid, psw, mChatter);
addIdentity(client);
}

public void addIdentity(Xcr3TClient client) {
try {
if (client.login()) {
mIdentityMap.put(client.getUsername(), client);
mCurrentIdentity = client;
mChatter.updateUI();
}
} catch (IOException e) {
throw new IllegalStateException("Cannot log Identity: " + client
.getUsername());
}
}

public void rmIdentity(Xcr3TClient client) throws IOException {
mCurrentIdentity = client;
String identity = mCurrentIdentity.getUsername();
Map<String, ChatHandler> handlerMap = mChatHandlerMap.get(identity);
for (ChatHandler handler : handlerMap.values()) {
handler.disconnect();
}
handlerMap.clear();
mCurrentIdentity.logout();
mIdentityMap.remove(identity);
mCurrentIdentity = null;
mChatter.updateUI();
}

public void addContact(ChatHandler handler) {
String identity = handler.getSelfUsername();
String contact = handler.getOppositeUsername();
getChatHandlerMap(identity).put(contact, handler);
mCurrentChatHandler = handler;
mChatter.printLog("contact connected:" + handler.getLink());
mChatter.updateUI();
}

public void rmContact(ChatHandler handler) {
mCurrentChatHandler = handler;
String identity = mCurrentChatHandler.getSelfUsername();
String contact = mCurrentChatHandler.getOppositeUsername();
getChatHandlerMap(identity).remove(contact);
mCurrentChatHandler = null;
mChatter.updateUI();
}

public void setCurrentClient(String identity) {
if (identity != null) {
if (mCurrentIdentity == null || (!identity.equals
(mCurrentIdentity.getUsername()) && getIdentityMap()
.containsKey(identity))) {
mChatter.printLog("switching to Identity: " + identity);
mCurrentIdentity = mIdentityMap.get(identity);
mChatter.updateUI();
}
}
}

public void setCurrentHandler(String link) {
if (link != null) {
String identity = link.split(" -> ")[0];
String contact = link.split(" -> ")[1];
if (!link.equals(mCurrentChatHandler.getLink())
&& getIdentityMap().containsKey(identity)
&& getChatHandlerMap(identity).containsKey(contact)) {
mChatter.printLog("switching to connection: " + link);
mCurrentChatHandler = mChatHandlerMap.get(identity).get
(contact);
mChatter.updateUI();
}
}
}

}

附:快速开发示例 ChatterUI

ChatterUI 是一个使用 Java API 进行快速开发的示例。它通过创建Xcr3TAdapter对象对多个客户端进行管理,实现Chattable接口以实现信息回显的功能。在文本框内键入命令行进行操作。也可以鼠标点选右方的列表栏进行客户端与会话的管理操作。ChatterUI截图

代码实现

ChatterUI.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
public class ChatterUI implements Chattable {
private JTextField cmdField;
private JPanel panel;
private JButton SENDButton;
private JTextPane chatPane;
private JList lsIdentity;
private JList lsContact;
private JScrollPane scrollPane;
private DefaultListModel<String> mListModelIdentity;
private DefaultListModel<String> mListModelContact;


private Xcr3TAdapter adapter;


public ChatterUI() {
adapter = new Xcr3TAdapter(this);

mListModelIdentity = new DefaultListModel<>();
mListModelContact = new DefaultListModel<>();
lsIdentity.setModel(mListModelIdentity);
lsContact.setModel(mListModelContact);

SENDButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String command = cmdField.getText();
cmdField.setText("");
adapter.cmd(command.split(" "));
}
});
lsIdentity.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting())
adapter.setCurrentClient((String) lsIdentity
.getSelectedValue());
}
});
lsContact.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
adapter.setCurrentHandler((String) lsContact
.getSelectedValue());
}
}
});
cmdField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
if (e.getKeyCode() == KeyEvent.VK_ENTER)
SENDButton.doClick();
}
});

}

public static void main(String[] args) throws IllegalStateException {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus" +
".NimbusLookAndFeel");
} catch (Exception e) {
e.printStackTrace();
}
JFrame frame = new JFrame("ChatterUI");
frame.setContentPane(new ChatterUI().panel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}

@Override
public void incoming(ChatHandler handler) {

adapter.addContact(handler);
}

@Override
public void disconnecting(ChatHandler handler) {

adapter.rmContact(handler);
}

@Override
public void showChat(String chat) {
Document doc = chatPane.getDocument();
try {
doc.insertString(doc.getLength(), chat + "\r\n", new
SimpleAttributeSet());
chatPane.setCaretPosition(doc.getLength());
} catch (BadLocationException e) {
e.printStackTrace();
}
}

@Override
public void printLog(String message) {
Document doc = chatPane.getDocument();
try {
doc.insertString(doc.getLength(), "System:" + message + "\r\n",
new SimpleAttributeSet());
chatPane.setCaretPosition(doc.getLength());
} catch (BadLocationException e) {
e.printStackTrace();
}
}

@Override
public void updateUI() {

mListModelIdentity.clear();
mListModelContact.clear();

for (String identity : adapter.getIdentityMap().keySet()) {
mListModelIdentity.addElement(identity);
for (ChatHandler handler : adapter.getChatHandlerMap(identity)
.values()) {
mListModelContact.addElement(handler.getLink());
}
}

if (adapter.getCurrentIdentity() != null)
lsIdentity.setSelectedValue(adapter.getCurrentIdentity()
.getUsername(), true);

if (adapter.getCurrentChatHandler() != null)
lsContact.setSelectedValue(adapter.getCurrentChatHandler()
.getLink(), true);

}


}

项目总结

本次实验工程量巨大,从一开始的功能制定就画了张大饼,接下来的工作就是竭尽全力去填满这张大饼。对于每个功能的流程构思,我使用了软件工程里面的开发方式,先画出各项功能的流程图,不得不说这种方式在开发一套复杂系统时能够保持思路的清晰,不至于一不小心迷失在细节的深渊当中。在构思和实现这套加密算法时,我对RSA、MD5操作、字符串转BASE64操作有了更深刻的认识。而写的这个加密类是我非常满意的一个地方。这个加密类使得复杂的加密解密过程被包装在了简单的两个方法pack()和unpack()中,使得编程效率大大的提高,这也是全局RSA加密得以实现的基础。对于跨平台开发,虽然这次项目提交的时候并没有实现一个跨平台的客户端,但是为此制定的一套RESTful API也是本次项目的一个亮点。因为通过这套API确实看到了跨平台开发的曙光。虽然这个作业提交了,但是这个项目还远远没有结束。我从中看到了一个非常大的可能性,所以并不会放弃该项目,会将持续对其进行开发。

#

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×