乐趣区

关于java:支付平台架构技术实现之终端安全

转载自:领取平台架构技术实现之终端平安

首先解说在本地加密存储的办法,加密办法有两种:对称加解密和非对称加解密。

实用场景:领取零碎中有局部配置文件的内容须要加密存储在本地,例如:跳转服务器的地址或领取 SDK 的运行参数在应用过程中须要先解密再应用。对于这种场景,比拟适宜采纳对称加密算法。

当然,也能够应用非对称加密。但因为非对称加密实用于安全级别较高、运算速度较慢及私钥个别不在终端存储等场景中,所以在技术选型下面不宜应用。

说到对称加密算法,能够抉择应用以下几种计划。这里的终端平安示例代码以 Android 操作系统为例,并且应用 Java 来实现平安加密、拜访受权和传输平安。

1、中低安全级别的数据(DES)

数据加密规范 DES

数据加密规范 DES(Data Encryption Standard)是应用对称密钥加密的一种块加密算法,解决数据的速度较快,性能较好,通常实用于对大块数据加解密的场景中。该算法的显著毛病是密钥较短,这意味着能够通过暴力破解来解密,升高了加密的安全性,但依然实用于对领取零碎配置文件的平安加密等场景中。

以下是基于 Android 零碎的 DES 加密的代码实现:

/**
 * 采纳 DES 加密字符串数据,应用 UTF-8 编码
 * @param plain 原字符串
 * @param encryKey 密钥
 * @return 密文
 * @throws Exception
 */
 public static String encryptByDES(String plain, String encryKey)
 throws Exception {
 // 获取明码实例对象,参数格局为 "算法 / 模式 / 填充"
 Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
 // 应用 Key 作为 DES 密钥的密钥内容,创立一个 DESKeySpec 对象
 DESKeySpec desKeySpec = new DESKeySpec(encryKey.getBytes("UTF-8"));
 // 返回 DES 算法的 SecretKeyFactory 对象
 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
 // 生成 SecretKey 对象
 SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
 // 应用密钥结构一个 IvParameterSpec 对象。IvParameterSpec iv = new IvParameterSpec(encryKey.getBytes());
 // 用密钥和一组算法参数初始化明码实例对象
 cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
 // 加密,应用 Base64 明码
 return new String(Base64.encode(cipher.doFinal(plain
 .getBytes("UTF-8"))));
 }

对应的解密函数如下:

 /**
 * 应用明码和密钥解密数据
 * @param encryString 密文
 * @param decodeKey 密钥
 * @return 明文
 * @throws Exception
 */
 public static String decryptByDES(String encryString, String decodeKey) throws Exception {
 // 应用密钥结构 IV 对象
 IvParameterSpec iv = new IvParameterSpec(decodeKey.getBytes());
 // 依据密钥和 DES 算法结构一个 SecretKeySpec
 SecretKeySpec skeySpec = new SecretKeySpec(decodeKey.getBytes(), "DES");
 // 返回实现了指定转换的 Cipher 对象
 Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
 // 解密初始化
 cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
 // 解码返回
 byte[] byteMi = Base64.decode(decodeString.toCharArray());
 byte decryptedData[] = cipher.doFinal(byteMi);
 return new String(decryptedData);
 }

这样就实现了加解密函数,只需在加密时调用 encryptByDES 函数,将明文数据和 8 位 Key 传入就能够失去密文数据,而后在应用时以雷同的 Key 值和密文调用 decryptByDES 函数实现密文解密失去明文信息。在以上代码中还应用了 Base64 编码方式,能够将二进制数据编码成可见的 ASCII 码字符串数据。在 Android 零碎中 Base64(残缺类名为 android.util.Base64)曾经是一种内置的工具类的编码转换算法,很多人都把 Base64 当成一个加解密算法,但从严格意义上来说,它不能算是一种加解密算法,只能算是一种编码格局的转换算法。

2、DES 算法演进之 3DES

在 DES 根底之上进化了三重数据加密算法(3DES),该算法应用了 K1、K2、K3 对同一组明文进行多重加密,其基本原理是对每个数据块都应用三次 DES 加密,如果密钥小于 64 位,则其加密强度与 DES 统一,个别倡议采纳的密钥超过 64 位。3DES 的加密函数示例如下:

/**
 * 采纳 3DES 加密字符串
 *
 * @param plain
 * 一般文本
 * @return
 * @throws Exception
 */
 public static String encryptBy3DES(String plain, String secretKey) throws Exception {
 Key deskey = null ;
 DESedeKeySpec spec = new DESedeKeySpec(secretKey.getBytes());
 // 依据 3DES 结构一个 SecretKeyFactory
 SecretKeyFactory keyfactory = SecretKeyFactory.getInstance("desede");
 deskey = keyfactory.generateSecret(spec);
 // 获取明码实例对象,参数格局为 "算法 / 模式 / 填充"
 Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
 IvParameterSpec ips = new IvParameterSpec(iv.getBytes());
 cipher.init(Cipher. ENCRYPT_MODE , deskey, ips);
 byte [] encryptData = cipher.doFinal(plain.getBytes("UTF-8"));
 return Base64.encodeToString(encryptData,Base64.DEFAULT);
 }

其中波及的加密编码方式和填充形式包含 3DES-ECB、3DES-CBC、3DES-CTR、3DES-OFB 和 3DES-CFB。解密函数示例如下:

/**
 * 3DES 解密
 * @param encryString 密文
 * @return 明文
 * @throws Exception
 */
 public static String decryptBy3DES(String encryString, String secretKey) throws Exception {
 Key deskey = null ;
 DESedeKeySpec spec = new DESedeKeySpec(secretKey.getBytes());
 SecretKeyFactory keyfactory = SecretKeyFactory.getInstance("desede");

其中三重数据加密算法的密钥长度是 128 位。除了 3DES 算法,还有人演算出 N-DES(N 重数据加密算法)。

3、高安全级别的数据(AES)

因为密钥长度过短、弱密钥等毛病的存在,DES 容易被暴力破解。随着计算机性能一直晋升,DES 被暴力破解的频率越来越高。所以,美国国家标准与技术研究院(NIST)在 1997 年放弃了对 DES 的官网反对,研发出 DES 的替代者 AES(Advanced Encryption Standard,高级加密规范)。

在 Android 零碎上应用 AES 与应用 DES 的实现难度、代码量和写法相差无几,比 DES 速度更快、性能更高,在理论的开发过程中倡议采纳 AES 算法对数据进行加解密,其加密代码如下:

/**
* AES 加密
* @param plain 明文
* @return 密文
* @throws Exception
*/
public static String encryptByAES(String plain, String secretKey){byte[] crypted = null;
try{SecretKeySpec spec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, spec);
crypted = cipher.doFinal(plain.getBytes());
}catch(Exception e){return "";}
return new String(Base64.encode(crypted, Base64.NO_WRAP));
}

解密代码如下:

/**
 * AES 解密
 * @param encryString 密文
 * @return 明文
 * @throws Exception
 */
 public static String decryptByAES(String encryString, String secretKey){byte[] output = null;
 try{SecretKeySpec spec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");
 Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
 cipher.init(Cipher.DECRYPT_MODE, spec);
 output = cipher.doFinal(Base64.decode(encryString, Base64.NO_WRAP));
 } catch(Exception e){return "";}
 return new String(output);
 }

针对对称加解密算法都有一个密钥须要存储的问题,目前有三种实现计划。生成密钥之后,能够将其保留在存储设备中,例如密钥文件或 Android 零碎的 SharedPreferences 中,在应用时将其读取到内存中。生成密钥之后,根据固定的设施个性(例如 DeviceId、OSID 等)将密钥信息上送到服务器端,在利用启动时将密钥信息获取到本地应用,因为挪动网络通信存在不确定性,所以不举荐采纳这种计划。将密钥放在 NDK 代码中,而后采纳数据位移或拆分等计划,再拼装为真正的密钥数据。这种算法的破解难度较高,也较平安,举荐采纳这种存储计划。

4、非对称加密(RSA)

RSA 是一种非对称加密算法,由三位数学家 Rivest、Shamir 和 Adleman 设计,其核心思想为将密钥分成以下两把密钥,简称密钥对。

在密钥对中有一个公钥,还有一个私钥。

  • 公钥(Public Key):是密钥对中齐全公开的局部,任何人都能够失去它,实用于客户端 – 服务端模型。
  • 私钥(Private Key):是密钥对中窃密的一部分,个别在服务端平安存储,不容许在客户端存储。

能够应用 OpenSSL 工具的命令生成公私钥,也能够应用开发语言生成公私钥。

(1)生成 RSA 算法的私钥时,应用以下命令:

openssl genrsa -out rsa_private_key.pem 2048 

(2)应用以下命令将 X509 编码文件转换成 PKCS8 编码格局:

openssl pkcs8 -in rsa_private_key.pem -out rsa_private_key_pkcs8.pem -nocrypt -topk8

(3)导出私钥对应的 X509 编码公钥文件:

openssl rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout

留神:能够应用 Java 代码从 rsa_private_key_pkcs8.pem 文件中读取私钥信息并生成数字签名,再应用 rsa_public_key.pem 公钥文件验证数字签名的正确性。

Java 虚拟机也提供了内置的办法来生成公私钥,代码如下:

/**
 * 生成非对称密钥对
 * @throws NoSuchAlgorithmException
 */
 public static void genKeyPair() throws NoSuchAlgorithmException {
 //KeyPairGenerator 类,基于 RSA 算法生成对象
 KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
 // 初始化密钥对生成器
 keyPairGen.initialize(1024,new SecureRandom());
 // 生成一个密钥对,保留在 keyPair 对象中
 KeyPair keyPair = keyPairGen.generateKeyPair();
 // 失去私钥对象
 RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
 // 失去公钥对象
 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
 // 公钥字符串
 String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));
 // 私钥字符串
 String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));
 } 

有了公私钥数据之后,就能够对数据进行加解密解决和数据加签、验签了。

其中,加密数据的一方应用公开取得的公钥(个别举荐应用 1024 位密钥,密钥越长越平安,也意味着加密性能越差),对明文数据进行加密失去密文:

/**
 * 应用公钥进行加密
 * @param plain 明文数据
 * @param publicKey 公钥数据
 * @return 密文
 * @throws Exception
 */
 public static byte[] encryptByPubKey(byte[] plain, byte[] publicKey) throws Exception {
 // 从公钥数据中失去 KeySpec 对象
 X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
 // 依据 RSA 算法结构一个 KeyFactory
 KeyFactory keyFactory = KeyFactory.getInstance("RSA");
 PublicKey pubKey = keyFactory.generatePublic(keySpec);
 // 获取明码实例对象 参数格局为 "算法 / 模式 / 填充"
 Cipher cp = Cipher.getInstance("RSA/None/PKCS1Padding");
 cp.init(Cipher.ENCRYPT_MODE, pubKey);
 return cp.doFinal(plain);
 } 

解密的一方具备私钥,拿到密文时,应用对应的私钥进行解密:

/**
 * 应用私钥解密
 * @param encrypted
 * @param privateKey
 * @return
 * @throws Exception
 */
 public static byte[] decryptByPrivKey(byte[] encrypted, byte[] privateKey) throws Exception {
 // 从私钥数据中失去 KeySpec 对象
 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
 KeyFactory kf = KeyFactory.getInstance("RSA");
 PrivateKey keyPrivate = kf.generatePrivate(keySpec);
 // 获取明码实例对象,参数格局为 "算法 / 模式 / 填充"
 Cipher cp = Cipher.getInstance("RSA/None/PKCS1Padding");
 cp.init(Cipher.DECRYPT_MODE, keyPrivate);
 byte[] arr = cp.doFinal(encrypted);
 return arr;
 }

如果解密失败,则代表公钥或私钥不匹配(不是一个密钥对),这也阐明如果没有对应的私钥,则解不出密文中的内容。RSA 个别只实用于小数据块的加解密场景中(例如加密动静密钥、短的要害数据),加解密速度较 AES 和 DES 慢。

5、传输平安

数据的传输平安须要满足以下条件。

  • 防窥探:数据明文受到爱护,不应该被黑客和歹意用户辨认、利用。爱护数据不被窥探是一项重要的指标,发送者和接收者单方都须要实现加密技术,保证数据无奈被第三方破解和解密。
  • 防篡改:爱护数据在传输过程中的完整性,必须确认不会在数据传输过程中被截获和篡改。
  • 防伪造:能辨认数据发送方是否具备合法性,并且能确认发送方的真实性。上面解说相应的技术实现计划。

1、防窥探

数据个别通过计算机网络进行传输,除了有从一个发送方(发送节点)发送到接管方(接管节点)的简略场景,还有简单的场景(通过 N 个网络节点传输能力达到最终目的地)。随着节点的增多,在这个传输过程中被截获、监听的危险越来越高(例如:当初罕用的网络数据抓包软件就有 Fiddler、Wireshark 等,能够监听到网络层都采纳了什么协定、调用了哪些 API,以及发送参数、返回的响应数据别离是什么)。

在客户端个别采纳公开的通道加密计划保障通道数据无奈被窥探。

TLS(Transport Layer Security)又叫作平安传输层协定,次要用于在两个通信应用程序之间提供保密性和数据完整性。

Android 零碎对应的实现如下。

首先,读取本人的证书并初始化 TLS 的工厂类:

// 用 keytool 将.keystore 中的证书写入文件中,而后从该文件中读取证书信息
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = new BufferedInputStream(new ByteArrayInputStream(caPath.getBytes()));
Certificate ca;
try {ca = cf.generateCertificate(caInput);
} finally {caInput.close();
}
// 创立一个蕴含认证证书的 KeyStore
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// 创立一个基于 KeyStore 算法的 TrustManager 对象
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// 初始化 TLS
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null); 

而后,获取 Socket 工厂类,创立 Socket 连贯,开始 TLS 握手:

// 近程服务器的地址
SocketAddress sockaddr = new InetSocketAddress("localhost", 80);
// 创立 Socket 对象实例
Socket socket = context.getSocketFactory().createSocket();
// 开始连贯
socket.connect(sockaddr, 60 * 1000);
// 开始 TLS 握手
socket.startHandshake(); 

2、防篡改

在某领取场景中,客户端与服务端的申请被黑客截获并将领取订单的金额批改为 0.1 元(原订单金额 10 元),因为未在这个过程中对订单数据进行防篡改校验,导致了商户的商品被便宜卖掉,造成了商户的经济损失。

数据防篡改的次要伎俩是针对数据进行客户端加签,在服务端接收数据时验证加签数据是否与签名统一。加签的过程本质上是发送端针对待发送的原始数据进行肯定的解决(例如字符串去空格、字段排序、数据加密)后针对数据加签生成签名摘要数据,这部分摘要数据个别不会参加加密。接收端在收到数据之后,先将签名摘要数据和加密数据取出来,而后解密已加密的数据块失去原始数据,最初像发送端一样进行解决,生成签名摘要数据。如果生成的摘要数据与发送端传送过去的统一,则示意数据没被篡改过,否则示意数据在传输过程中被篡改。

上面解说对防篡改的数据进行签名和验签的过程。

(1)对原始数据去空格,进行参数字段排序(升序或降序)和拼接。

(2)将原始数据(待签名内容)依据参数字段名称进行排序,能够保障加签、验签的单方待验证参数内容的一致性。例如:排序升序规定依照第一个字符的 ASCII 码值递增排序,如果遇到雷同的字符,则根据第 2 个字符排递增序,以此类推。将排序后的参数拼接成“参数 = 参数值”的格局,并且把这些参数应用“&”字符连接起来。

/**
 * 对参数字段进行排序
 * @param params 参数数据
 * @return
 */
 public String orderByParameters(Map<String, String> params) {StringBuilder sb = new StringBuilder();
 // 以参数名称的字典进行升序排列
 Map<String, String> sortParams = new TreeMap<String, String>(params);
 // 拼接成 "key=value" 格局
 for (Map.Entry<String, String> entry : sortParams.entrySet()) {String key = entry.getKey();
 // 字符串去空格
 String value = entry.getValue().trim();
 key = key.trim();
 if (TextUtils.isEmpty(value))
 sb.append("&").append(key).append("=").append(value);
 }
 return sb.toString();} 

(3)生成摘要数据,罕用的摘要算法有 MD5、SHA-1 等。上面应用 MD5 生成摘要数据:

// 替换第 1 个距离符号
String param = param.replaceFirst("&","");
// 生成摘要数据
String signValue = Md5Utils.md5(param);
// 拼接摘要数据
String param = param + "&sign=" + signValue;

(4)应用非对称加密算法 RSA,利用客户端的公钥对摘要值进行加密,将数据通过网络发送给接管方进行验证。

接管方在接管到数据之后进行验签,与加签的过程基本一致。

  • 参数排序。将收到的参数内容(key=value 字典)依据参数名称进行排序,其排序规定与签名方保持一致,对参数字符串去空格和拼接,其拼接形式与签名方保持一致,生成待生成摘要的原参数字符串。
  • 生成摘要数据。应用雷同的摘要算法(MD5)计算失去验签方的摘要值。
  • 进行非对称解密。应用 RSA 非对称加密算法,对收到的加密摘要数据应用私钥进行解密,并失去签名方的原始摘要值。
  • 摘要数据比照。如果签名方的摘要数值等于验签方计算出来的摘要值,则示意验签胜利,否则验签失败。
退出移动版