GTBank APP SecurityTest
前言
国庆放假期间领导给了一个任务,分析非洲某银行APP,绕过反抓包,并且分析加密算法,能实现自动登录,这也是我第一次分析APP(以前从未接触,也只是看大佬们的文章),所以记录一下
一、反编译APK获得源码
1、需要的工具
- Apktool
下载地址:http://ibotpeaches.github.io/Apktool/install/
- dex2jar
- jd-gui
2、反编译代码
先把.apk
后缀修改为.zip
后解压,再使用dex2jar工具
d2j-dex2jar classes.dex
该jar包可以用jd-gui打开
2、反编译资源
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
代码语言:javascript复制其实是在JustTrustme里面抄的
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中,打开控制台调用对应的函数即可
代码语言:javascript复制之前实战分析过steam登录的rsa加密算法,所以在此次分析中,直接照搬上次的方法,调用即可
<!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 = '