Android 如何避免 Okhttp 的 HTTPS 请求被抓包

原创 ImWiki  2020-07-17 06:34  阅读 147 次
摘要:

Android 网络安全:如何避免 Okhttp 的 HTTPS 请求被抓包

HTTPS 重要性

如果我们APP的API请求只使用了 HTTP,这个实在太容易被抓包了,我们的请求信息很容易就暴露,可能会被用来做不利于APP的事情。为了APP的请求安全,我们有必要改用HTTPS,用来保障我们的请求安全。但是使用HTTPS并不就代表我们的请求就是安全的了,因为还是可以实现抓包,接下来介绍如何通过杜绝 OkHttp 的 HTTPS 被抓包。

原理

杜绝 HTTPS 抓包的原理很简单,其实就是拦截非法的证书,只通过我们信任的 HTTPS 证书的请求。

代码
public class OkHttpSSLUtils {
    public static OkHttpClient.Builder getOkHttpBuilder(Context context, String baseHostname, String... assetNames) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        try {
            InputStream[] inputStreams = new InputStream[assetNames.length];
            for (int i = 0; i < assetNames.length; i++) {
                inputStreams[i] = context.getAssets().open(assetNames[i]);
            }
            GeneralHostnameVerifier hostnameVerifier = GeneralHostnameVerifier.getInstance(baseHostname, inputStreams);
            if (hostnameVerifier != null) {
                builder.hostnameVerifier(hostnameVerifier);
            }
            for (InputStream item : inputStreams) {
                item.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return builder;
    }

    public static OkHttpClient.Builder getOkHttpBuilder(String baseHostname, File... files) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        try {
            InputStream[] inputStreams = new InputStream[files.length];
            for (int i = 0; i < files.length; i++) {
                inputStreams[i] = new FileInputStream(files[i]);
            }
            GeneralHostnameVerifier hostnameVerifier = GeneralHostnameVerifier.getInstance(baseHostname, inputStreams);
            if (hostnameVerifier != null) {
                builder.hostnameVerifier(hostnameVerifier);
            }
            for (InputStream item : inputStreams) {
                item.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return builder;
    }
}

GeneralHostnameVerifier.java

public class GeneralHostnameVerifier implements HostnameVerifier {

    private static final int ALT_DNS_NAME = 2;
    private static final int ALT_IPA_NAME = 7;

    public static GeneralHostnameVerifier getInstance(String baseHostname, InputStream... iss) {
        try {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            List<Collection<? extends Certificate>> list = new ArrayList<>();
            for (InputStream is : iss) {
                list.add(certificateFactory.generateCertificates(is));
            }
            if (list.isEmpty()) {
                throw new IllegalArgumentException("expected non-empty set of trusted certificates");
            }
            return new GeneralHostnameVerifier(baseHostname, list);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // Put the certificates a key store.
        return null;
    }

    /**
     * 公钥列表
     */
    private List<String> clientPublicKey = new ArrayList<>();

    private String baseHostname;

    public GeneralHostnameVerifier(String baseHostname, List<Collection<? extends Certificate>> list) {
        this.baseHostname = baseHostname;
        for (Collection<? extends Certificate> certificates : list) {
            for (Certificate certificate : certificates) {
                clientPublicKey.add(certificate.getPublicKey().toString());
            }
        }
    }

    @Override
    public boolean verify(String hostname, SSLSession sslSession) {
        // 如果不是我们自己的域名就走用默认的验证方式,不验证公钥
        if (!hostname.contains(baseHostname)) {
            return defaultVerify(hostname, sslSession);
        }
        try {
            // 从 SSLSession 获取请求的公钥信息
            List<String> pubKeys = new ArrayList<>();
            for (X509Certificate certificate : sslSession.getPeerCertificateChain()) {
                pubKeys.add(certificate.getPublicKey().toString());
            }
            // 验证请求的 SSL 证书是否合法(我们认可的证书),才继续验证,否则返回失败
            if (comparePubKey(pubKeys)) {
                return defaultVerify(hostname, sslSession);
            }
            return false;
        } catch (SSLPeerUnverifiedException e) {
            e.printStackTrace();
        }
        return defaultVerify(hostname, sslSession);
    }

    private boolean comparePubKey(List<String> servicePubKeys) throws SSLPeerUnverifiedException {
        if (servicePubKeys == null || servicePubKeys.size() <= 0) {
            return false;
        }
        if (clientPublicKey == null || clientPublicKey.size() <= 0) {
            throw new SSLPeerUnverifiedException("clientPublicKey null");
        }
        for (String key : servicePubKeys) {
            if (clientPublicKey.contains(key)) {
                return true;
            }
        }
        return false;

    }


    public boolean defaultVerify(String hostname, SSLSession session) {
        try {
            Certificate[] certificates = session.getPeerCertificates();
            return verify(hostname, (java.security.cert.X509Certificate) certificates[0]);
        } catch (SSLException e) {
            return false;
        }
    }

    public boolean verify(String hostname, java.security.cert.X509Certificate certificate) {
        return verifyAsIpAddress(hostname) ? verifyIpAddress(hostname, certificate) : verifyHostname(hostname, certificate);
    }


    /**
     * Returns true if {@code certificate} matches {@code ipAddress}.
     */
    private boolean verifyIpAddress(String ipAddress, java.security.cert.X509Certificate certificate) {
        List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
        for (int i = 0, size = altNames.size(); i < size; i++) {
            if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if {@code certificate} matches {@code hostname}.
     */
    private boolean verifyHostname(String hostname, java.security.cert.X509Certificate certificate) {
        hostname = hostname.toLowerCase(Locale.US);
        List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
        for (String altName : altNames) {
            if (verifyHostname(hostname, altName)) {
                return true;
            }
        }
        return false;
    }

    public static List<String> allSubjectAltNames(java.security.cert.X509Certificate certificate) {
        List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
        List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
        List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size());
        result.addAll(altIpaNames);
        result.addAll(altDnsNames);
        return result;
    }

    private static List<String> getSubjectAltNames(java.security.cert.X509Certificate certificate, int type) {
        List<String> result = new ArrayList<>();
        try {
            Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
            if (subjectAltNames == null) {
                return Collections.emptyList();
            }
            for (Object subjectAltName : subjectAltNames) {
                List<?> entry = (List<?>) subjectAltName;
                if (entry == null || entry.size() < 2) {
                    continue;
                }
                Integer altNameType = (Integer) entry.get(0);
                if (altNameType == null) {
                    continue;
                }
                if (altNameType == type) {
                    String altName = (String) entry.get(1);
                    if (altName != null) {
                        result.add(altName);
                    }
                }
            }
            return result;
        } catch (CertificateParsingException e) {
            return Collections.emptyList();
        }
    }

    /**
     * Returns {@code true} iff {@code hostname} matches the domain name {@code pattern}.
     *
     * @param hostname lower-case host name.
     * @param pattern  domain name pattern from certificate. May be a wildcard pattern such as {@code
     *                 *.android.com}.
     */
    public boolean verifyHostname(String hostname, String pattern) {
        // Basic sanity checks
        // Check length == 0 instead of .isEmpty() to support Java 5.
        if ((hostname == null) || (hostname.length() == 0) || (hostname.startsWith("."))
                || (hostname.endsWith(".."))) {
            // Invalid domain name
            return false;
        }
        if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith("."))
                || (pattern.endsWith(".."))) {
            // Invalid pattern/domain name
            return false;
        }

        // Normalize hostname and pattern by turning them into absolute domain names if they are not
        // yet absolute. This is needed because server certificates do not normally contain absolute
        // names or patterns, but they should be treated as absolute. At the same time, any hostname
        // presented to this method should also be treated as absolute for the purposes of matching
        // to the server certificate.
        //   www.android.com  matches www.android.com
        //   www.android.com  matches www.android.com.
        //   www.android.com. matches www.android.com.
        //   www.android.com. matches www.android.com
        if (!hostname.endsWith(".")) {
            hostname += '.';
        }
        if (!pattern.endsWith(".")) {
            pattern += '.';
        }
        // hostname and pattern are now absolute domain names.

        pattern = pattern.toLowerCase(Locale.US);
        // hostname and pattern are now in lower case -- domain names are case-insensitive.

        if (!pattern.contains("*")) {
            // Not a wildcard pattern -- hostname and pattern must match exactly.
            return hostname.equals(pattern);
        }
        // Wildcard pattern

        // WILDCARD PATTERN RULES:
        // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
        //    only character in that label (i.e., must match the whole left-most label).
        //    For example, *.example.com is permitted, while *a.example.com, a*.example.com,
        //    a*b.example.com, a.*.example.com are not permitted.
        // 2. Asterisk (*) cannot match across domain name labels.
        //    For example, *.example.com matches test.example.com but does not match
        //    sub.test.example.com.
        // 3. Wildcard patterns for single-label domain names are not permitted.

        if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) {
            // Asterisk (*) is only permitted in the left-most domain name label and must be the only
            // character in that label
            return false;
        }

        // Optimization: check whether hostname is too short to match the pattern. hostName must be at
        // least as long as the pattern because asterisk must match the whole left-most label and
        // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters.
        if (hostname.length() < pattern.length()) {
            // hostname too short to match the pattern.
            return false;
        }

        if ("*.".equals(pattern)) {
            // Wildcard pattern for single-label domain name -- not permitted.
            return false;
        }

        // hostname must end with the region of pattern following the asterisk.
        String suffix = pattern.substring(1);
        if (!hostname.endsWith(suffix)) {
            // hostname does not end with the suffix
            return false;
        }

        // Check that asterisk did not match across domain name labels.
        int suffixStartIndexInHostname = hostname.length() - suffix.length();
        if ((suffixStartIndexInHostname > 0)
                && (hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1)) {
            // Asterisk is matching across domain name labels -- not permitted.
            return false;
        }

        // hostname matches pattern
        return true;
    }
    private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
            "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
    /** Returns true if {@code host} is not a host name and might be an IP address. */
    public static boolean verifyAsIpAddress(String host) {
        return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
    }

}
测试

如果是直接 new OkHttpClient() 是可以抓包,如果 OkHttpClient okHttpClient = builder.build(); 就会提示证书验证错误。

OkHttpClient.Builder builder = OkHttpSSLUtils.getOkHttpBuilder(context,"test.com", "*.test.com.cer");
OkHttpClient okHttpClient = builder.build();
// OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url("https://api.test.com/user?uid=1").get().build();
Response response = okHttpClient.newCall(request).execute();
System.out.println(response.toString());
System.out.println(response.body().string());

 

本文地址:https://www.dandroid.cn/?p=7116
关注我们:请关注一下我们的今日头条号:扫描二维码到桌源码网的头条号,头条号:大安卓源码
版权声明:本文为原创文章,版权归 ImWiki 所有,欢迎分享本文,转载请保留出处!

发表评论


表情