Skip to content

Instantly share code, notes, and snippets.

@PhoenixIllusion
Last active January 10, 2019 17:30
Show Gist options
  • Save PhoenixIllusion/1f273e327f21a92ec49bd2ccff2c42bf to your computer and use it in GitHub Desktop.
Save PhoenixIllusion/1f273e327f21a92ec49bd2ccff2c42bf to your computer and use it in GitHub Desktop.
This is a stand-alone HTML page with Javascript and no external script files for parsing a KeyStore, displaying the information about the internal public cert, and allowing checking of passwords agains the Store Password (file-hash check), and validating the Private Key password for the 1st alias using the JKS SHA1-chain technique.
<html>
<head>
<style>
#key_password, #password {
width: 300px;
margin-right: 5px;
}
#key_valid, #valid {
height: 30px;
line-height: 30px;
font-weight: bold;
padding-left: 10px;
padding-right: 10px;
}
#key_valid.valid, #valid.valid {
background-color: green
}
#key_valid.invalid, #valid.invalid {
background-color: pink
}
#keystore {
width: 300px;
height: 200px;
margin: 5px auto;
border: 1px solid black;
background: #CCC;
font-family: 'Courier New', Courier, monospace;
padding: 20px;
text-align: center;
}
#keystore.valid {
background: lightgreen;
}
#keystore.invalid {
background-color: pink
}
pre {
margin: 2px;
}
</style>
<script>
(function() {
"use strict";
const ENCRYPTION_LOOKUP = {
"1.2.840.113549.1.1.1": "RSA encryption",
"1.2.840.113549.1.1.2": "MD2 with RSA encryption",
"1.2.840.113549.1.1.4": "MD5 with RSA encryption",
"1.2.840.113549.1.1.5": "SHA-1 with RSA Encryption",
"1.2.840.113549.1.1.6": "rsaOAEPEncryptionSET",
"1.2.840.113549.1.1.7": "RSAES-OAEP",
"1.2.840.113549.1.1.10": "RSASSA-PSS",
"1.2.840.113549.1.1.11": "sha256WithRSAEncryption"
}
const OID_LOOKUP = {
"2.5.4.0": "objectClass",
"2.5.4.1": "aliasedEntryName",
"2.5.4.2": "knowldgeinformation",
"2.5.4.3": "commonName",
"2.5.4.4": "surname",
"2.5.4.5": "serialNumber",
"2.5.4.6": "countryName",
"2.5.4.7": "localityName",
"2.5.4.8": "stateOrProvinceName",
"2.5.4.9": "streetAddress",
"2.5.4.10": "organizationName",
"2.5.4.11": "organizationalUnitName",
"2.5.4.12": "title",
"2.5.4.13": "description",
"2.5.4.14": "searchGuide",
"2.5.4.15": "businessCategory",
"2.5.4.16": "postalAddress",
"2.5.4.17": "postalCode",
"2.5.4.18": "postOfficeBox",
"2.5.4.19": "physicalDeliveryOfficeName",
"2.5.4.20": "telephoneNumber",
"2.5.4.21": "telexNumber",
"2.5.4.22": "teletexTerminalIdentifier",
"2.5.4.23": "facsimileTelephoneNumber",
"2.5.4.24": "x121Address",
"2.5.4.25": "internationalISDNNumber",
"2.5.4.26": "registeredAddress",
"2.5.4.27": "destinationIndicator",
"2.5.4.28": "preferredDeliveryMethod",
"2.5.4.29": "presentationAddress",
"2.5.4.30": "supportedApplicationContext",
"2.5.4.31": "member",
"2.5.4.32": "owner",
"2.5.4.33": "roleOccupant",
"2.5.4.34": "seeAlso",
"2.5.4.35": "userPassword",
"2.5.4.36": "userCertificate",
"2.5.4.37": "cACertificate",
"2.5.4.38": "authorityRevocationList",
"2.5.4.39": "certificateRevocationList",
"2.5.4.40": "crossCertificatePair",
"2.5.4.41": "name",
"2.5.4.42": "givenName",
"2.5.4.43": "initials",
"2.5.4.44": "generationQualifier",
"2.5.4.45": "uniqueIdentifier",
"2.5.4.46": "dnQualifier",
"2.5.4.47": "enhancedSearchGuide",
"2.5.4.48": "protocolInformation",
"2.5.4.49": "distinguishedName",
"2.5.4.50": "uniqueMember",
"2.5.4.51": "houseIdentifier",
"2.5.4.52": "supportedAlgorithms",
"2.5.4.53": "deltaRevocationList",
"2.5.4.65": "pseudonym"
}
const PRIVATE_KEY = 1;
const TRUSTED_CERT = 2;
const MAGIC = 0xFEEDFEED;
const SALT = "Mighty Aphrodite";
const decoder = new TextDecoder();
const DER_SEQUENCE = 0x30;
const DER_SET = 0x31;
const DER_CONTEXT_SPEC_0 = 0xA0;
const DER_PRINT_STRING = 0x13;
const DER_UTF8_STRING = 0x0c;
const DER_IA5_STRING = 0x16;
const DER_OBJECT_ID = 0x06;
const DER_INTEGER = 0x02;
const DER_UTC_TIME= 0x17;
const DER_GENERAL_TIME = 0x18;
function derTagLabel(tag) {
switch(tag) {
case 0x03:
return "bit-string";
case 0x01:
return "boolean";
case 0x02:
return "integer";
case 0x05:
return "null";
case 0x06:
return "object-id";
case 0x04:
return "oct-string";
case 0x1e:
return "unicode-string";
case 0x16:
return "ia5-string";
case 0x17:
return "utc-time";
case 0x18:
return "general-time";
case 0x13:
return "printable-string";
case 0x0c:
return "utf8-string";
case 0x30:
return "sequence";
case 0x31:
return "set";
case 0xA0:
return "context-spec-0";
default:
return "unknown";
}
}
function derTag(buffer, index) {
function getBitString() {
let resp = readBitString(buffer, index);
index = resp.index;
return resp.value;
}
let response = {}
response.tag = buffer.getUint8(index);index++;
response.tagV = derTagLabel(response.tag);
response.len = getBitString()
response.data = new DataView(buffer.buffer.slice(index, index+response.len));
switch(response.tag) {
case DER_IA5_STRING:
case DER_UTF8_STRING:
case DER_PRINT_STRING:
response.value = decoder.decode(response.data);
break;
case DER_UTC_TIME:
case DER_GENERAL_TIME:
var str = decoder.decode(response.data);
if(str.length == 13) {
str = "20"+str;
}
response.value = str.substr(0,4)+"-"+str.substr(4,2)+"-"+str.substr(6,2)+"T"+str.substr(8,2)+":"+str.substr(10,2)+":"+str.substr(12);
response.value2 = new Date(response.value);
break;
case DER_OBJECT_ID:
var val = [];
var v = response.data.getUint8(0);
val.push(""+(Math.floor(v/40))+"."+(v % 40));
for(var i=1;i<response.data.byteLength;){
let resp = readVBitInt(response.data, i);
val.push(resp.value)
i = resp.index
}
response.value = val.join(".")
break;
}
index+=response.len;
return {
value: response,
index
}
}
function parseDER(der) {
var index = 0;
var resp = {};
resp = [];
while(index < der.byteLength) {
let entry = derTag(der, index);
index = entry.index;
let value = entry.value;
switch(value.tag) {
case DER_SEQUENCE:
case DER_SET:
case DER_CONTEXT_SPEC_0:
value.contents = parseDER(value.data);
}
resp.push(value);
}
return resp;
}
function readVBitInt(dataView, index) {
var start = index;
while(dataView.getUint8(index)>0x80){
index++;
}
index++;
var value=0;
for(var i=start;i<index;i++) {
value = value<<7 | (dataView.getUint8(i)&0x7F)
}
return {
value,
index
}
}
function readBitString(dataView, index) {
var len = dataView.getUint8(index);index++;
if(len >= 0x80){
let count = len-0x80;
len = 0;
for(var i=0;i<count;i++){
len = (len<<8)|dataView.getUint8(index);
index++;
}
}
return {
value: len,
index
}
}
function readUTF(dataView, index) {
let len = dataView.getUint16(index);index+=2;
return {
value: decoder.decode(new Uint8Array(dataView.buffer,index,len)),
index: index+len
}
}
function parseTBSName(seq) {
if(seq.filter(x => x.tag != DER_SET).length > 0) {
throw new Error("Cert TBSCert Name includes non-set items")
}
var el;
el = seq.filter(x => x.contents.length == 1 && x.contents[0].contents.length != 2);
if(el.length > 0) {
throw new Error(`Cert TBSCert Name sets not of 2 length, found length ${el[0].contents[0].contents.length}`)
}
el = seq.filter(x => x.contents[0].contents[0].tag != DER_OBJECT_ID)
if(el.length > 0) {
throw new Error(`Cert TBSCert Name sets not starting with ObjID, found ${el[0].contents[0].contents[0].tag}`)
}
el = seq.filter(x => x.contents[0].contents[1].tag != DER_PRINT_STRING)
if(el.length > 0) {
throw new Error(`Cert TBSCert Name sets not ending with PrintString, found ${el[0].contents[0].contents[1].tag}`)
}
return seq.map( x => {
let name = x.contents[0].contents[0].value;
let value = x.contents[0].contents[1].value;
return {
name: OID_LOOKUP[name]||name,
value
}
});
}
function parseKeyInfo(seq) {
let algorithmId = seq.contents[0].value
return ENCRYPTION_LOOKUP[algorithmId]||algorithmId;
}
function parseTBSCertificate(seq) {
var resp = {};
if(!seq.contents || seq.contents.length < 7) {
throw new Error(`Cert TBSCert is not long enough. Length ${seq.contents.length} < 7`)
}
var contents = seq.contents;
if(contents[0].tag != DER_CONTEXT_SPEC_0 || !contents[0].contents || contents[0].contents[0].tag != DER_INTEGER) {
throw new Error("Cert TBSCert did not have C[0] Integer version")
}
resp.version = contents[0].contents[0].data.getUint8(0)+1;
if(contents[1].tag != DER_INTEGER) {
throw new Error("Cert TBSCert serial was not integer type")
}
resp.serial = contents[1].data.getUint32(0).toString(16);
resp.algorithmId = parseKeyInfo(contents[2]);
resp.issuer = parseTBSName(contents[3].contents)
resp.valid = {
notBefore: contents[4].contents[0].value2,
notAfter: contents[4].contents[1].value2
}
resp.subject = parseTBSName(contents[5].contents)
resp.subjectPublicKeyInfo = {
algorithm: parseKeyInfo(contents[6].contents[0]),
bits: contents[6].contents[1].data
}
return resp;
}
async function calculateCertHashes(data, type) {
var hash = new Uint8Array(await window.crypto.subtle.digest(type, new Uint8Array(data.buffer)))
var ret=[];
new Uint8Array(hash.buffer).forEach(x => ret.push(x.toString(16)));
return ret.join(":").toUpperCase();
}
async function parseX509DER(data, der) {
let resp = {}
resp.hashes = {
"SHA1": await calculateCertHashes(data, "SHA-1"),
"SHA256": await calculateCertHashes(data, "SHA-256")
}
if(der.length != 1 && !der[0].contents) {
throw new Error("Cert does not have sequence at root")
}
var root = der[0].contents;
if(root.length != 3) {
throw new Error(`Cert is not of length 3, found ${root.length} elements`)
}
resp.certificate = parseTBSCertificate(root[0]);
resp.algorithm = parseKeyInfo(root[1]);
resp.signature = root[2].data
return resp;
}
async function readCert(dataView, index) {
function readInt() {
let resp = dataView.getUint32(index);index += 4;
return resp;
}
function readStr() {
let str = readUTF(dataView, index);
index = str.index;
return str.value;
}
let entry = {};
entry.type = readStr();
entry.len = readInt();
entry.data = new DataView(dataView.buffer.slice(index, index+entry.len));
index += entry.len;
if(entry.type = "X.509"){
entry.value = await parseX509DER(entry.data, parseDER(entry.data));
}
return {
value: entry,
index
}
}
async function readAlias(dataView, index) {
function readInt() {
let resp = dataView.getUint32(index);index += 4;
return resp;
}
function readStr() {
let str = readUTF(dataView, index);
index = str.index;
return str.value;
}
async function readCertificate() {
let cert = await readCert(dataView, index);
index = cert.index;
return cert.value;
}
let entry = {};
entry.type = readInt();
entry.alias = readStr();
entry.date = new Date(dataView.getUint32(index)*0x100000000+dataView.getUint32(index+4));index+=8;
entry.trustedCerts = [];
entry.privateKeys = [];
entry.certChains = [];
switch (entry.type) {
case PRIVATE_KEY:
let privateKey = {};
privateKey.len = readInt();
privateKey.data = new Uint8Array(dataView.buffer.slice(index, index+privateKey.len));
index += privateKey.len;
entry.privateKeys.push(privateKey);
let certChainCount = readInt();
for(var j=0;j<certChainCount;j++) {
entry.certChains.push(await readCertificate())
}
break;
case TRUSTED_CERT:
entry.trustedCerts.push(await readCertificate());
break;
}
return {
value: entry,
index
}
}
async function parseKeystore(keystore) {
var index = 0;
let dataView = new DataView(keystore.buffer);
function readInt() {
let resp = dataView.getUint32(index);index += 4;
return resp;
}
function readUTF() {
let str = readUTF(dataView, index);
index = str.offset;
return str.value;
}
let response = {};
let magic = readInt();
if(magic != MAGIC) {
throw new Error(`Keystore MagicNumber invalid, found 0x${dataView.getInt32(0).toString(16)}`)
}
response.version = readInt();
let aliasCount = readInt();
response.alias = [];
for(var i=0;i<aliasCount;i++) {
let alias = await readAlias(dataView, index);
response.alias.push(alias.value);
index = alias.index;
}
return {
value: response,
index
}
}
function charsToBytes(passwd) {
var buf = [];
for (var i = 0, j = 0; i < passwd.length; i++) {
let char = passwd.charCodeAt(i);
buf[j++] = (char >> 8) & 0xFF;
buf[j++] = char & 0xFF;
}
return buf;
}
window.checkKeystorePassword = async function (password, keystore) {
try {
let encoder = new TextEncoder();
let passwd = new Uint8Array(charsToBytes(password))
let salt = encoder.encode(SALT);
let hash = await keystore.slice(keystore.length-20);
let pack = new Uint8Array(passwd.length+salt.length+keystore.length-20);
pack.set(passwd,0)
pack.set(salt, passwd.length)
pack.set(keystore.slice(0,keystore.length-20), passwd.length+salt.length)
let digest = new Uint8Array(await window.crypto.subtle.digest('SHA-1', pack));
let isValid = true;
for(var i=0;i<digest.length;i++) {
isValid &= digest[i]==hash[i];
}
console.log(`Keystore Valid: ${isValid}`);
return isValid?"valid":"invalid";
} catch(err) {
return err.message
}
}
window.checkPrivateKeyPassword = async function (password, keystore) {
try {
let encoder = new TextEncoder();
let passwd = new Uint8Array(charsToBytes(password))
let keystoreData = (await parseKeystore(keystore)).value;
let privateDER = parseDER(new DataView(keystoreData.alias[0].privateKeys[0].data.buffer))[0];
if(!privateDER.contents || privateDER.contents.length != 2) {
throw new Error("JKS Private Key not DER of length 2")
}
let privateK = privateDER.contents[1].data
let salt = new Uint8Array(privateK.buffer, 0, 20);
let encrypted = new Uint8Array(privateK.buffer, 20, privateK.byteLength - 40);
let hash = new Uint8Array(privateK.buffer, privateK.byteLength - 20, 20);
let xorMap = new Uint8Array(Math.ceil(encrypted.byteLength/20)*20);
let buffer = new Uint8Array(salt.byteLength + passwd.byteLength)
buffer.set(passwd);
buffer.set(salt, passwd.byteLength);
for(var i=0;i<Math.ceil(encrypted.byteLength/20);i++) {
let sha1 = new Uint8Array(await window.crypto.subtle.digest('SHA-1', buffer));
xorMap.set(sha1, i*20)
buffer.set(sha1, passwd.byteLength);
}
let decrypted = encrypted.map( (x,i) => x^xorMap[i]);
let passwordCheckBuffer = new Uint8Array(decrypted.byteLength+passwd.byteLength);
passwordCheckBuffer.set(passwd, 0);
passwordCheckBuffer.set(decrypted, passwd.byteLength);
let digest = new Uint8Array(await window.crypto.subtle.digest('SHA-1', passwordCheckBuffer))
let isValid = true;
for(var i=0;i<digest.length;i++) {
isValid &= digest[i]==hash[i];
}
console.log(`Keystore Valid: ${isValid}`);
return isValid?"valid":"invalid";
} catch(err) {
return err.message
}
}
function createLiEle(text) {
let li = document.createElement("li");
let p = document.createElement("pre");
p.innerText = text;
li.appendChild(p);
return li;
}
function renderUser(title, entry) {
let resp = createLiEle(title);
let ul = document.createElement("ul");
entry.forEach( x => {
ul.appendChild(createLiEle(`${x.name} - ${x.value}`))
})
resp.appendChild(ul);
return resp;
}
function renderAlias(alias) {
let entry = document.createElement("li");
entry.appendChild(createLiEle(alias.alias))
let certChain = alias.certChains[0].value;
if(certChain) {
let ul = document.createElement("ul");
ul.appendChild(createLiEle(`Created: ${alias.date}`))
ul.appendChild(createLiEle(`Version: ${certChain.certificate.version}`))
ul.appendChild(createLiEle(`Algorithm: ${certChain.algorithm}`))
ul.appendChild(createLiEle(`Hash SHA1: ${certChain.hashes.SHA1}`))
ul.appendChild(createLiEle(`Hash SHA256: ${certChain.hashes.SHA256}`))
ul.appendChild(createLiEle(`Serial: ${certChain.certificate.serial}`))
ul.appendChild(renderUser("Issuer", certChain.certificate.issuer))
ul.appendChild(renderUser("Subject", certChain.certificate.subject))
ul.appendChild(createLiEle(`Validity - ${certChain.certificate.valid.notBefore} to ${certChain.certificate.valid.notAfter}`))
entry.appendChild(ul);
}
return entry;
}
window.parseKeyStore = async function(keystore) {
let keystoreData = (await parseKeystore(keystore)).value;
var aliasList = document.createElement("ul");
keystoreData.alias.forEach( alias => {
aliasList.appendChild(renderAlias(alias));
})
return aliasList;
}
})()
</script>
</head>
<body>
<h4>This page will validate the Store and Key Password of an Android KeyStore file (JKS keyfile)</h4>
<p>Enter a password and hit enter or press the "Check" button to validate the Store Password</p>
<hr />
Store Password: <input type="text" id="password" /><input id="button" type="button" value="Check" /><br />
Key Password: <input type="text" id="key_password" /><input id="key_button" type="button" value="Check" />
<hr />
Store Valid: <div id="valid">Unknown</div><br />
Key Valid: <div id="key_valid">Unknown</div><br />
<hr />
Keystore File <br />
<div id="keystore">
Drop Keystore Here
</div>
<div id="keystoreData"></div>
<script>
function configureFileDragDrop(ele, regex, func) {
ele.ondragover = function(ev){ev.preventDefault()}
ele.drop_funcs = ele.drop_funcs ||[];
ele.drop_funcs.push({regex:new RegExp(regex),func:func});
ele.ondrop = function(ev) {
ev.preventDefault();
var data = ev.dataTransfer;
if(data.files && data.files.length > 0) {
var files = [];
ele.drop_funcs.forEach(function(o){
o.valid = true
});
for(var i =0; i<data.files.length;i++) {
files[i]=data.files[i];
ele.drop_funcs.forEach(function(o){
o.valid &= o.regex.exec(files[i].name)&&1
});
}
ele.drop_funcs.forEach(function(o){
if(o.valid){
o.func(files);
}
});
}
}
}
function readBlobAsArrayBuffer(blob) {
return new Promise( resolve => {
var fileReader = new FileReader();
fileReader.onload = function(event) {
resolve(event.target.result);
};
fileReader.readAsArrayBuffer(blob);
});
}
</script>
<script>
var keystoreArr = undefined;
let input = document.getElementById("password");
let button = document.getElementById("button");
let key_password = document.getElementById("key_password");
let key_button = document.getElementById("key_button");
let valid = document.getElementById("valid");
let key_valid = document.getElementById("key_valid");
let keystoreEle = document.getElementById("keystore");
let keystoreData = document.getElementById("keystoreData");
input.onkeypress = (e) => {
if (e.keyCode == 13) {
button.onclick()
return false;
}
}
button.onclick = async () => {
let isValid = await checkKeystorePassword(input.value.trim(), keystoreArr);
valid.className = isValid;
valid.innerHTML = `<pre>${isValid} - [${input.value.trim()}]</pre>`;
}
key_password.onkeypress = (e) => {
if (e.keyCode == 13) {
key_button.onclick()
return false;
}
}
key_button.onclick = async () => {
let isValid = await checkPrivateKeyPassword(key_password.value.trim(), keystoreArr);
key_valid.className = isValid;
key_valid.innerHTML = `<pre>${isValid} - [${key_password.value.trim()}]</pre>`;
}
configureFileDragDrop(keystoreEle, ".+\.keystore", async (file) =>{
keystoreArr = new Uint8Array(await readBlobAsArrayBuffer(file[0]));
try {
let data = await parseKeyStore(keystoreArr)
keystoreEle.innerHTML = `Drop Keystore Here<br />File: ${file[0].name}`
keystoreEle.className = "valid"
keystoreData.innerHTML = "";
keystoreData.appendChild(data);
} catch (err) {
keystoreEle.innerHTML = `Error: ${err.message}`
keystoreEle.className = "invalid"
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment