UzzzzZ

2023-10-20 15:58:40 浏览数 (3)

GTBank APP SecurityTest

前言

国庆放假期间领导给了一个任务,分析非洲某银行APP,绕过反抓包,并且分析加密算法,能实现自动登录,这也是我第一次分析APP(以前从未接触,也只是看大佬们的文章),所以记录一下

一、反编译APK获得源码

1、需要的工具
  • Apktool

下载地址:http://ibotpeaches.github.io/Apktool/install/

  • dex2jar
  • jd-gui
2、反编译代码

先把.apk后缀修改为.zip后解压,再使用dex2jar工具

代码语言:javascript复制
d2j-dex2jar classes.dex

该jar包可以用jd-gui打开

2、反编译资源
代码语言:javascript复制
apktool.bat d xxx.apk

二、绕反抓包

1、安卓5.x

在自己写东拼西凑写Hook的时候发现,安卓版本在5.x以下的话,是可以不需要Xposed的,直接burp代理 证书即可

未安装Xposed JustTrustMe

1、SSL Pinning

夜神模拟器

主要是公司测试机放公司了忘记拿了,所以用夜神模拟器 :) 国庆放假太开心

安卓版本:Android 5(已Root)

Burp 2020

Java11

Fiddler

需要下载JustTrustMe:https://github.com/Fuzion24/JustTrustMe/releases/tag/v.2

Xposed框架: http://repo.xposed.info/module/de.robv.android.xposed.installer

但是如果使用夜神模拟器的话,安装Xposed框架的时候会出现一个情况,夜神模拟器会提示你该框架可能不兼容或不适用,夜神模拟器本身在应用商店中已经存在Xposed框架,是否需要安装夜神模拟器官方的Xposed框架,我这里测试使用的是夜神模拟器官方的Xposed框架 Tips:这里在安装Xposed.info下载的框架时遇到一个问题,如果使用官方的框架的话,就会遇到可能x86不兼容或者不适用的情况,夜神模拟器建议最方便的就是使用夜神模拟器的Xposed框架 如果不想使用夜神的Xposed框架,只需要更换降低安卓版本即可 夜神模拟器更换安卓版本的步骤 1、选择多开

2、选择添加模拟器中的三个点

随后安装安卓5即可 至于更低的版本,暂时没研究夜神如何更换

随后Burp开启代理,模拟器WiFi修改一下代理即可

正常打开APP抓包

那么如何验证这个包是否抓到,只需要点击forward,如果弹出账号错误,那么就是成功了

绕过原理

使用JustTrustMe来突破验证的原理就是,JustTrustMe将APK中所有用于校验的SSL证书的API都进行了HOOK,从而绕过证书验证。

Https建立的完整过程

客户端与服务端经过通信交换获得了三个随机数,通过这三个随机数,客户端与服务端能够使用相同的算法生成后续HTTP通信过程中对称加密算法使用的密钥。也就是说HTTPS协议中非对称加密只是在协议建立时使用,协议建立后使用的是对称加密。

SSL-PinNing技术,在开发的时候将服务端整数打包在客户端里,这样在Https建立时与服务端返回的证书对比一致性。

安卓实现Https的几种方式

1、通过OkHttp来实现

第三方库,OkHttp中进行SSL证书校验,有如下两种方式

1、CertificatePinner(证书锁定)

通过CertificatePinner进行连接的OkHttp,在连接之前,会调用其Check方法进行证书校验

2、自定义证书和HostNameVerify来实现Https校验

OkHttp中如果不执行HostNameVerifier默认调用的是OkHostNameVerifier.verify进行服务器主机名校验,如果设置了HostNameVerifier,则默认调用的是自定义的Verify方法

绕过SSL证书验证,Xposed需要Hook的方法名和类名

类名

方法名

com.squareup.okhttp.CertificatePinner

public void check(String hostname, List peerCertificates) throws SSLPeerUnverifiedException{}

com.squareup.okhttp.CertificatePinner

public void check(String,List)

okhttp3.internal.tls.OkHostnameVerifier

public boolean verify(String, SSLSession)

okhttp3.internal.tls.OkHostnameVerifier

public boolean verify(String, X509Certificate)

okhttp3.OkHttpClient.Builder

public OkHttpClient.Builder hostnameVerifier(HostnameVerifier hostnameVerifier)

JustTrustME 中的代码并没有 Hook (public OkHttpClient.Builder hostnameVerifier)这个方法,应 该是漏掉了这个方法。 对其中上述四个方法只需要 Hook 函数后,不抛出异常,并设置函数返回值为 true 即可绕过 验证。 对 于 OkHttpClient.Builder 中 的 hostnameVerifier 方 法 的 Hook , 替 换 成 自 定 义 的 HostnameVerifier(上述代码中的 MyHostnameVerifier 即可)。

2、通过Apache的HttpClient来实现

HttpClient中进行SSL证书校验,也分为两种方式

1、通过在APK中内置的整数初始化与一个KeyStore,然后用这个Keystore去引导生成的TrustManager来提供验证

2、自定义SSLSocketFacetory实现其中的TrustManager校验策略

绕过上述SSL证书验证,Xposed需要Hook的方法名和类名

类名

方法名

external/apachehttp/src/org/apache/http/impl/client/DefaultHttpClie nt.java

public DefaultHttpClient()

external/apachehttp/src/org/apache/http/impl/client/DefaultHttpClie nt.java

public DefaultHttpClient(HttpParams params)

external/apachehttp/src/org/apache/http/impl/client/DefaultHttpClie nt.java

public DefaultHttpClient(ClientConnectionMa nager conman, HttpParams params)

external/apachehttp/src/org/apache/http/conn/ssl/SSLSocketFactory. java

public SSLSocketFactory(String, KeyStore, String, KeyStore)

external/apachehttp/src/org/apache/http/conn/ssl/SSLSocketFactory. java

public SSLSocketFactory(String, KeyStore, String, KeyStore)

Hook 的 DefaultHttpClient 三个构造方法,对中都调用(ClientConnectionManager, HttpParams) 这个函数,其中重点需要 Hook 的是 ClientConnectionManager 这个参数,将其替换成如下函 数内容,让其信任所有证书:

代码语言:javascript复制
public ClientConnectionManager getSCCM() {
 KeyStore trustStore;
 try {
     trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
     trustStore.load(null, null);
     SSLSocketFactory sf = new TrustAllSSLSocketFactory(trustStore);
     sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
     SchemeRegistry registry = new SchemeRegistry();
     registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
     registry.register(new Scheme("https", sf, 443));
     ClientConnectionManager ccm = new SingleClientConnManager(null, registry);
     return ccm;
     } catch (Exception e) {
         return null;
 }
}

Hook 的 SSLSocketFactory 重点是替换其中 TrustManager,将其策略可以加载信任任意 证书,替换后“TrustManager”代码如下:

代码语言:javascript复制
class ImSureItsLegitTrustManager implements X509TrustManager {
 @Override
 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
{ }
 @Override
 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
{ }
 @Override
 public X509Certificate[] getAcceptedIssuers() {
 return new X509Certificate[0];
 }
}
3、通过HttpsURLConnection来实现

HttpsURLConnection中进行SSL证书校验,也分为两种方式

1、自定义的HostNameVerifier和X509TrustManager实现

2、使用内置的整数初始化一个KeyStore实现Manager

绕过上述SSL证书验证,Xposed需要Hook的方法名和类名

类名

方法名

libcore/luni/src/main/java/javax/net/ssl/TrustMana gerFactory.java

public final TrustManager[] getTrustManager()

libcore/luni/src/main/java/javax/net/ssl/HttpsURL Connection.java

public void setDefaultHostnameVerifier(HostnameVe rifier)

libcore/luni/src/main/java/javax/net/ssl/HttpsURL Connection.java

public void setSSLSocketFactory(SSLSocketFactory)

libcore/luni/src/main/java/javax/net/ssl/HttpsURL Connection.java

public void setHostnameVerifier(HostNameVerifier)

getTrustManager的Hook,跟2一样,换成自定义的TrustManager让其信任所有证书。

4、Webview加载Https页面时的证书校验

Adnroid中通过Webview加载Https页面时,如果出现证书校验错误,则会停止加载页面,因为只需要Hook掉WebView的证书校验失败的处理方法:onReceivedSsLError,让其继续加载即可

类名

方法名

frameworks/base/core/java/android/webkit/W ebViewClient.java

public void onReceivedSslError(Webview, SslErrorHandler, SslError)

frameworks/base/core/java/android/webkit/W ebViewClient.java

public void onReceivedError(WebView, int, String, String)

只需要WEbview继续加载网页即可:handler.processd()

5、JustTrustMe中其他Hook函数

类名

方法名

external/apachehttp/src/org/apache/http/conn/ssl/SSLSocketF actory.java

public static SSLSocketFactory getSocketFactory()/已废弃

external/apachehttp/src/org/apache/http/conn/ssl/SSLSocketF actory.java

public boolean isSecure(Socket)/已废弃

ch.boye.httpclientandroidlib.conn.ssl.Abstract Verifier

verify(String, String[], String[], boolean)

前两个函数已经基本没有在使用,而第三个是使用的第三方的库——httpclientandroidlib 进行进行 https 连接的,该 jar14 年以后也没在更新了,几乎没人在使用。

2、绕过WebView onReceivedSslError检测

对上述提供的方法,反编译APK后在Jar包中搜索可以发现,使用了onReceivedSsLError进行检测

1、检测点InAppBrowser.clss
2、检测点SystemWebViewClient.class

3、自己写Hook

虽然有现成的xposed和justTrustme,但是对原理比较好奇,自己动手照着教程也写了一个基于xposed的hook

其实是在JustTrustme里面抄的

代码语言:javascript复制
package Hook;

import android.net.http.SslError;
import android.util.Log;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

import org.json.JSONObject;

import javax.xml.transform.ErrorListener;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class HookMain implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        String packageName = lpparam.packageName;
        XposedHelpers.findAndHookMethod("android.webkit.WebViewClient", lpparam.classLoader, "onReceivedSslError",
                WebView.class, SslErrorHandler.class, SslError.class, new XC_MethodReplacement() {
                    @Override
                    protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
                        XposedBridge.log("开始绕过");
                        ((android.webkit.SslErrorHandler) param.args[1]).proceed();
                        return null;
                    }
                });

    }
}

但是在准备测试自己的Hook的时候突然傻了,发现安卓5.0以下都可以不需要用xposed就可以抓包。。。所以自己的hook就没有测试,也不知道该的对不对。。。

三、解密WebView前端代码

1、反编译APK获得WebView源码

对APK反编译后可以发现assets下是前端代码,但是该代码进行了加密,简单查看后发现第一次加密是Base64,

通过对APK的逆向,可以在源代码中发现,加密方式是AES(CBC, PKCS5Padding),在private中可以看到KEY和VI,那么对其解密

2、Python3解密WebView源码

代码语言:javascript复制
from Crypto.Cipher import AES  
from base64 import b64decode, b64encode
import os
BLOCK_SIZE = AES.block_size
# 不足BLOCK_SIZE的补位(s可能是含中文,而中文字符utf-8编码占3个位置,gbk是2,所以需要以len(s.encode()),而不是len(s)计算补码)
pad = lambda s: s   (BLOCK_SIZE - len(s.encode()) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s.encode()) % BLOCK_SIZE)
# 去除补位
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


class AESCipher:
    def __init__(self, secretkey: str, vi: str):
        self.key = secretkey  # 密钥
        self.iv = vi  # 偏移量

    def encrypt(self, text):
        """
        加密 :先补位,再AES加密,后base64编码
        :param text: 需加密的明文
        :return:
        """
        # text = pad(text) 包pycrypto的写法,加密函数可以接受str也可以接受bytess
        text = pad(text).encode()  # 包pycryptodome 的加密函数不接受str
        cipher = AES.new(key=self.key.encode(), mode=AES.MODE_CBC, IV=self.iv.encode())
        encrypted_text = cipher.encrypt(text)
        # 进行64位的编码,返回得到加密后的bytes,decode成字符串
        return b64encode(encrypted_text).decode('utf-8')

    def decrypt(self, encrypted_text):
        """
        解密 :偏移量为key[0:16];先base64解,再AES解密,后取消补位
        :param encrypted_text : 已经加密的密文
        :return:
        """
        encrypted_text = b64decode(encrypted_text)
        cipher = AES.new(key=self.key.encode(), mode=AES.MODE_CBC, IV=self.iv.encode())
        decrypted_text = cipher.decrypt(encrypted_text)
        return unpad(decrypted_text).decode('utf-8')



key = 'k6qBTDf7HVWSWdThFVkgYiTEdZFIRSAd'
vi = 'MCvyRMdSJW15wfBb'
NewCipher = AESCipher(key, vi)

Ciphertext = '密文'

decrypt = NewCipher.decrypt(Ciphertext)
print(decrypt)

3、分析WebView前端源码

1、寻找加密算法

通过报错关键词与抓包的关键字可以轻松的找到,随后用IDE的功能,查看password的调用

可以看到password都是来自Sign_In_View.js

跟过去后直接看encrypt这个方法

随后来到了aesCTR.js

通过注释可以发现,AES.Ctr.encrypt这个函数,接收三个参数,第一个Plaintext是要加密的文本字符串,第二个password是加密用的key,第三个是要在密钥中使用的位数

那么需要加密的密文很显然就是传进去的参数,password也写在了appData.js里面,密钥的位数也写在了调用传参里

目前可以确定,有AES的key,加密算法为AES的CTR模式,那么还需要找到一个计数器,寻找一些关键字发现,计数器是一个取当前时间戳

那么换一种思路,是不是只需要调用这个JS就可以达到逆加密的效果?因为这里也有encrypt也有decrypt,那么我只需要调用encrypt去加密,再调用decrypt解密即可,而且搜索银行官方的加密算法中的注释可以搜索到,其实这是一段公开的加密算法JS代码,既然找到一模一样的,直接引入到HTML中,打开控制台调用对应的函数即可

之前实战分析过steam登录的rsa加密算法,所以在此次分析中,直接照搬上次的方法,调用即可

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="aes.js" type="text/javascript"></script>
    <!-- aes.js见文章末尾 -->
</body>
</html>

打开控制台,直接调用,并输入appdate.js中的AESKEY和256的位数

可以正常加解密,那么抓包后,将POST请求的数据拿过来解密,看看能否解密

正常解密,再进行英文字母测试,成功!

至此,该APP 前端源码解密算法与登录加解密算法已全部逆向完成

1、WebView加密算法表

算法

密钥

AES CBC PkCsDDing Key

k6qBTDf7HVWSWdThFVkgYiTEdZFIRSAd

AES CBC PkCsDDing VI

MCvyRMdSJW15wfBb

2、账户加密算法表

算法

密钥

AES CTR Key

B374A26A71490437AA024E4FADD5B497FDFF1A8EA6FF12F6FB65AF2720B59CCF

2、分析加解密算法后的自动登录

知道加解密算法的原理后,其实自动登录是比较简单的,第一想法就是requests配合requests.session来达到自动登录的效果

但是目前这里只能自己手动去加密然后再输入进去,为了做到自动化,尝试了execjs用python的库来调用JS,不知道什么原因,一直报GBK的错误,无解!,只能换成js2py库了

可以正常加解密

那么只需要把加密和解密封装成两个函数即可

先手动输入正确的账号密码,看看APP返回什么信息

如果账号密码正确,那么就会提示用户需要绑定设备(也就是注册OTP)

那么看看脚本自动登录是不是也是这样

已大致实现了自动登录的功能,登录成功后,session会保持会话,只需要post和get其他页面即可,至此,目标为自动登录的任务已经完成。

1、Python实现自动登录代码
代码语言:javascript复制
import requests.sessions
import js2py

req = requests.session()

'''
POST /ISWBPGTBankWeb/ws/authenticate HTTP/1.1
Host: mbank.gtbank.com:8443
Connection: close
Content-Length: 199
Accept: */*
Origin: file://
User-Agent: Mozilla/5.0 (Linux; Android 5.1.1; SM-G973N Build/PPR1.190810.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.136 Mobile Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: visid_incap_981350=yQL3gJ2wSWG5sjrdkNj357NQXWEAAAAAQUIPAAAAAADbIvu6L78AQkiegOwinUSR; incap_ses_1223_981350=z0dND28uIk /JUv7Zvj4ELVQXWEAAAAAZ59d8Yt2cKjqPkqbgVpgeQ==; incap_ses_1224_981350= LfMHhgfpD vCvr34IX8EPVrXWEAAAAAc7GpzsoPIi39Crnw53T52w==
X-Requested-With: com.vanso.gtbankapp

loginId=pwInl/lrXWGGpfsqQSj/cgY=&password=pwKUPPlrXWE7qjD8EYMMjck=&device_uuid=pwL0dvlrXWFTlsXfG3oBCcB5QQ5y8IOm&email=&appVersion=4.4.4&airshipChannelId=&airshipIdentifierType=android_channel
'''

headers = {
    "Accept": "*/*",
    "Origin": "file://",
    "User-Agent": "Mozilla/5.0 (Linux; Android 5.1.1; SM-G973N Build/PPR1.190810.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.136 Mobile Safari/537.36",
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
    "Cookie": "visid_incap_981350=yQL3gJ2wSWG5sjrdkNj357NQXWEAAAAAQUIPAAAAAADbIvu6L78AQkiegOwinUSR; incap_ses_1223_981350=z0dND28uIk /JUv7Zvj4ELVQXWEAAAAAZ59d8Yt2cKjqPkqbgVpgeQ==; incap_ses_1224_981350=bb6DJp/28A7pbgj44IX8EBt7XWEAAAAAo5YIaGF6It7gKFrN9CqSmQ==",
    "X-Requested-With": "com.vanso.gtbankapp"
}

context = js2py.EvalJs()


# 解密算法
def decrypt(text):
    with open('aes.js', encoding='utf-8') as f:
        jsdata = f.read()

    context.execute(jsdata)
    result = context.Aes.Ctr.decrypt(text, 'B374A26A71490437AA024E4FADD5B497FDFF1A8EA6FF12F6FB65AF2720B59CCF', 256)
    print(result)


# 加密算法
def encrypt(text):
    with open('aes.js', encoding='utf-8') as f:
        jsdata = f.read()

    context.execute(jsdata)
    result = context.Aes.Ctr.encrypt(text, 'B374A26A71490437AA024E4FADD5B497FDFF1A8EA6FF12F6FB65AF2720B59CCF', 256)
    return result


def LoginBank(account, password):
    crypt_account = encrypt(account)
    crypt_password = encrypt(password)
    print('[ ]加密后账号: ', crypt_account)
    print('[ ]加密后密码: ', crypt_password)
    data = {
        'loginId': crypt_account,
        'password': crypt_password,
        'device_uuid': 'pwL0dvlrXWFTlsXfG3oBCcB5QQ5y8IOm',
        'email': '',
        "appVersion": "4.4.4",
        "airshipChannelId": "",
        "airshipIdentifierType": "android_channel"
    }
    result = req.post(url='https://mbank.gtbank.com:8443/ISWBPGTBankWeb/ws/authenticate', data=data, headers=headers)
    print(result.json())


LoginBank('51224580701', '456789')
2、aes.js解密算法原文
代码语言:javascript复制
var Aes = {};  // Aes namespace

/**
 * AES Cipher function: encrypt 'input' state with Rijndael algorithm
 *   applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage
 *
 * @param {Number[]} input 16-byte (128-bit) input state array
 * @param {Number[][]} w   Key schedule as 2D byte-array (Nr 1 x Nb bytes)
 * @returns {Number[]}     Encrypted output state array
 */
Aes.cipher = function(input, w) {    // main Cipher function [§5.1]
  var Nb = 4;               // block size (in words): no of columns in state (fixed at 4 for AES)
  var Nr = w.length/Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys

  var state = [[],[],[],[]];  // initialise 4xNb byte-array 'state' with input [§3.4]
  for (var i=0; i<4*Nb; i  ) state[i%4][Math.floor(i/4)] = input[i];

  state = Aes.addRoundKey(state, w, 0, Nb);

  for (var round=1; round<Nr; round  ) {
    state = Aes.subBytes(state, Nb);
    state = Aes.shiftRows(state, Nb);
    state = Aes.mixColumns(state, Nb);
    state = Aes.addRoundKey(state, w, round, Nb);
  }

  state = Aes.subBytes(state, Nb);
  state = Aes.shiftRows(state, Nb);
  state = Aes.addRoundKey(state, w, Nr, Nb);

  var output = new Array(4*Nb);  // convert state to 1-d array before returning [§3.4]
  for (var i=0; i<4*Nb; i  ) output[i] = state[i%4][Math.floor(i/4)];
  return output;
}

/**
 * Perform Key Expansion to generate a Key Schedule
 *
 * @param {Number[]} key Key as 16/24/32-byte array
 * @returns {Number[][]} Expanded key schedule as 2D byte-array (Nr 1 x Nb bytes)
 */
Aes.keyExpansion = function(key) {  // generate Key Schedule (byte-array Nr 1 x Nb) from Key [§5.2]
  var Nb = 4;            // block size (in words): no of columns in state (fixed at 4 for AES)
  var Nk = key.length/4  // key length (in words): 4/6/8 for 128/192/256-bit keys
  var Nr = Nk   6;       // no of rounds: 10/12/14 for 128/192/256-bit keys

  var w = new Array(Nb*(Nr 1));
  var temp = new Array(4);

  for (var i=0; i<Nk; i  ) {
    var r = [key[4*i], key[4*i 1], key[4*i 2], key[4*i 3]];
    w[i] = r;
  }

  for (var i=Nk; i<(Nb*(Nr 1)); i  ) {
    w[i] = new Array(4);
    for (var t=0; t<4; t  ) temp[t] = w[i-1][t];
    if (i % Nk == 0) {
      temp = Aes.subWord(Aes.rotWord(temp));
      for (var t=0; t<4; t  ) temp[t] ^= Aes.rCon[i/Nk][t];
    } else if (Nk > 6 && i%Nk == 4) {
      temp = Aes.subWord(temp);
    }
    for (var t=0; t<4; t  ) w[i][t] = w[i-Nk][t] ^ temp[t];
  }

  return w;
}

/*
 * ---- remaining routines are private, not called externally ----
 */

Aes.subBytes = function(s, Nb) {    // apply SBox to state S [§5.1.1]
  for (var r=0; r<4; r  ) {
    for (var c=0; c<Nb; c  ) s[r][c] = Aes.sBox[s[r][c]];
  }
  return s;
}

Aes.shiftRows = function(s, Nb) {    // shift row r of state S left by r bytes [§5.1.2]
  var t = new Array(4);
  for (var r=1; r<4; r  ) {
    for (var c=0; c<4; c  ) t[c] = s[r][(c r)%Nb];  // shift into temp copy
    for (var c=0; c<4; c  ) s[r][c] = t[c];         // and copy back
  }          // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES):
  return s;  // see asmaes.sourceforge.net/rijndael/rijndaelImplementation.pdf
}

Aes.mixColumns = function(s, Nb) {   // combine bytes of each col of state S [§5.1.3]
  for (var c=0; c<4; c  ) {
    var a = new Array(4);  // 'a' is a copy of the current column from 's'
    var b = new Array(4);  // 'b' is a•{02} in GF(2^8)
    for (var i=0; i<4; i  ) {
      a[i] = s[i][c];
      b[i] = s[i][c]&0x80 ? s[i][c]<<1 ^ 0x011b : s[i][c]<<1;

    }
    // a[n] ^ b[n] is a•{03} in GF(2^8)
    s[0][c] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3]; // 2*a0   3*a1   a2   a3
    s[1][c] = a[0] ^ b[1] ^ a[2] ^ b[2] ^ a[3]; // a0 * 2*a1   3*a2   a3
    s[2][c] = a[0] ^ a[1] ^ b[2] ^ a[3] ^ b[3]; // a0   a1   2*a2   3*a3
    s[3][c] = a[0] ^ b[0] ^ a[1] ^ a[2] ^ b[3]; // 3*a0   a1   a2   2*a3
  }
  return s;
}

Aes.addRoundKey = function(state, w, rnd, Nb) {  // xor Round Key into state S [§5.1.4]
  for (var r=0; r<4; r  ) {
    for (var c=0; c<Nb; c  ) state[r][c] ^= w[rnd*4 c][r];
  }
  return state;
}

Aes.subWord = function(w) {    // apply SBox to 4-byte word w
  for (var i=0; i<4; i  ) w[i] = Aes.sBox[w[i]];
  return w;
}

Aes.rotWord = function(w) {    // rotate 4-byte word w left by one byte
  var tmp = w[0];
  for (var i=0; i<3; i  ) w[i] = w[i 1];
  w[3] = tmp;
  return w;
}

// sBox is pre-computed multiplicative inverse in GF(2^8) used in subBytes and keyExpansion [§5.1.1]
Aes.sBox =  [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
             0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
             0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
             0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
             0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
             0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
             0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
             0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
             0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
             0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
             0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
             0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
             0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
             0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
             0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
             0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16];

// rCon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [§5.2]
Aes.rCon = [ [0x00, 0x00, 0x00, 0x00],
             [0x01, 0x00, 0x00, 0x00],
             [0x02, 0x00, 0x00, 0x00],
             [0x04, 0x00, 0x00, 0x00],
             [0x08, 0x00, 0x00, 0x00],
             [0x10, 0x00, 0x00, 0x00],
             [0x20, 0x00, 0x00, 0x00],
             [0x40, 0x00, 0x00, 0x00],
             [0x80, 0x00, 0x00, 0x00],
             [0x1b, 0x00, 0x00, 0x00],
             [0x36, 0x00, 0x00, 0x00] ]; 


Aes.Ctr = {};

Aes.Ctr.encrypt = function(plaintext, password, nBits) {
  var blockSize = 16;  // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
  if (!(nBits==128 || nBits==192 || nBits==256)) return '';  // standard allows 128/192/256 bit keys
  plaintext = Utf8.encode(plaintext);
  password = Utf8.encode(password);
  //var t = new Date();  // timer

  // use AES itself to encrypt password to get cipher key (using plain password as source for key
  // expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use)
  var nBytes = nBits/8;  // no bytes in key (16/24/32)
  var pwBytes = new Array(nBytes);
  for (var i=0; i<nBytes; i  ) {  // use 1st 16/24/32 chars of password for key
    pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
  }
  var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes));  // gives us 16-byte key
  key = key.concat(key.slice(0, nBytes-16));  // expand key to 16/24/32 bytes long

  // initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec,
  // [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
  var counterBlock = new Array(blockSize);

  var nonce = (new Date()).getTime();  // timestamp: milliseconds since 1-Jan-1970
  var nonceMs = nonce00;
  var nonceSec = Math.floor(nonce/1000);
  var nonceRnd = Math.floor(Math.random()*0xffff);

  for (var i=0; i<2; i  ) counterBlock[i]   = (nonceMs  >>> i*8) & 0xff;
  for (var i=0; i<2; i  ) counterBlock[i 2] = (nonceRnd >>> i*8) & 0xff;
  for (var i=0; i<4; i  ) counterBlock[i 4] = (nonceSec >>> i*8) & 0xff;

  // and convert it to a string to go on the front of the ciphertext
  var ctrTxt = '';
  for (var i=0; i<8; i  ) ctrTxt  = String.fromCharCode(counterBlock[i]);

  // generate key schedule - an expansion of the key into distinct Key Rounds for each round
  var keySchedule = Aes.keyExpansion(key);

  var blockCount = Math.ceil(plaintext.length/blockSize);
  var ciphertxt = new Array(blockCount);  // ciphertext as array of strings

  for (var b=0; b<blockCount; b  ) {
    // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
    // done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
    for (var c=0; c<4; c  ) counterBlock[15-c] = (b >>> c*8) & 0xff;
    for (var c=0; c<4; c  ) counterBlock[15-c-4] = (b/0x100000000 >>> c*8)

    var cipherCntr = Aes.cipher(counterBlock, keySchedule);  // -- encrypt counter block --

    // block size is reduced on final block
    var blockLength = b<blockCount-1 ? blockSize : (plaintext.length-1)%blockSize 1;
    var cipherChar = new Array(blockLength);

    for (var i=0; i<blockLength; i  ) {  // -- xor plaintext with ciphered counter char-by-char --
      cipherChar[i] = cipherCntr[i] ^ plaintext.charCodeAt(b*blockSize i);
      cipherChar[i] = String.fromCharCode(cipherChar[i]);
    }
    ciphertxt[b] = cipherChar.join('');
  }

  // Array.join is more efficient than repeated string concatenation in IE
  var ciphertext = ctrTxt   ciphertxt.join('');
  ciphertext = Base64.encode(ciphertext);  // encode in base64

  //alert((new Date()) - t);
  return ciphertext;
}

/** 
 * Decrypt a text encrypted by AES in counter mode of operation
 *
 * @param {String} ciphertext Source text to be encrypted
 * @param {String} password   The password to use to generate a key
 * @param {Number} nBits      Number of bits to be used in the key (128, 192, or 256)
 * @returns {String}          Decrypted text
 */
Aes.Ctr.decrypt = function(ciphertext, password, nBits) {
  var blockSize = 16;  // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
  if (!(nBits==128 || nBits==192 || nBits==256)) return '';  // standard allows 128/192/256 bit keys
  ciphertext = Base64.decode(ciphertext);
  password = Utf8.encode(password);
  //var t = new Date();  // timer

  // use AES to encrypt password (mirroring encrypt routine)
  var nBytes = nBits/8;  // no bytes in key
  var pwBytes = new Array(nBytes);
  for (var i=0; i<nBytes; i  ) {
    pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
  }
  var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes));
  key = key.concat(key.slice(0, nBytes-16));  // expand key to 16/24/32 bytes long

  // recover nonce from 1st 8 bytes of ciphertext
  var counterBlock = new Array(8);
  ctrTxt = ciphertext.slice(0, 8);
  for (var i=0; i<8; i  ) counterBlock[i] = ctrTxt.charCodeAt(i);

  // generate key schedule
  var keySchedule = Aes.keyExpansion(key);

  // separate ciphertext into blocks (skipping past initial 8 bytes)
  var nBlocks = Math.ceil((ciphertext.length-8) / blockSize);
  var ct = new Array(nBlocks);
  for (var b=0; b<nBlocks; b  ) ct[b] = ciphertext.slice(8 b*blockSize, 8 b*blockSize blockSize);
  ciphertext = ct;  // ciphertext is now array of block-length strings

  // plaintext will get generated block-by-block into array of block-length strings
  var plaintxt = new Array(ciphertext.length);

  for (var b=0; b<nBlocks; b  ) {
    // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
    for (var c=0; c<4; c  ) counterBlock[15-c] = ((b) >>> c*8) & 0xff;
    for (var c=0; c<4; c  ) counterBlock[15-c-4] = (((b 1)/0x100000000-1) >>> c*8) & 0xff;

    var cipherCntr = Aes.cipher(counterBlock, keySchedule);  // encrypt counter block

    var plaintxtByte = new Array(ciphertext[b].length);
    for (var i=0; i<ciphertext[b].length; i  ) {
      // -- xor plaintxt with ciphered counter byte-by-byte --
      plaintxtByte[i] = cipherCntr[i] ^ ciphertext[b].charCodeAt(i);
      plaintxtByte[i] = String.fromCharCode(plaintxtByte[i]);
    }
    plaintxt[b] = plaintxtByte.join('');
  }

  // join array of blocks into single plaintext string
  var plaintext = plaintxt.join('');
  plaintext = Utf8.decode(plaintext);  // decode from UTF8 back to Unicode multi-byte chars

  //alert((new Date()) - t);
  return plaintext;
}


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Base64 class: Base 64 encoding / decoding (c) Chris Veness 2002-2012                          */
/*    note: depends on Utf8 class                                                                 */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

var Base64 = {};  // Base64 namespace

Base64.code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 /=";

/**
 * Encode string into Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648]
 * (instance method extending String object). As per RFC 4648, no newlines are added.
 *
 * @param {String} str The string to be encoded as base-64
 * @param {Boolean} [utf8encode=false] Flag to indicate whether str is Unicode string to be encoded 
 *   to UTF8 before conversion to base64; otherwise string is assumed to be 8-bit characters
 * @returns {String} Base64-encoded string
 */ 
Base64.encode = function(str, utf8encode) {  // http://tools.ietf.org/html/rfc4648
  utf8encode =  (typeof utf8encode == 'undefined') ? false : utf8encode;
  var o1, o2, o3, bits, h1, h2, h3, h4, e=[], pad = '', c, plain, coded;
  var b64 = Base64.code;

  plain = utf8encode ? str.encodeUTF8() : str;

  c = plain.length % 3;  // pad string to length of multiple of 3
  if (c > 0) { while (c   < 3) { pad  = '='; plain  = ''; } }
  // note: doing padding here saves us doing special-case packing for trailing 1 or 2 chars

  for (c=0; c<plain.length; c =3) {  // pack three octets into four hexets
    o1 = plain.charCodeAt(c);
    o2 = plain.charCodeAt(c 1);
    o3 = plain.charCodeAt(c 2);

    bits = o1<<16 | o2<<8 | o3;

    h1 = bits>>18 & 0x3f;
    h2 = bits>>12 & 0x3f;
    h3 = bits>>6 & 0x3f;
    h4 = bits & 0x3f;

    // use hextets to index into code string
    e[c/3] = b64.charAt(h1)   b64.charAt(h2)   b64.charAt(h3)   b64.charAt(h4);
  }
  coded = e.join('');  // join() is far faster than repeated string concatenation in IE

  // replace 'A's from padded nulls with '='s
  coded = coded.slice(0, coded.length-pad.length)   pad;

  return coded;
}

/**
 * Decode string from Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648]
 * (instance method extending String object). As per RFC 4648, newlines are not catered for.
 *
 * @param {String} str The string to be decoded from base-64
 * @param {Boolean} [utf8decode=false] Flag to indicate whether str is Unicode string to be decoded 
 *   from UTF8 after conversion from base64
 * @returns {String} decoded string
 */ 
Base64.decode = function(str, utf8decode) {
  utf8decode =  (typeof utf8decode == 'undefined') ? false : utf8decode;
  var o1, o2, o3, h1, h2, h3, h4, bits, d=[], plain, coded;
  var b64 = Base64.code;

  coded = utf8decode ? str.decodeUTF8() : str;


  for (var c=0; c<coded.length; c =4) {  // unpack four hexets into three octets
    h1 = b64.indexOf(coded.charAt(c));
    h2 = b64.indexOf(coded.charAt(c 1));
    h3 = b64.indexOf(coded.charAt(c 2));
    h4 = b64.indexOf(coded.charAt(c 3));

    bits = h1<<18 | h2<<12 | h3<<6 | h4;

    o1 = bits>>>16 & 0xff;
    o2 = bits>>>8 & 0xff;
    o3 = bits & 0xff;

    d[c/4] = String.fromCharCode(o1, o2, o3);
    // check for padding
    if (h4 == 0x40) d[c/4] = String.fromCharCode(o1, o2);
    if (h3 == 0x40) d[c/4] = String.fromCharCode(o1);
  }
  plain = d.join('');  // join() is far faster than repeated string concatenation in IE

  return utf8decode ? plain.decodeUTF8() : plain; 
}


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Utf8 class: encode / decode between multi-byte Unicode characters and UTF-8 multiple          */
/*              single-byte character encoding (c) Chris Veness 2002-2012                         */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

var Utf8 = {};  // Utf8 namespace

/**
 * Encode multi-byte Unicode string into utf-8 multiple single-byte characters 
 * (BMP / basic multilingual plane only)
 *
 * Chars in range U 0080 - U 07FF are encoded in 2 chars, U 0800 - U FFFF in 3 chars
 *
 * @param {String} strUni Unicode string to be encoded as UTF-8
 * @returns {String} encoded string
 */
Utf8.encode = function(strUni) {
  // use regular expressions & String.replace callback function for better efficiency 
  // than procedural approaches
  var strUtf = strUni.replace(
      /[u0080-u07ff]/g,  // U 0080 - U 07FF => 2 bytes 110yyyyy, 10zzzzzz
      function(c) { 
        var cc = c.charCodeAt(0);
        return String.fromCharCode(0xc0 | cc>>6, 0x80 | cc&0x3f); }
    );
  strUtf = strUtf.replace(
      /[u0800-uffff]/g,  // U 0800 - U FFFF => 3 bytes 1110xxxx, 10yyyyyy, 10zzzzzz
      function(c) { 
        var cc = c.charCodeAt(0); 
        return String.fromCharCode(0xe0 | cc>>12, 0x80 | cc>>6&0x3F, 0x80 | cc&0x3f); }
    );
  return strUtf;
}

/**
 * Decode utf-8 encoded string back into multi-byte Unicode characters
 *
 * @param {String} strUtf UTF-8 string to be decoded back to Unicode
 * @returns {String} decoded string
 */
Utf8.decode = function(strUtf) {
  // note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char!
  var strUni = strUtf.replace(
      /[u00e0-u00ef][u0080-u00bf][u0080-u00bf]/g,  // 3-byte chars
      function(c) {  // (note parentheses for precence)
        var cc = ((c.charCodeAt(0)&0x0f)<<12) | ((c.charCodeAt(1)&0x3f)<<6) | ( c.charCodeAt(2)&0x3f); 
        return String.fromCharCode(cc); }
    );
  strUni = strUni.replace(
      /[u00c0-u00df][u0080-u00bf]/g,                 // 2-byte chars
      function(c) {  // (note parentheses for precence)
        var cc = (c.charCodeAt(0)&0x1f)<<6 | c.charCodeAt(1)&0x3f;
        return String.fromCharCode(cc); }
    );
  return strUni;
}

四、参考

加密算法(原版):https://www.cnblogs.com/pengyingh/articles/4829871.html

https://bbs.pediy.com/thread-214012.htm

https://www.jianshu.com/p/22b56d977825

0 人点赞