赞
踩
仅供参考,请勿直接抄袭,抄袭者后果自负。
仓库地址:
后端地址:https://github.com/yijunquan-afk/safechat-server
前端地址: https://github.com/yijunquan-afk/safechat-client
CosUpload.java中的COS设置,需要自己配
结合所学安全机制设计实现一个简单的安全通信软件,包含机密性,消息认证等基本功能。并考虑其中涉及的密钥分配方式与机密性算法等相关问题的解决.实现方法不限,使用机制不限。
要求:
1、 独立完成
2、 具有完整的流程设计,报文格式等相关分析。
3、 具备自圆其说的安全性设计思考
SHA-2,名称来自于安全散列算法2(英语:Secure Hash Algorithm 2)的缩写,一种密码散列函数算法标准,由美国国家安全局研发[3],由美国国家标准与技术研究院(NIST)在2001年发布。属于SHA算法之一,是SHA-1的后继者。其下又可再分为六个不同的算法标准,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。
RSA加密算法是一种非对称加密算法,在公开密钥加密和电子商业中被广泛使用。RSA是由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)在1977年一起提出的。当时他们三人都在麻省理工学院工作。RSA 就是他们三人姓氏开头字母拼在一起组成的。
对极大整数做因数分解的难度决定了 RSA 算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA 算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用 RSA 加密的信息的可靠性就会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的 RSA 钥匙才可能被强力方式破解。到2020年为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被破解的。
WebSocket是双向的,在客户端-服务器通信的场景中使用的全双工协议,与HTTP不同,它以ws://或wss://开头。它是一个有状态协议,这意味着客户端和服务器之间的连接将保持活动状态,直到被任何一方(客户端或服务器)终止。在通过客户端和服务器中的任何一方关闭连接之后,连接将从两端终止。
以客户端-服务器通信为例,每当启动客户端和服务器之间的连接时,客户端-服务器进行握手随后创建一个新的连接,该连接将保持活动状态,直到被他们中的任何一方终止。建立连接并保持活动状态后,客户端和服务器将使用相同的连接通道进行通信,直到连接终止。
新建的连接被称为WebSocket。一旦通信链接建立和连接打开后,消息交换将以双向模式进行,客户端-服务器之间的连接会持续存在。如果其中任何一方(客户端服务器)宕掉或主动关闭连接,则双方均将关闭连接。套接字的工作方式与HTTP的工作方式略有不同,状态代码101表示WebSocket中的交换协议。
JWT就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密,签名等相关处理。
首先,前端通过Wb表单将自己的用戶名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。
2、后端核对用戶名和密码成功后,将用戶的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同11.Zzz.xx的字符串。token head.payload.signature
3、后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage.上,退出登录时前端删除保存的JWT即可。
4、前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
5、后端检查JWT是否存在,如存在验证JWT的有效性。检查签名是否正确,检查Token是否过期,检查Token的接收方是否是自己(可选)
jwt生成的字符串包含有三部分
1、 jwt头信息部分header:标头通常由两部分组成:令牌的类型(即JWT所使用的签名算法,例如HMAC、SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。
2、 在效载荷Payload:令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用戶)和其他数据的声明。同样的,它会使用Ba$64编码组成JWT结构的第二部分
3、 签名哈希Signature:header和payload都是结果Base64编码过的,中间用.隔开,第三部分就是前面两部分合起来做签名,密钥绝对自己保管好,签名值同样做Base64编码拼接在JWT后面。(签名并编码)
高级加密标准(英语:Advanced Encryption Standard,缩写:AES),又称Rijndael加密法(荷兰语发音: [ˈrɛindaːl],音似英文的“Rhine doll”),是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。现在,高级加密标准已然成为对称密钥加密中最流行的算法之一。
严格地说,AES和Rijndael加密法并不完全一样(虽然在实际应用中两者可以互换),因为Rijndael加密法可以支持更大范围的区块和密钥长度:AES的区块长度固定为128比特,密钥长度则可以是128,192或256比特;而Rijndael使用的密钥和区块长度均可以是128,192或256比特。加密过程中使用的密钥是由Rijndael密钥生成方案产生。
大多数AES计算是在一个特别的有限域完成的。
AES加密过程是在一个4×4的字节矩阵上运作,这个矩阵又称为“体(state)”,其初值就是一个明文区块(矩阵中一个元素大小就是明文区块中的一个Byte)。(Rijndael加密法因支持更大的区块,其矩阵的“列数(Row number)”可视情况增加)加密时,各轮AES加密循环(除最后一轮外)均包含4个步骤:
① AddRoundKey—矩阵中的每一个字节都与该次回合密钥(round key)做XOR运算;每个子密钥由密钥生成方案产生。
② SubBytes—透过一个非线性的替换函数,用查找表的方式把每个字节替换成对应的字节。
③ ShiftRows—将矩阵中的每个横列进行循环式移位。
④ MixColumns—为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每内联的四个字节。最后一个加密循环中省略MixColumns步骤,而以另一个AddRoundKey取代。
本次设计中,我使用了HTTP协议处理一般的网络请求:如登录、注册、好友列表获取、个人信息获取、头像更新等功能。
而好友之间点对点的通信,为了持续快速地沟通,我是用WebSocket协议来处理信息发送请求。
客户端负责的是与用户进行交互,因此在实用之外还需要考虑到界面美观整洁,以给用户带来良好的使用体验。因此,前端选择使用 vue + AntDesign 组件库进行界面构建。另一方面,由于需要建立 WebSocket 连接,发送 WebSocket 请求,因此需要引入 WebSocket 相关功能的实现。这里使用的是 socket.io 这一 NodeJS 第三方模块。
对于服务端,采用了 Java + SpringBoot 为大框架来进行服务端的开发。数据库采用的是经典的关系型数据库 MySql。同时为了建立 WebSocket 连接,处理 WebSocket 请求,选择了 socket.io 的一个 Java 移植版本 netty-socketio。netty-socketio是一个开源的Socket.io服务器端的一个java的实现,它基于Netty框架,可用于服务端推送消息给客户端。
本系统主要包含六个大的功能模块:登陆注册、用户信息获取、信息发送、好友列表显示、头像上传以及退出系统。其中信息发送是本次课程设计最重要的部分,是安全通信的主要体现。
private static String sign(String userId,String password){ Algorithm algorithm = Algorithm.HMAC256(password); String token = JWT.create() .withClaim(CLAIM_USERID_NAME,userId) .withExpiresAt(new Date(System.currentTimeMillis()+EXPIRED_TIME/2)) .sign(algorithm); return token; } /** * 生成一个登录token * @param userId * @param password * @return */ public static String loginSign(String userId,String password){ String token = sign(userId,password); cache.putToken(token,token); return token; }
每次登录产生Token,并存储在前端的localStorage中,每次发送HTTP的POST和GET请求时加在HTTP Header中的Authorization位。(解决XSS和XSRF问题)
后端接收HTTP请求时需要认证Token。
如此做可以认证发送HTTP请求的用户身份,适用于所有HTTP请求
/** * 验证客户端传来token是否有效 * 验证逻辑顺序如下: * 1. token是否为空 * 2. token中账号是否存在 * 3. 根据token中账号从数据库中获取真实密码等用户信息,并验证用户信息是否有效 */ public static void verifyToken(String clientToken, stu.software.chatroom.common.CommonService commonService){ if(!StringUtils.hasText(clientToken)){ //token为空 throw new RuntimeException("无登录令牌!"); } //从客户端登录令牌中获取当前用户账号 String userId = JWT.decode(clientToken).getClaim(CLAIM_USERID_NAME).asString(); if(!StringUtils.hasText(userId)){ //token中账号不存在 throw new RuntimeException("登录令牌失效!"); } //取出缓存中的登录令牌 String cacheToken = cache.getToken(clientToken); if(!StringUtils.hasText(cacheToken)){ //缓存中没有登录令牌 throw new RuntimeException("登录令牌失效!"); } User user = commonService.getUserById(userId); if(user==null){ //用户不存在 throw new RuntimeException("用户不存在!"); } //验证Token有效性 try{ Algorithm algorithm = Algorithm.HMAC256(user.getU_pwd()); JWTVerifier jwtVerifier = JWT.require(algorithm).withClaim(CLAIM_USERID_NAME,userId).build();//构建验证器 jwtVerifier.verify(cacheToken); }catch(TokenExpiredException e){ //令牌过期,刷新令牌 String newToken = sign(userId,user.getU_pwd()); cache.putToken(clientToken,newToken); }catch(Exception e){ e.printStackTrace(); //令牌验证未通过 throw new RuntimeException("令牌错误!请登录。"); }
使用SHA256加密注册时用户使用的密码,数据库中存的是密文,这样可防止数据库被攻击导致密码泄露。
/*** * 利用Apache的工具类实现SHA-256加密 * @return str 加密后的报文 */ public static String getSHA256Str(String str) { MessageDigest messageDigest; String encodeSir = str; try { messageDigest = MessageDigest.getInstance("SHA-256"); byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8)); encodeSir = Hex.encodeHexString(hash); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return encodeSir; } /** * 通过该方法将密码加密(实际上并没有) */ public static String encodePwd(String u_pwd) { // 密码通过此方法解密并再加密 return getSHA256Str(u_pwd); }
登录时,前端输入明文密码,使用SHA256加密该密码以后,再加数据发送到后端。后端根据该加密后的密码与数据库比对,从而验证用户身份。
此做法避免了前端请求数据被拦截导致密码泄露。
import { sha256 } from 'js-sha256'; /** * 加密方法 */ export function PASSWORD(str) { let encodedStr = str; encodedStr = sha256(encodedStr); return encodedStr; } const login = () => { post("/user/login", { u_name: u_name.value, u_pwd: PASSWORD(u_pwd.value), }) .then((res) => { tip.success(res.message); let token = res.data; setLocalToken(token); router.push({ name: "Room", query: { usr: u_name.value } }); }) .catch((err) => { tip.error("账号密码错误!"); }); };
参考教程 https://blog.csdn.net/m0_59579040/article/details/124811147
keytool 是个密钥和证书管理工具。它使用户能够管理自己的公钥/私钥对及相关证书,用于(通过数字签名)自我认证(用户向别的用户/服务认证自己)或数据完整性以及认证服务。它还允许用户储存他们的通信对等者的公钥(以证书形式)。
在计算机网络上,OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。
通过如下步骤可以产生证书和公钥
keytool -genkeypair -storetype PKCS12 -alias yjq - -keyalg RSA -keysize 1024 -dname "CN=xxx, OU=xxx, O=xxx, L=xx, ST=xx, C=CN" -keystore D:\mygit\大三下笔记\网安课设\safechat-server\src\main\resources\keys-and-certs\yjq.keystore -keypass 123456 -storepass 123456 -validity 36500 -v
产生二进制文件yjq.keystore,以上部分可由脚本生成。
经过KeyStore的相关操作生成公钥、证书和私钥
当用户需要公钥和私钥时,只需要调用相关方法即可。
public static void genKeyPair(String name) throws Exception { //以 PKCS12 规格,创建 KeyStore KeyStore keyStore = KeyStore.getInstance("PKCS12"); path = "keys-and-certs/" + name + ".keystore"; //载入 jks 和该 jks 的密码 到 KeyStore 内 keyStore.load(new FileInputStream(new ClassPathResource("keys-and-certs/yjq.keystore").getFile()), "123456".toCharArray()); // 要获取 key,需要提供 KeyStore 的别名 和该 KeyStore 的密码 // 获取 keyStore 内所有别名 alias Enumeration<String> aliases = keyStore.aliases(); String alias = null; alias = aliases.nextElement(); char[] keyPassword = "123456".toCharArray(); keyPairString.clear(); //私钥 privateKey = (PrivateKey) keyStore.getKey(alias, keyPassword); keyPairString.put("PR", new String(Base64.getEncoder().encode(privateKey.getEncoded()))); //证书 Certificate certificate = keyStore.getCertificate(alias); //公钥 publicKey = certificate.getPublicKey(); keyPairString.put("PU", new String(Base64.getEncoder().encode(publicKey.getEncoded()))); }
参考教程https://blog.csdn.net/m0_59579040/article/details/124811147.
A和B进行通信,首先使用A的私钥对报文M进行加密——数字签名;然后A用B的公钥对上述结果进行加密——保证了保密性。
B收到消息后,用B的私钥解密,再用A的公钥验证签名。
这里我使用RSA作为加密算法、SHA1WithRSA作为签名算法,签名和加密的操作实现在类RSAUtils.java中。
/** * 私钥签名 * @param content 字符串 * @param priKey 私钥 * @return * @throws Exception */ public static byte[] sign(String content, PrivateKey priKey) throws Exception { Signature signature = Signature.getInstance(SIGALG); signature.initSign(priKey); signature.update(content.getBytes()); return signature.sign(); } /** * 公钥验证签名 * @param content 字符串 * @param sign 签名 * @param pubKey 公钥 * @return 身份是否真实 * @throws Exception */ public static boolean verify(String content, byte[] sign, PublicKey pubKey) throws Exception { Signature signature = Signature.getInstance(SIGALG); signature.initVerify(pubKey); signature.update(content.getBytes()); return signature.verify(sign); }
/** * RSA公钥加密 * * @param content 加密字符串 * @param publicKey 公钥 * @return 密文 * @throws Exception 加密过程中的异常信息 */ public static String encrypt(String content, String publicKey) throws Exception { //base64编码的公钥 byte[] decoded = Base64.getMimeDecoder().decode(publicKey); RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance(KEYALG).generatePublic(new X509EncodedKeySpec(decoded)); System.out.println(pubKey.getAlgorithm()); //RSA加密 Cipher cipher = Cipher.getInstance(KEYALG); cipher.init(Cipher.ENCRYPT_MODE, pubKey); String outStr = Base64.getEncoder().encodeToString(cipher.doFinal(content.getBytes("UTF-8"))); return outStr; } /** * RSA私钥解密 * * @param content 加密字符串 * @param privateKey 私钥 * @return 明文 * @throws Exception 解密过程中的异常信息 */ public static String decrypt(String content, String privateKey) throws Exception { //64位解码加密后的字符串 byte[] inputByte = Base64.getMimeDecoder().decode(content); // //base64编码的私钥 byte[] decoded = Base64.getMimeDecoder().decode(privateKey); RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded)); //RSA解密 Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, priKey); String outStr = new String(cipher.doFinal(inputByte)); return outStr; }
因为公钥加密的消息认证比较费时间,所以当两个用户建立消息通信时由一方产生会话密钥,使用公钥加密来传送会话密钥并认证身份。身份认证完成后,使用该会话密钥加密消息,其中使用对称加密技术AES加密消息。
消息报文格式如下:
1、 id:报文标识id;
2、 time:报文发送时间
3、 content:报文内容(加密)
4、 type:报文类型:会话密钥消息/公钥消息
5、 sender_name:发送者
6、 receiver_name:接收者
7、 sign:发送者签名。
加密过程如下:
public final class AESUtils{ private static final String ALGORITHM = "AES"; public static String genAesSecret(){ try { KeyGenerator kg = KeyGenerator.getInstance("AES"); //下面调用方法的参数决定了生成密钥的长度,可以修改为128, 192或256 kg.init(256); SecretKey sk = kg.generateKey(); byte[] b = sk.getEncoded(); String secret = Base64.encodeBase64String(b); return secret; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("没有此算法"); } } /** * 根据密钥对指定的明文plainText进行加密. * * @param plainBytes 明文 * @param keyBytes 密码 * @return 加密后的密文. * @since 0.0.8 */ public static byte[] encrypt(byte[] plainBytes, byte[] keyBytes) { try { SecretKey secretKey = getSecretKey(keyBytes); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); return cipher.doFinal(plainBytes); } catch (Exception e) { throw new RuntimeException(e); } } /** * 根据密钥对指定的密文 cipherBytes 进行解密. * * @param cipherBytes 加密密文 * @param keyBytes 秘钥 * @return 解密后的明文. * @since 0.0.8 */ public static byte[] decrypt(byte[] cipherBytes, byte[] keyBytes) { try { SecretKey secretKey = getSecretKey(keyBytes); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); return cipher.doFinal(cipherBytes); } catch (Exception e) { throw new RuntimeException(e); } } /** * 获取加密 key * @param keySeed seed * @return 结果 * @since 0.0.8 */ private static SecretKey getSecretKey(byte[] keySeed) { try { // 避免 linux 系统出现随机的问题 SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); secureRandom.setSeed(keySeed); KeyGenerator generator = KeyGenerator.getInstance("AES"); generator.init(secureRandom); return generator.generateKey(); } catch (Exception e) { throw new RuntimeException(e); } } }
结合RSA与AES的加密如下:
先用公钥加密RSA发送对称加密使用的会话密钥,然后再用会话密钥进行AES对称加密通信。
// 监听客户端发送消息 socketIOServer.addEventListener(Constants.EVENT_MESSAGE_TO_SERVER, String.class, (client, data, ackSender) -> { String sender_name = getParamsByClient(client, "u_name"); ObjectMapper mapper = new ObjectMapper(); Message message = mapper.readValue(data, Message.class); String receiver_name = message.getReceiver_name(); if (message.getType().equals(Constants.MASTER_MESSAGE)) { //使用公钥加密传送会话密钥 if (AesKey.equals("")) { log.info("用户" + sender_name + "生成会话密钥"); AesKey = AESUtils.genAesSecret(); message.setContent(AesKey); log.info("用户" + sender_name + "使用用户" + sender_name + "的私钥对会话密钥进行签名"); String sign = new String(RSAUtils.sign(message.getContent(), RSAUtils.getPrivateKey()), "ISO-8859-1"); message.setSign(sign); String result = RSAUtils.encrypt(message.getContent(), publicKeyStringMap.get(receiver_name)); log.info("使用用户" + receiver_name + "的公钥对会话密钥进行加密:" + result); message.setContent(result); sendMessageToFriend(message.getReceiver_name(), message); } else { return; } } else { //使用会话密钥发送消息 byte[] bytes = AESUtils.encrypt(message.getContent().getBytes(), AesKey.getBytes()); String encrypt = new String(bytes, "ISO-8859-1"); log.info("用户" + sender_name + "使用会话密钥加密消息"); message.setContent(encrypt); sendMessageToFriend(message.getReceiver_name(), message); } }); // //GBK, GB2312,UTF-8等一些编码方式为多字节或者可变长编码,原来的字节数组就被改变了,再转回原来的byte[]数组就会发生错误了。 //ISO-8859-1通常叫做Latin-1,Latin-1包括了书写所有西方欧洲语言不可缺少的附加字符,其中 0~127的字符与ASCII码相同, // 它是单字节的编码方式,在来回切换时不会出现错误。 // 监听客户端接收消息 socketIOServer.addEventListener("receive_triger", String.class, (client, data, ackSender) -> { ObjectMapper mapper = new ObjectMapper(); Message message = mapper.readValue(data, Message.class); String sender_name = message.getSender_name(); String receiver_name = message.getReceiver_name(); if (message.getType().equals(Constants.MASTER_MESSAGE)) { log.info("收到来自" + sender_name + "发送给" + message.getReceiver_name() + "的消息: " + message.getContent()); String result = RSAUtils.decrypt(message.getContent(), RSAUtils.getKeyPair().get("PR")); log.info("用户" + receiver_name + "使用用户" + receiver_name + "的私钥对消息进行解密:"); message.setContent(result); log.info("用户" + receiver_name + "使用用户" + sender_name + "的公钥对消息进行验证签名"); Boolean sign = (RSAUtils.verify(message.getContent(), message.getSign().getBytes("ISO-8859-1"), publicKeyMap.get(sender_name))); if (sign) { log.info("签名验证成功!身份无误"); } else { throw new Exception("签名错误!"); } receiveMessageFromFriend(message.getReceiver_name(), message); } else { log.info("收到来自" + sender_name + "发送给" + message.getReceiver_name() + "的消息: " + message.getContent()); String text = new String(AESUtils.decrypt(message.getContent().getBytes("ISO-8859-1"), AesKey.getBytes()), "UTF-8"); log.info("用户" + receiver_name + "使用会话密钥进行解密"); message.setContent(text); receiveMessageFromFriend(message.getReceiver_name(), message); } });
可以看到好友列表
同时获取本地密钥库中的公私钥并将其加入公钥库
选择好友进行私聊,进入聊天界面。
在输入框中输入消息,点击发送,接收者和发送者的聊天框都会出现相应的消息。此消息是经过后端AES对称加密解密得到的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。