diff options
author | Thomas Van Iseghem <[email protected]> | 2023-04-23 22:27:31 +0200 |
---|---|---|
committer | Thomas Van Iseghem <[email protected]> | 2023-04-23 22:27:31 +0200 |
commit | 9b4ec4262272f14fd69f87dd5c9f7b86a7a1e479 (patch) | |
tree | a3aa7a4b775be9e8de611f67ee172fea92e184d6 /File-decryption | |
parent | 5774a79a26e2a1f97f5c1f3a407bffb5b887df5e (diff) | |
download | OpenCortex-9b4ec4262272f14fd69f87dd5c9f7b86a7a1e479.tar.gz OpenCortex-9b4ec4262272f14fd69f87dd5c9f7b86a7a1e479.zip |
Refactored / decrypt toggle / download button / live encoding
Diffstat (limited to 'File-decryption')
-rw-r--r-- | File-decryption/webapp/assets/css/style.css | 273 | ||||
-rw-r--r-- | File-decryption/webapp/assets/js/file-processing.js | 104 | ||||
-rw-r--r-- | File-decryption/webapp/assets/js/json-viewer.js | 97 | ||||
-rw-r--r-- | File-decryption/webapp/assets/js/main.js | 218 | ||||
-rw-r--r-- | File-decryption/webapp/index.html | 68 |
5 files changed, 544 insertions, 216 deletions
diff --git a/File-decryption/webapp/assets/css/style.css b/File-decryption/webapp/assets/css/style.css index 64fa213..ca14480 100644 --- a/File-decryption/webapp/assets/css/style.css +++ b/File-decryption/webapp/assets/css/style.css @@ -1,3 +1,9 @@ +html{ + scroll-behavior: smooth; + height: 100vh; + overflow-y: hidden; +} + * { box-sizing: border-box; margin: 0; @@ -12,6 +18,13 @@ body { line-height: 1.5; } +hr { + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.09); + margin: 2rem 0; + width: 100%; +} + header { padding: 1rem; display: flex; @@ -32,6 +45,27 @@ input{ text-align: center; } +button { + font-family: 'IBM Plex sans', sans-serif; + color: white; + border: none; + border-radius: 20px; + font-size: 1rem; +} + +select { + padding: 0.5rem; + border-radius: 20px; + border: 3px solid rgb(53, 107, 62, 0); + outline-style: none; + + background-color: rgb(18, 18, 18); + color: rgb(185, 185, 185); + font-size: 1rem; + margin: 0.5rem; + text-align: center; +} + input:focus-visible{ border: 3px solid rgb(53, 107, 62); } @@ -46,73 +80,264 @@ header .header-title{ } .content { - max-width: 800px; + width: 100%; + height: calc(100vh - 96px); + overflow-y: scroll; + overflow-x: hidden; margin: 0 auto; - padding: 20px; + padding: 2rem; +} +.content-container { /* Vertical align in page */ display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; + max-width: 800px; + margin: 0 auto; +} - /* Center in page */ +.serial-input p { + color: rgb(185, 185, 185); + margin-bottom: 1rem; +} + +.enable-decryption { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; +} + +label.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; + margin: 0 1rem; +} + +label.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; } -.serial-input{ - margin-bottom: 3rem; +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; } -.serial-input p{ - color: rgb(185, 185, 185); +input:checked + .slider { + background-color: #2196F3; } -.serial-input p { - margin-bottom: 1rem; +input:focus + .slider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; } -.file-upload { +.slider.round:before { + border-radius: 50%; +} + +.file-btns { width: 100%; } -.file-upload .file-input-title { +.btn-cta { display: flex; - + border-radius: 1rem; align-items: center; justify-content: center; - background-color: rgb(18, 18, 18); + background-color: rgb(63, 13, 242); height: 3rem; width: 100%; + max-width: 200px; + margin: auto; } -.file-upload .file-input { - display: none; +.btn-cta:hover { + background-color: rgb(98, 13, 255); + cursor: pointer; } -.file-upload .file-upload-title{ +.file-btns .file-upload { margin-bottom: 1rem; } -.file-upload .file-input-label { +.file-btns .file-download { + background-color: #45f862; + color: #232323; +} + +.file-btns .file-download:hover { + background-color: #37c64f; + color: #232323; +} + +.file-btns .file-input-title { display: flex; - border-radius: 1rem; + align-items: center; justify-content: center; - background-color: rgb(63, 13, 242); + background-color: rgb(18, 18, 18); height: 3rem; width: 100%; } -.file-upload .file-input-label:hover{ +.file-btns-input { + display: none !important; +} + +.btn-cta.file-download .fa-solid { + margin-right: 0.5rem; +} + +.file-btns .file-upload-input { + display: none; +} + +.file-btns .section-title{ + margin-bottom: 1rem; +} + +.file-btns .file-input-label:hover{ background-color: rgb(98, 13, 255); cursor: pointer; } -.file-upload .file-input-label .fa-cloud-arrow-up{ +.file-btns .file-input-label .fa-solid{ margin-right: 0.5rem; +} + +.file-decode { + width: 100%; + margin-bottom: 3rem; +} + +.file-decode .section-title { + align-items: center; + justify-content: center; + display: flex; + margin-bottom: 1rem; +} + +.file-decode #protobuf-list { + margin-bottom: 2rem; +} + +.file-decode #decrypt-output { + width: 100%; + min-height: 20rem; +} + +/* Based on JSON viewer by: https://codepen.io/WartClaes/pen/QGGPJL */ + +@import url("https://fonts.googleapis.com/css?family=Source+Code+Pro"); +.json { + font-family: "Source Code Pro", monospace; + font-size: 16px; + background-color: rgb(50, 50, 50); + text-align: start; + padding: 1rem 2rem 1.5rem 2rem; + border-radius: 10px; +} +.json > .json__item { + display: block; +} + +.json__item { + display: none; + margin-top: 10px; + padding-left: 20px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.json__item--collapsible { + cursor: pointer; + overflow: hidden; + position: relative; +} +.json__item--collapsible::before { + content: "+"; + position: absolute; + left: 5px; +} +.json__item--collapsible::after { + background-color: lightgrey; + content: ""; + height: 100%; + left: 9px; + position: absolute; + top: 26px; + width: 1px; +} +.json__item--collapsible:hover > .json__key, .json__item--collapsible:hover > .json__value { + text-decoration: underline; +} + +.json__toggle { + display: none; +} +.json__toggle:checked ~ .json__item { + display: block; +} + +.json__key { + color: rgb(123, 123, 227); + display: inline; +} +.json__key::after { + content: ": "; +} + +.json__value { + display: inline; +} + +.json__value--string { + color: green; +} + +.json__value--number { + color: rgb(90, 90, 194); +} + +.json__value--boolean { + color: red; }
\ No newline at end of file diff --git a/File-decryption/webapp/assets/js/file-processing.js b/File-decryption/webapp/assets/js/file-processing.js new file mode 100644 index 0000000..d642a92 --- /dev/null +++ b/File-decryption/webapp/assets/js/file-processing.js @@ -0,0 +1,104 @@ +// as seen in /usr/lib/libzc.so / SetupKeys +const MASTER_KEY = new Uint8Array([ + 0x13, 0x27, 0x3f, 0x42, + 0xa5, 0xb6, 0x79, 0xe8, + 0x20, 0x31, 0xc4, 0xf5, + 0x16, 0x17, 0x88, 0x2f, + 0x43, 0xa4, 0x55, 0x69, + 0x77, 0xb8, 0xe2, 0x83, + 0x04, 0x05, 0x60, 0x70, + 0x80, 0x02, 0x03, 0x04, + 0x50, 0x6a, 0x7c, 0x8a, + 0x02, 0x30, 0x40, 0x51, + 0x6a, 0x7d, 0x8d, 0x22, + 0x33, 0x44, 0x59, 0x66, + 0x71, 0x08, 0x02, 0x03, + 0x43, 0x05, 0x67, 0x7a, + 0x8f]); + +function hex(byteArray) { + return Array.prototype.map.call(byteArray, function (byte) { + return ('0' + (byte & 0xFF).toString(16)).slice(-2); + }).join(''); +} + +// ported from EVP_BytesToKey +// https://github.com/openssl/openssl/blob/c04e78f0c69201226430fed14c291c281da47f2d/crypto/evp/evp_key.c#L78 +function deriveKeyAndIV(password, iterations = 10) { + var keyLen = 16; + var ivLen = 16; + var addmd = 0; + + var key = new Uint8Array(keyLen); + var iv = new Uint8Array(ivLen); + var tmp = new Uint8Array(); + var i, key_i = 0, iv_i = 0; + + for (; keyLen > 0 || ivLen > 0;) { + if (addmd++) { + block = new Uint8Array([...tmp, ...password]); + } else { + block = password; + } + + tmp = new Uint8Array(sha1.array(block)); + for (i = 1; i < iterations; i++) { + tmp = sha1.array(tmp); + } + + i = 0; + while (keyLen && i != tmp.length) { + key[key_i++] = tmp[i]; + keyLen--; + i++; + } + + while (ivLen != 0 && i != tmp.length) { + iv[iv_i++] = tmp[i]; + ivLen--; + i++; + } + } + + return { key: key, iv: iv }; +} + +function processFileInput(e) { + const fileName = e.target.fileName; + + const serial = document.getElementById('serial-input').value; + let main_key = null; + if (serial.length > 0) { + // local decryption, use master key + serial + main_key = new Uint8Array([...MASTER_KEY, ...new TextEncoder("utf-8").encode(serial)]); + } else { + // global decryption, use master key only + main_key = MASTER_KEY; + } + + // derive key and iv with our EVP_BytesToKey port + const derived = deriveKeyAndIV(main_key); + // encrypted file contents + const ciphertext = e.target.result; + // import the raw key + window.crypto.subtle.importKey("raw", derived.key, "AES-CTR", true, ["encrypt", "decrypt"]).then(function (key) { + // decrypt using aes-128-ctr + window.crypto.subtle.decrypt({ name: "AES-CTR", counter: derived.iv, length: 128 }, key, ciphertext).then(function (cleartext) { + var blob = new Blob([cleartext], { type: "application/octet-stream" }); + decryptedBlobUrl = window.URL.createObjectURL(blob); + currentClearText = cleartext; + }); + }); +} + +function processFileDecode(clearText) { + const protobufType = document.getElementById('protobuf-list').value; + + if(protobufType != ""){ + protoTypes.forEach((type)=>{ + if(type.name === protobufType){ + decodedProtobuf = type.decode(new Uint8Array(clearText)) + } + }) + } +}
\ No newline at end of file diff --git a/File-decryption/webapp/assets/js/json-viewer.js b/File-decryption/webapp/assets/js/json-viewer.js new file mode 100644 index 0000000..9a65f49 --- /dev/null +++ b/File-decryption/webapp/assets/js/json-viewer.js @@ -0,0 +1,97 @@ +// Based on the JSON viewer by https://codepen.io/WartClaes/pen/QGGPJL + +function jsonViewer(json, collapsible=false) { + var TEMPLATES = { + item: '<div class="json__item"><div class="json__key">%KEY%</div><div class="json__value json__value--%TYPE%">%VALUE%</div></div>', + itemCollapsible: '<label class="json__item json__item--collapsible"><input type="checkbox" class="json__toggle"/><div class="json__key">%KEY%</div><div class="json__value json__value--type-%TYPE%">%VALUE%</div>%CHILDREN%</label>', + itemCollapsibleOpen: '<label class="json__item json__item--collapsible"><input type="checkbox" checked class="json__toggle"/><div class="json__key">%KEY%</div><div class="json__value json__value--type-%TYPE%">%VALUE%</div>%CHILDREN%</label>' + }; + + function createItem(key, value, type){ + var element = TEMPLATES.item.replace('%KEY%', key); + + if(type == 'string') { + element = element.replace('%VALUE%', '"' + value + '"'); + } else { + element = element.replace('%VALUE%', value); + } + + element = element.replace('%TYPE%', type); + + return element; + } + + function createCollapsibleItem(key, value, type, children){ + var tpl = 'itemCollapsible'; + + if(collapsible) { + tpl = 'itemCollapsibleOpen'; + } + + var element = TEMPLATES[tpl].replace('%KEY%', key); + + element = element.replace('%VALUE%', type); + element = element.replace('%TYPE%', type); + element = element.replace('%CHILDREN%', children); + + return element; + } + + function handleChildren(key, value, type) { + var html = ''; + + for(var item in value) { + var _key = item, + _val = value[item]; + + html += handleItem(_key, _val); + } + + return createCollapsibleItem(key, value, type, html); + } + + function handleItem(key, value) { + var type = typeof value; + + if(typeof value === 'object') { + return handleChildren(key, value, type); + } + + return createItem(key, value, type); + } + + function parseObject(obj) { + _result = '<div class="json">'; + + for(var item in obj) { + var key = item, + value = obj[item]; + + _result += handleItem(key, value); + } + + _result += '</div>'; + + return _result; + } + + return parseObject(json); +}; + + + +var json = { + 'User' : { + 'Personal Info': { + 'Name': 'Eddy', + 'Age': 3 + }, + 'Active': true, + 'Messages': [ + 'Message 1', + 'Message 2', + 'Message 3' + ] + }, + 'Total': 1 +}
\ No newline at end of file diff --git a/File-decryption/webapp/assets/js/main.js b/File-decryption/webapp/assets/js/main.js index aec1baa..23def70 100644 --- a/File-decryption/webapp/assets/js/main.js +++ b/File-decryption/webapp/assets/js/main.js @@ -1,188 +1,66 @@ -// as seen in /usr/lib/libzc.so / SetupKeys -const MASTER_KEY = new Uint8Array([ - 0x13, 0x27, 0x3f, 0x42, - 0xa5, 0xb6, 0x79, 0xe8, - 0x20, 0x31, 0xc4, 0xf5, - 0x16, 0x17, 0x88, 0x2f, - 0x43, 0xa4, 0x55, 0x69, - 0x77, 0xb8, 0xe2, 0x83, - 0x04, 0x05, 0x60, 0x70, - 0x80, 0x02, 0x03, 0x04, - 0x50, 0x6a, 0x7c, 0x8a, - 0x02, 0x30, 0x40, 0x51, - 0x6a, 0x7d, 0x8d, 0x22, - 0x33, 0x44, 0x59, 0x66, - 0x71, 0x08, 0x02, 0x03, - 0x43, 0x05, 0x67, 0x7a, - 0x8f]); +let decryptedBlobUrl = null; +let currentFileName = null; +let currentClearText = null; +let decodedProtobuf = null; -function hex(byteArray) { - return Array.prototype.map.call(byteArray, function (byte) { - return ('0' + (byte & 0xFF).toString(16)).slice(-2); - }).join(''); -} - -// ported from EVP_BytesToKey -// https://github.com/openssl/openssl/blob/c04e78f0c69201226430fed14c291c281da47f2d/crypto/evp/evp_key.c#L78 -function deriveKeyAndIV(password, iterations = 10) { - var keyLen = 16; - var ivLen = 16; - var addmd = 0; - - var key = new Uint8Array(keyLen); - var iv = new Uint8Array(ivLen); - var tmp = new Uint8Array(); - var i, key_i = 0, iv_i = 0; - - for (; keyLen > 0 || ivLen > 0;) { - if (addmd++) { - block = new Uint8Array([...tmp, ...password]); - } else { - block = password; - } - - tmp = new Uint8Array(sha1.array(block)); - for (i = 1; i < iterations; i++) { - tmp = sha1.array(tmp); - } - - i = 0; - while (keyLen && i != tmp.length) { - key[key_i++] = tmp[i]; - keyLen--; - i++; - } - - while (ivLen != 0 && i != tmp.length) { - iv[iv_i++] = tmp[i]; - ivLen--; - i++; - } - } - - return { key: key, iv: iv }; -} - -function processFileInput(e) { - const fileName = e.target.fileName; - const serial = document.getElementById('serial-input').value; - let main_key = null; - if (serial.length > 0) { - // local decryption, use master key + serial - main_key = new Uint8Array([...MASTER_KEY, ...new TextEncoder("utf-8").encode(serial)]); - } else { - // global decryption, use master key only - main_key = MASTER_KEY; - } - - // derive key and iv with our EVP_BytesToKey port - const derived = deriveKeyAndIV(main_key); - // encrypted file contents - const ciphertext = e.target.result; - // import the raw key - window.crypto.subtle.importKey("raw", derived.key, "AES-CTR", true, ["encrypt", "decrypt"]).then(function (key) { - // decrypt using aes-128-ctr - window.crypto.subtle.decrypt({ name: "AES-CTR", counter: derived.iv, length: 128 }, key, ciphertext).then(function (cleartext) { - var blob = new Blob([cleartext], { type: "application/octet-stream" }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = fileName + '.dec'; - link.click(); - }); - }); - processFileDecode(e) -} - -function processFileDecode(e) { - const fileName = e.target.fileName; - const serial = document.getElementById('serial-input').value; - let main_key = null; - if (serial.length > 0) { - // local decryption, use master key + serial - main_key = new Uint8Array([...MASTER_KEY, ...new TextEncoder("utf-8").encode(serial)]); - } else { - // global decryption, use master key only - main_key = MASTER_KEY; - } - - // derive key and iv with our EVP_BytesToKey port - const derived = deriveKeyAndIV(main_key); - // encrypted file contents - const ciphertext = e.target.result; - // import the raw key - window.crypto.subtle.importKey("raw", derived.key, "AES-CTR", true, ["encrypt", "decrypt"]).then(function (key) { - // decrypt using aes-128-ctr - window.crypto.subtle.decrypt({ name: "AES-CTR", counter: derived.iv, length: 128 }, key, ciphertext).then(function (cleartext) { - const protobufType = document.getElementById('protobuf-list').value; +document.addEventListener("DOMContentLoaded", function () { + // handle file uploads + let fileInput = document.getElementById('file-upload'); + let fileDownloadBtn = document.getElementById('file-download'); + let decryptToggle = document.getElementById('decrypt-toggle'); + let serialForm = document.getElementById('serial-form'); + var jsonRenderer = document.querySelector('.target'); - if(protobufType != ""){ - protoTypes.forEach((type)=>{ - if(type.name === protobufType){ - try { - var protoDecoded = type.decode(new Uint8Array(cleartext)) - if(protoDecoded){ - document.getElementById('decrypt-output').value = JSON.stringify(protoDecoded.toJSON(),null,2) - } - } catch (error) { - document.getElementById('decrypt-output').value = 'ERROR PARSING PROTOBUF' - } - } - }) - }else{ - let maxDecodedSize = 0; - let maxDecodedType = ""; - protoTypes.forEach((type)=>{ - try { - var protoDecoded = type.decode(new Uint8Array(cleartext)) - var protoDecodedString = JSON.stringify(protoDecoded.toJSON()) - if(protoDecodedString.length > maxDecodedSize){ - maxDecodedSize = protoDecodedString.length - maxDecodedType = type - } - - } catch (error) { - document.getElementById('decrypt-output').value = 'ERROR PARSING PROTOBUF' - } - }) - if(maxDecodedType !== ""){ - document.getElementById('protobuf-list').value = maxDecodedType.name - var protoDecoded = maxDecodedType.decode(new Uint8Array(cleartext)) - document.getElementById('decrypt-output').value = JSON.stringify(protoDecoded.toJSON(),null,2) - }else{ - var decoder = new TextDecoder("utf-8"); - document.getElementById('decrypt-output').value = decoder.decode(cleartext) - } - } - }); - }); -} + // Don't display the download button until a file is selected + fileDownloadBtn.style.display = 'none'; + serialForm.style.display = 'none'; -document.addEventListener("DOMContentLoaded", function () { - // handle file uploads - let fileInput = document.getElementById('file-input') fileInput.onchange = () => { const reader = new FileReader() - reader.onload = processFileInput; + reader.addEventListener('load', (e) => { + console.log(e); + currentFileName = e.target.fileName; + processFileInput(e); + }); + for (let file of fileInput.files) { // https://stackoverflow.com/questions/24245105/how-to-get-the-filename-from-the-javascript-filereader reader.fileName = file.name; reader.readAsArrayBuffer(file); } + if(fileInput.files.length > 0){ + fileDownloadBtn.style.display = 'flex'; + }else{ + fileDownloadBtn.style.display = 'none'; + } }; - document.getElementById('protobuf-list').onchange = (e)=>{ - let fileInput = document.getElementById('file-input') - const reader = new FileReader() - reader.onload = processFileDecode; - for (let file of fileInput.files) { - // https://stackoverflow.com/questions/24245105/how-to-get-the-filename-from-the-javascript-filereader - reader.fileName = file.name; - reader.readAsArrayBuffer(file); + fileDownloadBtn.onclick = () => { + const link = document.createElement('a'); + link.href = decryptedBlobUrl + link.download = currentFileName + '.dec'; + link.click(); + }; + + decryptToggle.onchange = () => { + if (decryptToggle.checked) { + serialForm.style.display = 'block'; + } else { + serialForm.style.display = 'none'; } - } -}); + }; + + document.getElementById('protobuf-list').onchange = (e)=>{ + processFileDecode(currentClearText); + console.log(decodedProtobuf); + + // Decode protobuf is a class, so we need to convert it to a JSON object + var json = JSON.parse(JSON.stringify(decodedProtobuf)); + jsonRenderer.innerHTML = jsonViewer(json, true); + } +}); var protoPaths = ["assets/proto/Capture.proto","assets/proto/Preset.proto"] var protoTypes = [] diff --git a/File-decryption/webapp/index.html b/File-decryption/webapp/index.html index 529fb0b..c522c08 100644 --- a/File-decryption/webapp/index.html +++ b/File-decryption/webapp/index.html @@ -17,6 +17,8 @@ <link rel="stylesheet" href="assets/css/style.css"/> <script src="assets/js/protobuf.min.js"></script> <script src="assets/js/sha1.js"></script> + <script src="assets/js/json-viewer.js"></script> + <script src="assets/js/file-processing.js"></script> <script src="assets/js/main.js"></script> </head> @@ -28,28 +30,50 @@ </header> <div class="content"> - <div class="serial-input"> - <h3>Enter your Quad Cortex serial number</h3> - <p>(leave blank for global encrypted file)</p> - <input type="text" id="serial-input" /> - </div> - <div class="file-upload"> - <h3 class="file-upload-title">Encrypted file:</h3> - - <label class="file-input-label" id="file-input-label" for="file-input"> - <i class="fa-solid fa-cloud-arrow-up pr-5"></i> - <p>Choose File</p> - </label> - <input class="file-input" id="file-input" type="file"> - </div> - <div class="file-decode"> - <h3 class="file-decode-label">Decoded File:</h3> - <select id="protobuf-list"> - <option value="">None</option> - </select> - <div> - <textarea id="decrypt-output"></textarea> - </div> + <div class="content-container"> + <section class="decryption-form"> + <div class="enable-decryption"> + <h2 class="section-title">Decrypt file</h2> + <label class="switch"> + <input type="checkbox" id="decrypt-toggle"> + <span class="slider round"></span> + </label> + </div> + <div class="serial-input" id="serial-form"> + <h3>Enter your Quad Cortex serial number</h3> + <p>(leave blank for global encrypted file)</p> + <input type="text" id="serial-input" /> + </div> + </section> + + <hr/> + + <section class="file-btns"> + <label class="btn-cta file-input-label file-upload" id="file-input-label" for="file-upload"> + <i class="fa-solid fa-cloud-arrow-up pr-5"></i> + <p>Choose File</p> + </label> + <input class="file-upload-input" id="file-upload" type="file"> + + <button class="btn-cta file-download" id="file-download"> + <i class="fa-solid fa-cloud-arrow-down pr-5"></i> + <p>Download File</p> + </button> + </section> + + <hr/> + + <section class="file-decode"> + <div class="section-title"> + <h3 class="title">Live decoding</h3> + </div> + <select id="protobuf-list"> + <option value="">None</option> + </select> + <div> + <div id="decrypt-output" class="target"></div> + </div> + </section> </div> </div> </body> |