`

iOS、Android、java服务端 DES+RSA安全传输统一实现

阅读更多
工作中遇到了安全传输问题,需要解决iOS和Android客户端跟java服务端的安全传输问题,结合对HTTPS的了解,便使用DES+RSA方式模拟HTTPS。在实现过程中,遇到了一些瓶颈,主要是保持平台兼容性的问题,Android和服务的还可以,统一使用java API,但要包含iOS就比较麻烦了,参考了网上很多资料,忙了三四天,终于搞通了。
瓶颈卡在用openssl生成的pem文件在java没找到合适的API来解析获取私钥,最后是参考网上资料用openssl命令将pem文件转换为pkcs8格式文件才能读取。

Mac OS上执行openssl命令操作
1)创建私钥
openssl genrsa -out private_key.pem 1024
2)创建证书请求(按照提示输入信息)
openssl req -new -out cert.csr -key private_key.pem
3)自签署根证书
openssl x509 -req -in cert.csr -out public_key.der -outform der -signkey private_key.pem -days 3650
4)用java代码要从这个文件中得到想要的priavtekey 可以先用命令(就被这东西卡住了)
openssl pkcs8 -topk8 -inform PEM -outform DER -in private_key.pem -out private_pkcs8_der.key -nocrypt

Android及java调用API

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;


public class Crypt {
	
	static BASE64Decoder decoder = new BASE64Decoder();
	static BASE64Encoder encoder = new BASE64Encoder();
    
    static String DES = "DES";
    static String RSA = "RSA";
    
    static String encode = "UTF-8";//保持平台兼容统一使用utf-8
    
    //私钥文件路径
    static String privateFile = "/XXX/private_pkcs8_der.key";
    //公钥文件路径
    static String publicFile = "/XXX/public_key.der";
	
	//des 加密
    private static byte [] encryptByteDES(byte[] byteD,String strKey) throws Exception {  
        return doEncrypt(byteD, getKey(strKey), DES);
    }
    //des 解密
    private static byte [] decryptByteDES(byte[] byteD,String strKey) throws Exception {  
        return doDecrypt(byteD, getKey(strKey), DES);
    }

    public static SecretKey getKey(String strKey)  throws Exception {
		DESKeySpec desKeySpec = new DESKeySpec(strKey.getBytes());
		SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
		SecretKey sk = keyFactory.generateSecret(desKeySpec);
		return sk;
	}
    
	//pkcs8_der.key文件为私钥 只能保存在服务端 
	//public_key.der为公钥文件,保存在客户端
	public static void main(String[] args) throws Exception {

		String text = "ceshishuuju测试数据ceshishuuju测试数据";
		String pwd="12345678";
		//客户端加密
		HashMap<String, String> data = DESAndRSAEncrypt(text.getBytes(encode),pwd);
		System.out.println("pwd RSA加密后base64:"+data.get("key"));
		System.out.println("text DES加密后base64:"+data.get("data"));

		//服务端解密
		String textDecrypt = DESAndRSADecrypt(data);
		System.out.println("未处理原文:"+text);
		System.out.println("解密后数据:"+textDecrypt);
//		generateKeyPair();
	}
	
	//客户端加密
	static HashMap<String, String> DESAndRSAEncrypt(byte[] dataToEncypt,String pwd) throws Exception{
		
		byte[] encryptData = encryptByteDES(dataToEncypt, pwd);
		String dataBase64 = encoder.encode(encryptData);
		
		byte[] encryptKey = RSAEncrypt(pwd.getBytes(encode));
		String keyBase64 = encoder.encode(encryptKey);

		HashMap<String, String> data = new HashMap<String, String>();
		data.put("data", dataBase64);
		data.put("key", keyBase64);
		return data;
	}
	//服务端解密
	static String DESAndRSADecrypt(HashMap<String, String> data) throws Exception {
		String dataBase64 = data.get("data");
		String keyBase64 = data.get("key");
		
		byte[] encryptedData = decoder.decodeBuffer(dataBase64);
		byte[] encryptedKey = decoder.decodeBuffer(keyBase64);
		
		byte[] decryptedKey= RSADecrypt(encryptedKey);
		String pwd = new String(decryptedKey,encode);
		System.out.println("DES密码:"+pwd);
		
		byte[] decryptedData  = decryptByteDES(encryptedData, pwd);
		String textDecrypt = new String(decryptedData,encode);
		return textDecrypt;
	}
	
	//公钥加密 
	public static byte[] RSAEncrypt(byte[] plainText) throws Exception{
		//读取公钥
		CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509");
		FileInputStream bais = new FileInputStream(publicFile);
		Certificate cert = certificatefactory.generateCertificate(bais);
		bais.close();
		PublicKey puk = cert.getPublicKey();
//		System.out.println("公钥base64:"+encoder.encode(puk.getEncoded()));
		return doEncrypt(plainText, puk, RSA);
	}
	//私钥解密
	public static byte[] RSADecrypt(byte[] encryptData) throws Exception{
		FileInputStream in = new FileInputStream(privateFile);
		ByteArrayOutputStream bout = new ByteArrayOutputStream();
		byte[] tmpbuf = new byte[1024];
		int count = 0;
		while ((count = in.read(tmpbuf)) != -1) {
			bout.write(tmpbuf, 0, count);
		}
		in.close();
		//读取私钥
		KeyFactory keyFactory = KeyFactory.getInstance(RSA);
		PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(bout.toByteArray());
		PrivateKey prk = keyFactory.generatePrivate(privateKeySpec);
//		System.out.println("私钥base64:"+encoder.encode(prk.getPrivateExponent().toByteArray()));
		return doDecrypt(encryptData, prk, RSA);
	}
	
	/**
	 * 执行加密操作
	 * @param data 待操作数据
	 * @param key Key
	 * @param type 算法 RSA or DES
	 * @return
	 * @throws Exception
	 */
	public static byte[] doEncrypt(byte[] data,Key key,String type) throws Exception{
		Cipher cipher = Cipher.getInstance(type);
		cipher.init(Cipher.ENCRYPT_MODE, key);
		return cipher.doFinal(data);
	}
	
	/**
	 * 执行解密操作
	 * @param data 待操作数据
	 * @param key Key
	 * @param type 算法 RSA or DES
	 * @return
	 * @throws Exception
	 */
	public static byte[] doDecrypt(byte[] data,Key key,String type) throws Exception{
		Cipher cipher = Cipher.getInstance(type);
		cipher.init(Cipher.DECRYPT_MODE, key);
		return cipher.doFinal(data);
	}
	
	public static void generateKeyPair() throws Exception{
		KeyPairGenerator kpg = KeyPairGenerator.getInstance(RSA);
		kpg.initialize(1024); // 指定密钥的长度,初始化密钥对生成器
		KeyPair kp = kpg.generateKeyPair(); // 生成密钥对
		RSAPublicKey puk = (RSAPublicKey) kp.getPublic();
		RSAPrivateKey prk = (RSAPrivateKey) kp.getPrivate();
		BigInteger e = puk.getPublicExponent();
		BigInteger n = puk.getModulus();
		BigInteger d = prk.getPrivateExponent();
		
		BASE64Encoder encoder = new BASE64Encoder();
		System.out.println("public key:\n"+encoder.encode(n.toByteArray()));
		System.out.println("private key:\n"+encoder.encode(d.toByteArray()));
	}
}



iOS调用API(其中有网上直接copy来的代码,在此多谢网友分享)

#import <Foundation/Foundation.h>

@interface WDCrypto : NSObject


/*
 * DES加密数据 RSA加密DES密钥 公钥证书路径(默认mainBundle下 public_key.der)
 * 返回加密后数据base64编码 {key:xxx,data:XXX}
 */
+(NSDictionary*)encryptWithDESAndRSA:(NSData*) data withKey:(NSString*)key keyPath:(NSString*)keyPath;

/*
 * RSA加密 输入NSString类型数据 公钥证书路径(默认mainBundle下 public_key.der)
 * 返回加密后数据base64编码
 */
+(NSString*)RSAEncryptData:(NSString*)text keyPath:(NSString*)keyPath;

/*
 * RSA加密 输入NSString类型数据 公钥证书路径(默认mainBundle下 public_key.der)
 * 返回加密后数据base64编码
 */
+(NSData*)RSAEncryptToData:(NSString*)text keyPath:(NSString*)keyPath;

/*
 * DES加密 输入NSString类型数据和NSString加密密钥 返回加密后数据base64编码
 */
+(NSString*)DESEncryptWithBase64:(NSString*)str key:(NSString*)key;

/*
 * DES加密 输入密文base64和NSString解密密钥 返回解密后数据明文
 */
+(NSString*)DESDecryptBase64:(NSString*)base64 key:(NSString*)key;

/*
 * DES加密 输入NSData类型数据和NSString加密密钥 返回加密后数据
 */
+(NSData *)DESEncrypt:(NSData *)data WithKey:(NSString *)key;

/*
 * DES加密 输入NSData类型密文和NSString解密密钥 返回解密后数据
 */
+(NSData *)DESDecrypt:(NSData *)data WithKey:(NSString *)key;

@end

/*
 * RSA加密 公钥文件默认为public_key.der
 */
@interface WDRSACrypt : NSObject {
    SecKeyRef publicKey;
    SecCertificateRef certificate;
    SecPolicyRef policy;
    SecTrustRef trust;
    size_t maxPlainLen;
}

/*
 *指定证书路径
 */
- (id)initWithKeyPath:(NSString*) publicKeyPath;

- (NSData *) encryptWithData:(NSData *)content;

- (NSData *) encryptWithString:(NSString *)content;

@end




#import "WDCrypto.h"

#import <CommonCrypto/CommonCryptor.h>

@implementation WDCrypto

//客户端加密
+(NSDictionary*)encryptWithDESAndRSA:(NSData*) data withKey:(NSString*)key keyPath:(NSString*)keyPath{

    //rsa
    NSString* encryptedKey = [WDCrypto RSAEncryptData:key keyPath:keyPath];
    
    //des
    NSData* encryptedData = [WDCrypto DESEncrypt:data WithKey:key];
    NSString* encryptedBase64 = [encryptedData base64Encoding];
    
    NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:encryptedKey,@"key",encryptedBase64,@"data",nil];
    return dict;
}

+(NSString*)RSAEncryptData:(NSString*)text keyPath:(NSString*)keyPath{
    NSData* data = [WDCrypto RSAEncryptToData:text keyPath:keyPath];
    NSString* encryptedKey = [data base64Encoding];
    return encryptedKey;
}

+(NSData*)RSAEncryptToData:(NSString*)text keyPath:(NSString*)keyPath{
    NSData* encryptData = nil;
    if (!keyPath) {
        keyPath = [[NSBundle mainBundle] pathForResource:@"public_key" ofType:@"der"];
    }
    WDRSACrypt *rsa = [[WDRSACrypt alloc] initWithKeyPath:keyPath];
    if (rsa != nil) {
        encryptData = [rsa encryptWithString:text];
    }else {
        NSLog(@"init rsa error");
    }
    return encryptData;
}

+(NSString*)DESEncryptWithBase64:(NSString*)str key:(NSString*)key{
    NSData* data = [str dataUsingEncoding:(NSUTF8StringEncoding)];
    NSData* encryptData = [WDCrypto DESEncrypt:data WithKey:key];
    NSString* base64 = [encryptData base64Encoding];
    return base64;
}

+(NSString*)DESDecryptBase64:(NSString*)base64 key:(NSString*)key{
    NSData* encryptData = [[NSData alloc] initWithBase64Encoding:base64];
    NSData* data = [WDCrypto DESDecrypt:encryptData WithKey:key];
    NSString* decryptStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    return decryptStr;
}

/******************************************************************************
 函数名称 : + (NSData *)DESEncrypt:(NSData *)data WithKey:(NSString *)key
 函数描述 : 文本数据进行DES加密
 输入参数 : (NSData *)data
 (NSString *)key
 输出参数 : N/A
 返回参数 : (NSData *)
 备注信息 : 此函数不可用于过长文本
 ******************************************************************************/
+ (NSData *)DESEncrypt:(NSData *)data WithKey:(NSString *)key
{
    char keyPtr[kCCKeySizeAES256+1];
    bzero(keyPtr, sizeof(keyPtr));
    
    [key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
    
    NSUInteger dataLength = [data length];
    
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);
    
    size_t numBytesEncrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, kCCAlgorithmDES,
                                          kCCOptionPKCS7Padding | kCCOptionECBMode,
                                          keyPtr, kCCBlockSizeDES,
                                          NULL,
                                          [data bytes], dataLength,
                                          buffer, bufferSize,
                                          &numBytesEncrypted);
    if (cryptStatus == kCCSuccess) {
        return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
    }
    
    free(buffer);
    return nil;
}

/******************************************************************************
 函数名称 : + (NSData *)DESEncrypt:(NSData *)data WithKey:(NSString *)key
 函数描述 : 文本数据进行DES解密
 输入参数 : (NSData *)data
 (NSString *)key
 输出参数 : N/A
 返回参数 : (NSData *)
 备注信息 : 此函数不可用于过长文本
 ******************************************************************************/
+ (NSData *)DESDecrypt:(NSData *)data WithKey:(NSString *)key
{
    char keyPtr[kCCKeySizeAES256+1];
    bzero(keyPtr, sizeof(keyPtr));
    
    [key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
    
    NSUInteger dataLength = [data length];
    
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);
    
    size_t numBytesDecrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt, kCCAlgorithmDES,
                                          kCCOptionPKCS7Padding | kCCOptionECBMode,
                                          keyPtr, kCCBlockSizeDES,
                                          NULL,
                                          [data bytes], dataLength,
                                          buffer, bufferSize,
                                          &numBytesDecrypted);
    
    if (cryptStatus == kCCSuccess) {
        return [NSData dataWithBytesNoCopy:buffer length:numBytesDecrypted];
    }
    
    free(buffer);
    return nil;
}

@end



@implementation WDRSACrypt

- (id)initWithKeyPath:(NSString*) publicKeyPath{
    self = [super init];
    
    if (publicKeyPath == nil) {
        NSLog(@"Can not find pub.der");
        return nil;
    }
    
    NSData *publicKeyFileContent = [NSData dataWithContentsOfFile:publicKeyPath];
    
    if (publicKeyFileContent == nil) {
        NSLog(@"Can not read from pub.der");
        return nil;
    }
    
    certificate = SecCertificateCreateWithData(kCFAllocatorDefault, ( __bridge CFDataRef)publicKeyFileContent);
    if (certificate == nil) {
        NSLog(@"Can not read certificate from pub.der");
        return nil;
    }
    
    policy = SecPolicyCreateBasicX509();
    OSStatus returnCode = SecTrustCreateWithCertificates(certificate, policy, &trust);
    if (returnCode != 0) {
        NSLog(@"SecTrustCreateWithCertificates fail. Error Code: %ld", returnCode);
        return nil;
    }
    
    SecTrustResultType trustResultType;
    returnCode = SecTrustEvaluate(trust, &trustResultType);
    if (returnCode != 0) {
        NSLog(@"SecTrustEvaluate fail. Error Code: %ld", returnCode);
        return nil;
    }
    
    publicKey = SecTrustCopyPublicKey(trust);
    if (publicKey == nil) {
        
        NSLog(@"SecTrustCopyPublicKey fail");
        return nil;
    }
    
    maxPlainLen = SecKeyGetBlockSize(publicKey) - 12;
    return self;
}

- (NSData *) encryptWithData:(NSData *)content {
    
    size_t plainLen = [content length];
    if (plainLen > maxPlainLen) {
        NSLog(@"content(%ld) is too long, must < %ld", plainLen, maxPlainLen);
        return nil;
    }
    
    void *plain = malloc(plainLen);
    [content getBytes:plain
               length:plainLen];
    
    size_t cipherLen = 128; // 当前RSA的密钥长度是128字节
    void *cipher = malloc(cipherLen);
    
    OSStatus returnCode = SecKeyEncrypt(publicKey, kSecPaddingPKCS1, plain,
                                        plainLen, cipher, &cipherLen);
    
    NSData *result = nil;
    if (returnCode != 0) {
        NSLog(@"SecKeyEncrypt fail. Error Code: %ld", returnCode);
    }
    else {
        result = [NSData dataWithBytes:cipher
                                length:cipherLen];
    }
    
    free(plain);
    free(cipher);
    
    return result;
}

- (NSData *) encryptWithString:(NSString *)content {
    return [self encryptWithData:[content dataUsingEncoding:NSUTF8StringEncoding]];
}

- (void)dealloc{
    CFRelease(certificate);
    CFRelease(trust);
    CFRelease(policy);
    CFRelease(publicKey);
}

@end


参考网址:
http://blog.iamzsx.me/show.html?id=155002
http://shuany.iteye.com/blog/730910
分享到:
评论
3 楼 a30292330 2015-07-12  
那怎么办?难道有的版本不行?
2 楼 geniuswxk 2015-02-05  
这个地方后来又发现一个问题,Android和Java服务端RSA加密解密的方式有所不同,好像是虚拟机的实现方式不一样,所以Android还不能直接用这套东西
1 楼 geniuswxk 2015-02-02  
给自己点个赞

相关推荐

    JAVA实现的DES+RSA

    本文将深入探讨如何在Java环境中实现DES(Data Encryption Standard)和RSA(Rivest-Shamir-Adleman)这两种加密算法的结合,以提供更为安全的数据保护。 DES是一种对称加密算法,它使用相同的密钥进行加密和解密。...

    Ios (3DES+RSA)

    openssl生成rsa参考 http://blog.csdn.net/chaijunkun/article/details/7275632/ rsa 公私钥加解密设置参考https://github.com/jslim89/RSA-objc 3des部分直接des3文件

    Java实现文件的RSA和DES加密

    Java 实现文件的 RSA 和 DES 加密 在现代密码技术中,根据密钥类型的不同,可以将其分为两类:对称加密算法(秘密钥匙加密)和非对称加密算法(公开密钥加密)。对称加密算法用来对敏感数据等信息进行加密,常用的...

    密码学编程操作指南(DES+RSA)

    密码学编程操作指南(包括DES+RSA)

    基于C++实现DES+RSA+ZUC等算法源码+PPT报告(信息安全课程实验).zip

    基于C++实现DES+RSA+ZUC等算法源码+PPT报告(信息安全课程实验).zip 该资源内项目源码是个人的课设,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到94.5分,放心下载使用! 该资源适合计算机相关专业...

    Java RSA,MD5 string,MD5 File,DES,ELGamal算法实现+RSA数字签名

    RSA、MD5 字符串、MD5文件、DES、ELGamal等加密算法实现,以及RSA模拟数字签名过程(工程Crypto和RSASignature),全部用GUI展现(NetBean6.5的Swing库),要求JDK1.5+,读者可以直接运行dist目录下的jar文件,一目了然...

    Java语言实现的md5,rsa算法传送文件

    总的来说,这个项目结合了Java的MD5和RSA算法,通过Applet和Socket实现了安全的文件传输。用户通过Applet选择并加密文件,然后通过Socket将加密后的文件发送到服务器,服务器端再用对应的私钥解密并验证MD5摘要,以...

    基于MD5+RSA的数字签名设计与实现(源码+文档)-RSA-数字签名.zip

    资源名字:基于MD5+RSA的数字签名设计与实现(源码+文档)_RSA_数字签名.zip 资源内容:项目全套源码+完整文档 源码说明: 全部项目源码都是经过测试校正后百分百成功运行。 适合场景:相关项目设计 项目详细介绍可...

    redis+mybatis+RSA+DES+登陆授权.zip

    标题中的“redis+mybatis+RSA+DES+登陆授权”是一个综合性的技术组合,涉及到数据库缓存、持久化框架、加密算法以及用户登录授权等多个关键领域。这些技术在实际的Web开发中扮演着重要的角色。下面将分别详细介绍...

    Android Java iOS 三端RSA和AES双向加密修正版

    以下是关于RSA和AES加密算法以及如何在Android、Java和iOS上实现它们的关键知识点。 **RSA加密算法**: RSA是一种非对称加密算法,由Ron Rivest、Adi Shamir和Leonard Adleman在1977年提出。它基于大整数因子分解的...

    AES+RSA加密解密(js和java互通).zip

    总的来说,"AES+RSA加密解密(js和java互通)"项目旨在解决跨平台间的加密通信问题,通过结合AES和RSA的优势,确保数据在JavaScript和Java环境间的安全传输。开发者需要深入理解这两种加密算法,并熟练掌握相关库的...

    Java实现DES、RSA、MD5

    在Java中,可以使用内置的`java.security`和`javax.crypto`包来实现DES、RSA和MD5算法。以下是这些技术的详细介绍: 1. **DES(Data Encryption Standard)**:DES是一种对称加密算法,它使用64位的密钥对数据进行...

    Java_DES和RSA加解密

    在实际应用中,DES常用于对大量数据进行快速加密,而RSA则适用于在不安全的网络上安全传输密钥,或者在小规模数据上提供更高级别的安全性。两种加密算法各有优缺点,可以根据具体需求和场景选择使用。

    Android+Java中使用RSA加密实现接口调用时的校验功能的工具类.rar

    在Android和Java应用开发中,RSA(Rivest-Shamir-Adleman)加密算法是一种广泛用于数据安全传输和接口调用校验的技术。RSA是一种非对称加密算法,其核心特点是拥有两个密钥:公钥和私钥。公钥可以公开给任何人,而...

    C#和Java实现互通的RSA&DES加解密算法

    本话题主要关注如何在C#和Java之间通过RSA和DES加密算法实现数据的安全互通。RSA是一种非对称加密算法,适用于小量数据加密,如密钥交换;而DES是对称加密算法,适合大量数据的快速加密,但安全性相对较低。 首先,...

    使用objective c 和java实现的rsa sha256 适用于ios与android

    综合来看,这个项目可能是提供了一套跨平台的解决方案,用于在iOS和Android之间进行RSA SHA256签名和验证,确保数据的安全传输。通过Objective-C和Java实现的API,可以在两个平台上分别进行加密和解密操作,保证信息...

    ECC+DES+RSA加解密c程序源码

    密码学ECC椭圆曲线,RSA,DES加解密C程序实现。

    rsa与aes混合加密java实现

    本文将深入探讨两种常见的加密算法:RSA和AES,并介绍如何在Java中实现这两种算法的混合加密。RSA是一种非对称加密算法,而AES是一种对称加密算法,它们各有优势,结合使用可以提供更强大的安全保障。 **RSA算法** ...

    AES+RSA加解密服务端

    综上所述,"AES+RSA加解密服务端"是信息安全领域的一个重要应用,它利用了两种加密算法的优势,确保了数据在传输过程中的安全性。在Java开发环境中,我们可以方便地利用提供的API来实现这些功能,从而构建安全的数据...

    安全+加密+RSA+原理和实现

    本资源详细讲解了RSA的原理及其在Java环境中的实现,帮助理解加密技术的核心概念。 首先,RSA算法基于数论中的两个基本事实:大整数分解困难(即大素因数分解问题)和欧拉函数性质。RSA的名字来源于其三位发明者...

Global site tag (gtag.js) - Google Analytics