背景

这几天,咱们家娃吃奶粉的量嗷嗷的涨起来了。我这颗小鹿乱撞的心,不由得去面了两家互联网金融公司。
由于没有准备,成果你懂的~
这两家一起都有一个一起点,特别联系安全问题,尤其是看到你用过 okhttp3,那不得给你撸脱毛了就不算完事儿。

协议

HTTP vs HTTPS

咱们都知道,https 相比于之前的 http 多了一层, 如下:

https 协议是安全传输,为啥还要再加密?

HTTP是一个基于TCP/IP通讯协议来传递数据的协议,TCP/IP通讯协议只要四层,从上往下依次为:运用层、传输层、网络层、数据链路层这四层,大学课本上的计算机网络知识是不是来了。可是,HTTP 协议在在网上进行传输用的是明文,就像某鸟给你发的快递,你的手机号、姓名都是写的清清楚楚,用户看着都瑟瑟发抖。
后来京东和顺丰认识到了这一点,就对手机号中间四位做了加密处理,姓名中间那几个个字也看不到了,乃至快递小哥给你打电话都是虚拟号码,你自己的电话只要自己心里清楚。
HTTPS 也是这个路子,为了处理 HTTP 明文传输存在的安全问题,在运用层和传输层之间加了一层安全层:SSL/TLS。
SSL: Secure Socket Layer, 安全套接层
TLS: Transport Layer Security,传输层安全协议
关于 HTTP 和 HTTPS 的对比文章,知乎上有一大坨,这儿贴几篇,喜欢的可以多撸几遍:

  1. www.zhihu.com/question/19…
  2. zhuanlan.zhihu.com/p/158593966
  3. 握手与挥手:zhuanlan.zhihu.com/p/78405711

HTTPS优缺点

  1. 运用 HTTP 明文传输存在被偷听危险,HTTPS 经过内容加密处理,注意这一步用的是对称加密哦
  2. 运用 HTTP 明文传输存在通讯内容被篡改危险,HTTPS 经过校验机制处理
  3. 运用 HTTP 明文传输存在假充服务危险,HTTPS 经过身份证书认证机制处理

经过以上三点,咱们知道HTTPS 相关于 HTTP 有很强的安全感,可是关于一些app 这种安全级别还不够,假如出现以下意外咋整?

我爽性去问了讯飞星火,他是这么答复我的:

https 协议是安全传输,为啥还要再加密?

我又去问了 ChatGPT,他的答复是这样的

https 协议是安全传输,为啥还要再加密?

这俩货说的差不多,HTTPS 只能确保传输进程的安全性,不能确保全链路的安全性,更不能满足特定安全标准的标准诉求,所以咱们需求根据安全的级别要求,采纳不同加密战略。

既然如此,咱们运用的三方网络库这儿特指 okhttp 是怎么全链路传输安全的?咱们接着往下唠:

OKhttp 库

Okhttp 库是基于HTTP和HTTPS协议封装的网络恳求库,用于 Android 和 Java 运用中的网络通讯。库在这儿:github.com/square/okht…

咱们可以先了解一下okhttp 库是怎么确保传输安全的~

问:OKhttp是怎么支撑https协议,以确保传输安全的,尤其是怎么进行避免侦听、篡改、阻拦信息?

AI 答复:
OkHttp是一个盛行的开源HTTP客户端库,由Square公司开发。它供给了对HTTP和HTTPS的支撑,并经过内置的TLS(Transport Layer Security)来确保HTTPS衔接的安全性。以下是OkHttp怎么支撑HTTPS协议以及确保传输安全的一些要害方面:

  1. TLS/SSL支撑:

    • OkHttp运用Java的javax.net.ssl包来支撑TLS/SSL协议。TLS是HTTP的安全版别,用于加密通讯内容,避免中间人进犯、偷听和篡改。
    • OkHttp经过SSLSocketFactoryHostnameVerifier等类来处理TLS/SSL握手和验证服务器证书的进程。
  2. 证书验证:

    • OkHttp会验证服务器供给的SSL证书,确保通讯两边的身份合法。假如服务器的证书无效或无法验证,OkHttp会拒绝衔接,然后避免中间人进犯。
  3. 证书确定(Certificate Pinning):

    • OkHttp支撑证书确定机制,即在运用中预先指定服务端的证书信息。这样可以避免运用伪造证书的进犯,即使系统遭到信赖的根证书被破坏,运用依然可以保持安全。
  4. 衔接池和耐久衔接:

    • OkHttp经过衔接池(Connection Pool)来重用已树立的衔接,然后减少握手次数,提高功用。耐久衔接也有助于减少树立衔接时的潜在危险。
  5. 安全装备:

    • OkHttp答应开发者装备TLS版别、暗码套件(Cipher Suites)等参数,以满足特定的安全要求。
  6. 阻拦器(Interceptors):

    • OkHttp供给了阻拦器机制,可以在HTTP恳求和呼应的进程中刺进自定义的逻辑。这使得开发者可以在恳求或呼应阶段增加额外的安全查看或处理逻辑,然后增强安全性。

总的来说,OkHttp经过支撑TLS/SSL、证书验证、证书确定、衔接池等安全机制,以及供给灵敏的装备和阻拦器功用,确保在HTTPS衔接中避免侦听、篡改和阻拦信息的危险。开发者在运用OkHttp时,应该合理装备相关参数,以满足运用的安全需求。

好家伙,okhttp 是支撑合理装备安全参数的,而且其阻拦器机制可以很方便的进行安全查看,那还等什么,走一波~

战略一:对称加密(加密内容)

这儿主打一个,客户端不想让别人看到自己的传输内容,想当然对自己的恳求内容进行加密处理。基于这种思路咱们封装一个 EncryptedOkHttpClient,代码如下:

public static OkHttpClient createEncryptedOkHttpClient() {
    // 创立一个OkHttpClient.Builder
    OkHttpClient.Builder builder = new OkHttpClient.Builder();
    // 增加自定义阻拦器,用于加密恳求内容
    builder.addInterceptor(new EncryptionInterceptor());
    // 创立OkHttpClient实例
    return builder.build();
}

注释里现已写了,经过EncryptionInterceptor阻拦器对恳求进行加密处理,这儿挑选加密恳求体 RequestBody

https 协议是安全传输,为啥还要再加密?
在encryptRequestBody办法中,RequestBody 依靠 okio 的 Buffer 类转换为ByteArray用于加密,加密算法挑选对称加密算法 AES 加密字节数据,完成如下:

private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
    // 从原始RequestBody中读取字节数据
    // Read the byte data from the original RequestBody using Okio
    Buffer buffer = new Buffer();
    originalRequestBody.writeTo(buffer);
    byte[] bytes = buffer.readByteArray();
    // 运用对称加密算法(AES)加密字节数据
    byte[] encryptedBytes = encryptWithAES(bytes, SECRET_KEY);
    // 创立新的RequestBody
    return RequestBody.create(originalRequestBody.contentType(), encryptedBytes);
}

可以看到,AES 运用了encryptWithAES办法加密字节数据,一起传了SECRET_KEY这个密钥,那咱们看看 AES 是怎么加密的:

private byte[] encryptWithAES(byte[] input, String key) {
    try {
        SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        return cipher.doFinal(input);
    } catch (Exception e) {
        e.printStackTrace();
        return new byte[0];
    }
}

四行代码搞定,首要经过SecretKeySpec类将SECRET_KEY字符串加密成 SecretKey 方针,然后Cipher以加密方式 对密钥进行初始化然后加密 input 也便是转换为字节数组的恳求体。 加密完成了,服务器当然要进行解密,解密办法如下:

public static String decrypt(String encryptedText) {
    try {
        byte[] encryptedData = Base64.decode(encryptedText,Base64.DEFAULT);
        SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decryptedBytes = cipher.doFinal(encryptedData);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

可以看到,解密进程运用了相同AES算法和密钥SECRET_KEY,这便是对称加密运用一把钥匙上锁和开锁。可是这种加密算法有很大的问题:

首要,这把钥匙假如想经过网络传输让服务端知道,传输进程中被劫持了密钥就会暴露。

另外,SECRET_KEY是硬编码在代码中的,这也不安全,这可咋整啊?

千里之堤,溃于hacker

为了避免这种中间人进犯的问题,非对称加密开始表演了~

战略二:非对称加密

非对称加密是一把锁两把钥匙:公钥和私钥。前者是给大家伙用的,谁都可以来配一把公钥进行数据加密,可是要对加密数据进行解密,只能运用私钥。

假设咱们用公钥加密一份数据,就不怕阻拦了。由于只要拿着私钥的服务端才能解密数据,咱们拿着服务器供给的公钥把战略一中的对称密钥给加密了,那不就处理了网络传输密钥的问题了。对的,HTTPS 也是这么做的,按照这个思路咱们再增加一个 MixtureEncryptionInterceptor 阻拦器。

// 增加自定义阻拦器,用服务器非对称加密的公钥加密对称加密的密钥,然后用对称加密密钥加密恳求内容
builder.addInterceptor(new MixtureEncryptionInterceptor());

MixtureEncryptionInterceptor 阻拦器相同完成 Interceptor 接口如下:

https 协议是安全传输,为啥还要再加密?

其 intercept 办法跟 EncryptionInterceptor 一模一样,详细的变化在 encryptRequestBody() 办法中。详细完成如下:

private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
    // 生成对称加密的密钥
    byte[] secretKeyBytes = generateSecretKey();
    SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "AES");
    // 运用服务器的公钥加密对称加密的密钥
    byte[] encryptedSecretKey = encryptWithPublicKey(secretKeyBytes, SERVER_PUBLIC_KEY);
    // 从原始 RequestBody 中读取字节数据
    Buffer buffer = new Buffer();
    originalRequestBody.writeTo(buffer);
    byte[] bytes = buffer.readByteArray();
    // 运用对称加密算法(AES)加密恳求体
    byte[] encryptedRequestBodyBytes = encryptWithAES(bytes, secretKeySpec);
    // 创立新的 RequestBody,将加密后的密钥和恳求体一起传输
    return RequestBody.create(null, concatenateArrays(encryptedSecretKey, encryptedRequestBodyBytes));
}

如代码中注释,整个混合加密共 4 个步骤,依次是:

  1. 生成对称加密的密钥,用来加密传输内容。代码如下:
/**
 * try block 里运用的是加密算法和随机数生成器,生成的较为复杂的密钥
 * catch block 里运用的是演示性的非安全密钥
 * @return
 */
private byte[] generateSecretKey() {
    // 生成对称加密的密钥
    try {
        // 创立KeyGenerator方针,指定运用AES算法
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        // 初始化KeyGenerator方针,设置密钥长度为128位
        keyGenerator.init(128, new SecureRandom());
        // 生成密钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 获取密钥的字节数组表明方式
        byte[] keyBytes = secretKey.getEncoded();
        // 打印密钥的字节数组表明方式
        for (byte b : keyBytes) {
            Log.d(TAG,b + " ");
        }
        return keyBytes;
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
        // 这儿简单地演示了生成密钥的进程,实际上可以运用更复杂的办法来生成密钥
        return "YourSecretKey".getBytes(StandardCharsets.UTF_8);
    }
}

如注释所言,上面try block 里运用的是加密算法和随机数生成器,生成的较为复杂的密钥,catch block 里运用的是演示性的非安全密钥。这儿主要是想阐明生成对称密钥的方式有很多,可是硬编码生成密钥那是不推荐的,由于太不安全了,很容易被恶意用户获取到。

  1. 运用服务器的公钥加密对称加密的密钥,避免被破解
private byte[] encryptWithPublicKey(byte[] input, String publicKeyString) {
    try {
        // 封装 PublicKey
        byte[] keyBytes = Base64.decode(publicKeyString, Base64.DEFAULT);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(input);
    } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException |
             InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
        e.printStackTrace();
        return new byte[0];
    }
}

将服务端供给的公钥字符串转化成字节数组,然后经过 RSA 非对称算法加密 input,也便是咱们的对称密钥。

注意:Cipher.getInstance(“RSA/ECB/PKCS1Padding”) 表明获取一个Cipher方针,该方针运用RSA算法、ECB方式和PKCS1填充方式。

  1. 运用对称加密算法(AES)加密恳求体,恳求体依然要用对称加密密钥加密,仅仅对称加密密钥用公钥保护起来
private byte[] encryptWithAES(byte[] input, SecretKeySpec secretKeySpec) {
    try {
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        return cipher.doFinal(input);
    } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
             BadPaddingException | IllegalBlockSizeException e) {
        e.printStackTrace();
        return new byte[0];
    }
}

非对称加密加密内容,战略一现已完成了。

  1. 创立新的 RequestBody,将加密后的密钥和恳求体一起传输,这样就算 hacker 阻拦了恳求解分出恳求体的数据,也无法直接获取到原始对称密钥。 加密完成后,经过 concatenateArrays 办法将加密后的密钥和恳求体,完成如下:
private byte[] concatenateArrays(byte[] a, byte[] b) {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    try {
        outputStream.write(a);
        outputStream.write(b);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return outputStream.toByteArray();
}

非对称加密处理了密钥网络传输的问题,可是 hacker 仍是可以伪装成方针服务端,骗取客户端的密钥。在伪装成客户端,用服务端的公钥加密自己篡改的内容,方针服务端对此无法区分真伪。这就需求证书校验。

战略三:证书校验(单向认证)

okhttp3 供给了CertificatePinner这个类用于证书校验,CertificatePinner 可以验证服务器回来的证书是否是预期的证书。在创立createEncryptedOkHttpClient()办法中,增加证书代码如下:

https 协议是安全传输,为啥还要再加密?

okhttp 会利用其内置的证书固定机制来校验服务器回来证书的有效性。假如证书匹配,恳求会继续进行;假如不匹配,OkHttp会抛出一个异常,通常是一个SSLPeerUnverifiedException,表明证书验证失败。验证进程在CertificatePinner 类的check()办法中,如下:

/**
 * Confirms that at least one of the certificates pinned for {@code hostname} is in {@code
 * peerCertificates}. Does nothing if there are no certificates pinned for {@code hostname}.
 * OkHttp calls this after a successful TLS handshake, but before the connection is used.
 *
 * @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match the certificates
 * pinned for {@code hostname}.
 */
public void check(String hostname, List<Certificate> peerCertificates)
    throws SSLPeerUnverifiedException {
  List<Pin> pins = findMatchingPins(hostname);
  if (pins.isEmpty()) return;
  if (certificateChainCleaner != null) {
    peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
  }
  for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
    X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
    // Lazily compute the hashes for each certificate.
    ByteString sha1 = null;
    ByteString sha256 = null;
    for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
      Pin pin = pins.get(p);
      if (pin.hashAlgorithm.equals("sha256/")) {
        if (sha256 == null) sha256 = sha256(x509Certificate);
        if (pin.hash.equals(sha256)) return; // Success!
      } else if (pin.hashAlgorithm.equals("sha1/")) {
        if (sha1 == null) sha1 = sha1(x509Certificate);
        if (pin.hash.equals(sha1)) return; // Success!
      } else {
        throw new AssertionError();
      }
    }
  }
  // If we couldn't find a matching pin, format a nice exception.
  StringBuilder message = new StringBuilder()
      .append("Certificate pinning failure!")
      .append("n  Peer certificate chain:");
  for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
    X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
    message.append("n    ").append(pin(x509Certificate))
        .append(": ").append(x509Certificate.getSubjectDN().getName());
  }
  message.append("n  Pinned certificates for ").append(hostname).append(":");
  for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
    Pin pin = pins.get(p);
    message.append("n    ").append(pin);
  }
  throw new SSLPeerUnverifiedException(message.toString());
}

从校验办法中得知,

  1. 可以没有固定证书
  2. 证书加密运用sha256/sha1
  3. 证书校验失败会抛出AssertionError过错
  4. 获取不到匹配的固定证书,会抛异常SSLPeerUnverifiedException

可以看到,运用相当方便。可是它有一个问题:恳求之前需求预先知道服务端证书的 hash 值。便是说假如证书到期需求更换,老版别的运用就无法获取到更新的证书 hash 值了,老用户要统一晋级。这~~~

战略四:创立SSLContext认证(客户端、服务端双向认证)

除了固定证书校验,还有一种基于 SSLContext 的校验方式。在树立HTTPS衔接时,在客户端它依靠 SSLContext 和 TrustManager 来验证服务端证书。这儿咱们经过一createTwoWayAuthClient()办法完成如下:

private static OkHttpClient createTwoWayAuthClient() throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
    // 服务器证书
    InputStream serverCertStream = TwoWayAuthHttpClient.class.getResourceAsStream("/server_certificate.crt");
    X509Certificate serverCertificate = readCertificate(serverCertStream);
    if (serverCertStream != null) {
        serverCertStream.close();
    }
    // 客户端证书和私钥
    InputStream clientCertStream = TwoWayAuthHttpClient.class.getResourceAsStream("/client_centificate.p12");
    KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
    clientKeyStore.load(clientCertStream, "client_password".toCharArray());
    if (clientCertStream != null) {
        clientCertStream.close();
    }
    // 创立 KeyManagerFactory 和 TrustManagerFactory
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(clientKeyStore, "client_password".toCharArray());
    // 创立信赖办理器,信赖服务器证书
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustStore.load(null, null);
    trustStore.setCertificateEntry("server", serverCertificate);
    trustManagerFactory.init(trustStore);
    // 初始化 SSL 上下文
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
    // 创立 OkHttpClient
    return new OkHttpClient.Builder()
            .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0])
            .build();
}
private static X509Certificate readCertificate(InputStream inputStream) throws CertificateException {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    return (X509Certificate) certificateFactory.generateCertificate(inputStream);
}
  1. 加载服务器证书

    • 运用getResourceAsStream从类途径中加载服务器证书文件(.crt格局)。
    • 经过readCertificate办法读取证书内容,并生成一个X509Certificate方针。
    • 封闭输入流以开释资源。

注意:/server_certificate.crt可以动态加载服务器自签名证书的办法避免更新旧版别运用

  1. 加载客户端证书和私钥

    • 相同运用getResourceAsStream从类途径中加载客户端证书和私钥文件(.p12格局,通常是PKCS#12格局的密钥库)。
    • 创立一个KeyStore实例,并运用PKCS12算法加载客户端证书和私钥。暗码为"client_password"
    • 封闭输入流。
  2. 创立KeyManagerFactory和TrustManagerFactory

    • KeyManagerFactory用于办理客户端的私钥和证书,以便在树立SSL/TLS衔接时运用。
    • TrustManagerFactory用于办理信赖的证书,以便在树立SSL/TLS衔接时验证服务器的证书。
    • 运用默认算法初始化这两个工厂,并分别加载客户端的密钥库和信赖的服务器证书。
  3. 初始化SSLContext

    • 创立一个SSLContext实例,指定运用TLS协议。
    • 运用之前创立的KeyManagerFactoryTrustManagerFactory初始化SSLContext。这会将客户端的私钥和证书,以及信赖的服务器证书整合到SSL/TLS握手进程中。
  4. 创立OkHttpClient

    • 运用OkHttpClient.Builder创立一个新的OkHttpClient实例。
    • 装备SSL套接字工厂和信赖办理器,以确保在树立衔接时运用两向认证。
    • 构建并回来装备好的OkHttpClient实例。

这样客户端建议恳求时,会将客户端证书发送给服务端,一起会校验服务端握手时回来的证书。校验逻辑如下:

https 协议是安全传输,为啥还要再加密?

这样整个双向校验工作就完成了。

总结

okhttp 作为一个支撑 HTTPS 协议的网络库,一起支撑对称加密非对称加密客户端证书校验客户端、服务端双向证书校验等安全加密方式,足见其强大的功用。

此外,为了统筹功用:它运用证书校验确保通讯两边的合法性,运用对称加密加密传输内容确保功用,运用非对称加密加密对称密钥避免hacker 阻拦,整体提高了网络通讯的安全性。

AI 是个好东西~

参阅文章

  1. TCP/IP 协议: zh.wikipedia.org/wiki/TCP/IP…
  2. okhttp库: github.com/square/okht…
  3. X.509