Last active
January 10, 2019 17:30
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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