TrustyHash/index.html

569 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrustyHash</title>
<script>
/*
This is free Javascript licensed under the MIT (Expat) License.
Source and documentation:
https://github.com/sprin/TrustyHash
@licstart The following is the entire license notice for the
JavaScript code in this page.
Copyright (C) 2016 Steffen Prince
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/
</script>
<style>
html {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
"Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
padding-right: 15%;
padding-left: 15%;
}
h1, h2 {
margin: 0.8em 0 0.2em 0;
}
p {
font-size: 18px;
margin: 0.2em 0 0.2em 0;
}
#lang-widget {
cursor: pointer;
margin: 0.8em 0 0.2em 0;
font-size: 14px;
float: right;
}
#lang-widget>ul {
list-style-type: none;
padding: 5px 10px 5px 10px;
margin: 0px;
display: block;
}
#lang-widget li {
padding: 3px;
display: inline;
}
#lang-widget a {
text-decoration: none;
}
#url {
width: 450px;
margin: 0.8em 0 0.2em 0;
}
#drop-area {
width: 96%;
margin: 0.5em 1em 0.5em 1em;
border: solid #000 1px;
padding: 2em;
text-align: center;
font-size: 20px;
}
#fileinput {
margin-left: 75%;
}
#save-file {
visibility: hidden;
}
[data-tooltip] {
border-bottom: 1px dotted #000;
color: #000;
outline: none;
cursor: help;
text-decoration: none;
position: relative;
}
[data-tooltip]:after {
-webkit-transition: .2s;
transition: .2s;
opacity: 0;
visibility: hidden;
top: 2em;
display: block;
position: absolute;
width: 350px;
padding: 10px;
left: 0px;
margin-bottom: 15px;
background: #FFF;
border: 1px solid #000;
text-align: center;
content: attr(data-tooltip);
}
[data-tooltip]:hover:after {
opacity: 1;
visibility: visible;
margin-bottom: 12px;
}
#noscript-overlay {
position: absolute;
left: 0px;
background: #FFF;
width: 100%;
height: 100%;
z-index: 10000;
}
#result {
height: 8em;
margin-top: 1.5em;
}
footer {
padding-bottom: 2px;
position: fixed;
bottom: 0px;
left: 0px;
width: 100%;
text-align: center;
background: #FFF;
}
</style>
<script>
// Set up handlers on load.
window.addEventListener("load", function () {
// Localize the interface.
localizeDOM();
var langChoices = document.querySelectorAll("[data-lang]");
for (choice of langChoices) {
choice.addEventListener('click', handleLangClick);
}
// Check if app loaded from local copy, and hide reminder to save if local.
checkIfLocalCopy();
// Compute the hash when a selection is made with the file input
var fileinput = document.getElementById('fileinput');
fileinput.addEventListener('change', handleFileInputChange);
// Compute the hash when a file is dropped into the drop area
var dropArea = document.getElementById('drop-area');
dropArea.addEventListener('dragover', handleDropAreaDragover);
dropArea.addEventListener('drop', handleDropAreaDrop);
// Open the file input prompt when the drop area is clicked
dropArea.addEventListener('click', handleDropAreaClick);
// Fetch the remote resource upon submit, and hash if fetch succeeds.
var form = document.getElementById('url-form');
form.addEventListener("submit", handleURLSubmit);
});
// Event handlers
function handleLangClick() {
setLang(this.dataset.lang);
localizeDOM();
}
function handleFileInputChange() {
// Hash a local file when selected via file input.
clearResult();
var fileinput = this;
var file = fileinput.files[0];
hashFile(file)
.then(showHash)
.catch(showHashError);
}
function handleDropAreaDragover(evt) {
// Set up the drop area.
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
}
function handleDropAreaDrop(evt) {
// Hash the file that is dropped into the drop area.
evt.preventDefault();
clearResult();
var file = evt.dataTransfer.files[0];
hashFile(file)
.then(showHash)
.catch(showHashError);
}
function handleDropAreaClick (evt) {
// Show the file select dialog.
evt.preventDefault();
var doc = document.getElementById('fileinput').ownerDocument;
var fileinputClick = doc.createEvent('MouseEvents');
fileinputClick.initEvent('click', true, true);
fileinput.dispatchEvent(fileinputClick, true);
}
function handleURLSubmit(evt) {
// Trigger the GET request when submit is clicked in the URL form.
evt.preventDefault();
clearResult();
var saveFile = document.getElementById('save-file');
saveFile.style.visibility = 'hidden';
var url = document.getElementById('url').value;
if (url == '') {
return;
}
var url = normalizeURL(url)
fetchRemoteResource(url)
.then(function(xhr) {
// Hash response.
crypto.subtle.digest("SHA-256", xhr.response)
.then(showHash)
.catch(showHashError);
// Allow response to be saved.
setUpSaveFile(xhr);
})
.catch(showFetchError);
}
function setUpSaveFile(xhr) {
// Set up the Save File.
var saveFile = document.getElementById('save-file');
saveFile.style.visibility = 'visible';
// Unbind previous Save File handler, if any.
saveFile.removeEventListener('click', saveFile.handleSaveFileClick);
// Show the save prompt on click.
saveFile.handleSaveFileClick = function() {
var filename = filenameFromURL(xhr.responseURL);
showSavePrompt(filename, xhr.response);
};
saveFile.addEventListener('click', saveFile.handleSaveFileClick);
}
function checkIfLocalCopy() {
// Hide the description, including a recommendation to save if the app
// has been loaded from a local file.
if (/^file:\/\//.test(window.location.toString())) {
document.getElementById('description').style.visibility = 'hidden';
}
}
// Utilities
function bufferToHex(buffer) {
// Convert a buffer into a hexadecimal string.
// From https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
var hexCodes = [];
var view = new DataView(buffer);
for (var i = 0; i < view.byteLength; i += 4) {
// Using getUint32 reduces the number of iterations needed (we process
// 4 bytes each time).
var value = view.getUint32(i);
// toString(16) will give the hex representation of the number without
// padding
var stringValue = value.toString(16);
// We use concatenation and slice for padding.
var padding = '00000000';
var paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue);
}
return hexCodes.join("");
}
function bufferToBase64(buffer) {
// Convert a buffer into a base64 string.
var str = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
str += String.fromCharCode(bytes[i]);
}
return window.btoa(str);
}
function fetchRemoteResource(url) {
// Fetch a URL with a GET request, saving the response as a buffer.
// Returns a Promise of a successful XHR.
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', function() {
if (xhr.status == 200) {
resolve(xhr);
} else {
reject(Error('Non-200 Status Code'));
}
});
function onerror(e) {
// Browser may have blocked request due to mixed content
if (
url.substr(0,7) == 'http://'
&& window.location.toString().substr(0,8) == 'https://'
) {
reject(Error('Mixed content blocked'));
} else {
reject(e)
}
}
xhr.addEventListener('error', onerror)
xhr.open('GET', url);
// Must wrap call in try/catch because the error event is not correctly
// fired in Firefox when mixed-content requests are blocked.
try {
xhr.send();
} catch(e) {
onerror(e)
};
});
}
function hashFile(file) {
// Hash a File object.
// Returns a Promise of a successful hash.
return new Promise(function(resolve, reject) {
var fileReader = new FileReader();
fileReader.addEventListener('load', function() {
crypto.subtle.digest("SHA-256", this.result)
.then(resolve)
.catch(reject);
});
fileReader.readAsArrayBuffer(file);
});
}
function filenameFromURL(url) {
// Generate a reasonable filename for a given URL.
var noprefix = url.replace(/^http:\/\/|^https:\/\//, '');
var parts = noprefix.split('/');
if (parts.length > 1 && parts[parts.length !== '']) {
// Use the part of the URL after the last '/'.
return parts[parts.length - 1];
} else {
// Assume that if there is no part after hostname, or no part after
// the final '/', then this must be an html file.
return noprefix + '.html';
}
}
function normalizeURL(url) {
// Add http:// protocol if no protocol is supplied.
if (!/^http:\/\/|^https:\/\//.test(url)) {
return 'http://' + url;
}
return url;
}
// DOM mutation
function showHash(hash) {
// Display the hash as a hex string.
var sha256 = bufferToHex(hash);
document.getElementById('result').innerHTML =
'<h2>SHA-256 Hash</h2><p>' + sha256;
}
function clearResult() {
document.getElementById('result').innerHTML = '';
}
function showHashError(err) {
// Display a generic hash error.
document.getElementById('result').innerHTML =
'<h2>' + gettext('error-header') + '</h2>' +
'<p>' + gettext('error-hash');
}
function showFetchError(e) {
var url = normalizeURL(document.getElementById('url').value);
var errorMsg;
if (e.message == 'Mixed content blocked') {
// Display a message specific to mixed content errors.
errorMsg = 'error-mixed-content';
} else {
// Otherwise, display a generic fetch error.
errorMsg = 'error-fetch';
}
document.getElementById('result').innerHTML =
'<h2>' + gettext('error-header') + '</h2>' +
'<p>' + gettext(errorMsg, {'url': url});
}
function showSavePrompt(filename, buffer) {
// Prompt the user to save the file.
var link = document.createElement('a');
link.download = filenameFromURL(xhr.responseURL);
link.href = 'data:Application/octet-stream;base64,' +
bufferToBase64(xhr.response);
// Firefox requires that the link exist on page before we can click.
document.body.appendChild(link);
link.click();
}
// Internationalization and Localization
var localization = {
"en": {
"title": "TrustyHash - Trustable Hash Calculator",
"description": "This tool computes SHA-256 hashes directly in your browser. Files are not sent to any server.<br>It's recommended to save this page and <a href=\"https://github.com/sprin/TrustyHash#trust\">verify the integrity before using</a>.",
"local-header": "Hash a File from Your Computer",
"drop-area-label": "Drop file here, or click to browse",
"remote-header": "Hash a File from a Website",
"remote-label": "<a href=\"#\" class=\"tooltip\" data-tooltip=\"Websites which enable Cross Origin Resource Sharing (CORS) can be accessed by this tool. However, most websites do not enable CORS.\">Certain websites</a> may allow this tool to retrieve a file directly, without you having to save it to your computer first.",
"submit-btn": "Submit",
"save-file-btn": "Save File",
"result-header": "SHA-256 Hash",
"error-header": "Error",
"error-hash": "Error hashing file",
"error-fetch": "Error fetching {url}<br>If this URL is valid, the remote server probably does not allow cross-domain requests.",
"error-mixed-content": "Error fetching {url}<br>Browser blocked HTTP request because this page was loaded over HTTPS. To work around this, save this page locally, open the local copy, and retry.",
"footer": "Version 1.0.0<br>Free Software under MIT License<br>Read how this tool relates to trust, integrity, and authenticity at the <a href=\"https://github.com/sprin/TrustyHash\">TrustyHash homepage</a>."
},
"es": {
"title": "TrustyHash - Calculadora Hash Confiable",
"description": "Calcula los hashes SHA-256 directamente en el navegador. No se envian los archivos a ningún servidor.<br>Se recomienda guardar esta página y <a href=\"https://github.com/sprin/TrustyHash#trust\">verificar la integridad antes de usar</a>.",
"local-header": "Calcula el hash de un archivo local",
"drop-area-label": "Solta un archivo aquí, o haga clic para buscar",
"remote-header": "Calcula el hash de un sitio web",
"remote-label": "<a href=\"#\" class=\"tooltip\" data-tooltip=\"Se puede ser acceder a sitios web con esta calculadora que son configurados con Cross Origin Resource Sharing (CORS). Sin embargo, la mayoria de los sitios no son configurados con CORS.\">Algunos sitios web</a> podría permitir descargar directamenta.",
"submit-btn": "Enviar",
"save-file-btn": "Guardar",
"result-header": "Hash SHA-256",
"error-header": "Error",
"error-hash": "Error al calcular de hash",
"error-fetch": "Error al descargar {url}<br>Si este URL es valido, probablemente el servidor remoto no permite peticiones cross-domain.",
"error-mixed-content": "Error al descargar {url}<br>El navegador bloqueó el petición HTTP porque esta pagina fue cargado con HTTPS. Para solucionar este problema, guarda esta pagina localmente, abre la copia, y reintente.",
"footer": "Versión 1.0.0<br>Software Libre con MIT License<br>Aprende a verificar la integridad y la autenticidad de esta calculadora en la <a href=\"https://github.com/sprin/TrustyHash\">página de TrustyHash</a>."
}
}
// Use the language of the browser as default.
// For now, ignore everything after the first two characters.
var lang;
setLang(window.navigator.language.slice(0,2));
function setLang(newLang) {
// Set the language for localization, falling back to 'en' if there
// is no localization for the language.
if (newLang in localization) {
lang = newLang;
} else {
lang = 'en';
}
}
function gettext(msgid) {
// Translate the msgid into a localized string and perform string substition.
// Format tokens for string substitution are expressed as { ... }, where the
// string inside the brackets is the key of the substitution string.
// Values are looked up from the second argument, a key/value Object.
var msgstr = localization[lang][msgid];
if (msgstr == null) {
throw Error("Attempted lookup on non-existent msgid: " + msgid);
}
// Perform token substitutions
if (arguments.length > 1) {
var subs = arguments[1];
for (var key in subs) {
msgstr = msgstr.replace(RegExp('{'+ key + '}'), subs[key]);
}
}
// Check to see that all format tokens in string have been replaced.
if (/{.+\}/.test(msgstr)) {
throw Error("Unmatched tokens when localizing " + msgid + ": " + msgstr);
}
return msgstr;
}
function localizeDOM() {
// Translate all localized elements in the DOM.
clearResult();
// Hide current language choice
var choices = document.querySelectorAll("[data-lang]");
for (var el of choices) {
el.style.display = 'inline';
}
document.querySelector("[data-lang=" + lang + "]").style.display = 'none'
var elements = document.querySelectorAll("[data-l10n-id]");
for (var el of elements) {
var msgid = el.attributes['data-l10n-id'].value;
if (el.nodeName == 'INPUT') {
el.value = gettext(msgid);
} else {
el.innerHTML = gettext(msgid);
}
}
}
</script>
</head>
<body>
<div id="lang-widget">
<ul>
<li><a data-lang="en" href="#">English</a></li>
<li><a data-lang="es" href="#">Español</a></li>
</ul>
</div>
<h1 data-l10n-id="title">TrustyHash - Trustable Hash Calculator</h1>
<p data-l10n-id="description" id="description">This tool computes SHA-256 hashes directly in your browser. Files are not sent to any server.<br>It's recommended to save this page and <a href=\"https://github.com/sprin/TrustyHash#trust\">verify the integrity before using</a></p>
<noscript>
<h1>Please enable JavaScript. This tool uses 100% free JavaScript. View page source for license and code.</h1>
<h2>Version 1.0.0<br>Learn how to verify the integrity and authenticity of this tool at the <a href="https://github.com/sprin/TrustyHash">TrustyHash homepage</a>.</h2>
<div id="noscript-overlay"></div>
</noscript>
<h2 data-l10n-id="local-header">Hash a File from Your Computer</h2>
<div data-l10n-id="drop-area-label" id="drop-area">Drop file here, or click to browse</div>
<p><input type="file" id="fileinput" />
<h2 data-l10n-id="remote-header">Hash a File from a Website</h2>
<p data-l10n-id="remote-label"><a href="#" class="tooltip" data-tooltip="Websites which enable Cross Origin Resource Sharing (CORS) can be accessed by this tool. However, most websites do not enable CORS.">Certain websites</a> may allow this tool to retrieve a file directly, without you having to save it to your computer first.
<form id="url-form" name="url-form">
<p>URL: <input id="url">
<input data-l10n-id="submit-btn" type="submit" value="Submit">
<input data-l10n-id="save-file-btn" id="save-file" type="button" value="Save File">
</form>
<div id="result"></div>
<footer data-l10n-id="footer">Version 1.0.0<br>Free Software under MIT License<br>Read how this tool relates to trust, integrity, and authenticity at the <a href="https://github.com/sprin/TrustyHash">TrustyHash homepage</a>.</footer>
</body>
</html>