add footer, contact button
All checks were successful
Deploy Pages / deploy (push) Successful in 24s
All checks were successful
Deploy Pages / deploy (push) Successful in 24s
This commit is contained in:
40
index.html
40
index.html
@ -41,6 +41,9 @@
|
|||||||
<meta name="msapplication-TileImage" content="favicon.png?3">
|
<meta name="msapplication-TileImage" content="favicon.png?3">
|
||||||
<meta name="theme-color" content="#67EA94">
|
<meta name="theme-color" content="#67EA94">
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="link-aes.js" defer></script>
|
||||||
|
|
||||||
<title>CT Mesh</title>
|
<title>CT Mesh</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -72,8 +75,8 @@
|
|||||||
.splash-box {
|
.splash-box {
|
||||||
background-color: #EBEBEB;
|
background-color: #EBEBEB;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
padding: 3em;
|
padding: 3em 3em 2em 3em;
|
||||||
margin: 1em;
|
margin: 1em auto 0 auto;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||||
color: #000000;
|
color: #000000;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -88,7 +91,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 35px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
.logo {
|
.logo {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@ -149,6 +152,31 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
}
|
}
|
||||||
|
.btn-small {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #2C2D3C;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.5em 1.25em;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
.site-footer {
|
||||||
|
margin-top: 2em;
|
||||||
|
padding-top: 2em;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #2C2D3C;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.site-footer a {
|
||||||
|
color: #757684;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.site-footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
/* Adjustments for devices with a viewport width of 800px or less */
|
/* Adjustments for devices with a viewport width of 800px or less */
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
@ -212,7 +240,7 @@
|
|||||||
<div class="text-content">
|
<div class="text-content">
|
||||||
<h1>CT Mesh</h1>
|
<h1>CT Mesh</h1>
|
||||||
<p class="sub-header">Connecticut Meshtastic User Group</p>
|
<p class="sub-header">Connecticut Meshtastic User Group</p>
|
||||||
<p>Join the local community!</p>
|
<a href="Ws9mCBVzpuQdAEBDZLxJxNbKOflt64pOog" rel="nofollow noindex" class="btn-small link-aes">Contact</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-groups">
|
<div class="btn-groups">
|
||||||
@ -252,6 +280,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<footer class="site-footer">
|
||||||
|
<p><a href="https://ctmesh.org/">CT Mesh</a> is a volunteer-run user group for <a href="https://meshtastic.org" target="_blank">Meshtastic</a> enthusiasts in Connecticut.</p>
|
||||||
|
<p><a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">Content licensed CC BY-SA 4.0</a></p>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
236
link-aes.js
Normal file
236
link-aes.js
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function ()
|
||||||
|
{
|
||||||
|
const keyText = '1CnBWwQtMAwRP8E0mZLkRxywSisGedf3XzyHmCIK1BM';
|
||||||
|
|
||||||
|
const onGetKey = function (key)
|
||||||
|
{
|
||||||
|
const coder = new AesLinkCoder(key);
|
||||||
|
|
||||||
|
document.querySelectorAll('.link-aes')
|
||||||
|
.forEach(coder.decode.bind(coder));
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('subtle' in window.crypto) {
|
||||||
|
Aes.getKey(keyText).then(onGetKey);
|
||||||
|
} else {
|
||||||
|
console.log('AES encryption is only available in secure contexts (such as over https or localhost)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Aes
|
||||||
|
|
||||||
|
const Aes = {
|
||||||
|
'KEY_BITS': 256, // 128 (SECRET) | 192 (TOP SECRET) | 256 (TOP SECRET)
|
||||||
|
'NONCE_BYTES': 3, // 3 bytes = 16,777,216 possible nonces
|
||||||
|
'BLOCK_BYTES': 16, // Required by AES
|
||||||
|
'BITS_PER_BYTE': 8 // Conversion factor
|
||||||
|
};
|
||||||
|
|
||||||
|
Aes.newKeyText = async function ()
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
'name': 'AES-CTR',
|
||||||
|
'length': Aes.KEY_BITS
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = await window.crypto.subtle.generateKey(options, true, ['encrypt', 'decrypt']);
|
||||||
|
const keyBuffer = await window.crypto.subtle.exportKey('raw', key);
|
||||||
|
const keyBytes = new Uint8Array(keyBuffer);
|
||||||
|
const keyText = Base64.text(keyBytes);
|
||||||
|
|
||||||
|
return keyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aes.getKey = async function (keyText)
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
'name': 'AES-CTR'
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyBytes = Base64.bytes(keyText);
|
||||||
|
const key = window.crypto.subtle.importKey('raw', keyBytes, options, false, ['encrypt', 'decrypt']);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aes.encrypt = async function (plainText, key)
|
||||||
|
{
|
||||||
|
const nonceBytes = Aes.__newNonce();
|
||||||
|
const counterBytes = Aes.__newCounter(nonceBytes);
|
||||||
|
const options = Aes.__newOptions(counterBytes);
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const plainBytes = encoder.encode(plainText);
|
||||||
|
const codeBuffer = await window.crypto.subtle.encrypt(options, key, plainBytes);
|
||||||
|
const codeBytes = new Uint8Array(codeBuffer);
|
||||||
|
const outputBytes = new Uint8Array(nonceBytes.length + codeBytes.length);
|
||||||
|
outputBytes.set(nonceBytes);
|
||||||
|
outputBytes.set(codeBytes, nonceBytes.length);
|
||||||
|
const outputText = Base64.text(outputBytes);
|
||||||
|
|
||||||
|
return outputText;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aes.__newNonce = function ()
|
||||||
|
{
|
||||||
|
const nonce = new Uint8Array(Aes.NONCE_BYTES);
|
||||||
|
window.crypto.getRandomValues(nonce);
|
||||||
|
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aes.__newCounter = function (nonce)
|
||||||
|
{
|
||||||
|
const counter = new Uint8Array(Aes.BLOCK_BYTES);
|
||||||
|
counter.set(nonce);
|
||||||
|
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aes.__newOptions = function (counter)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
'name': 'AES-CTR',
|
||||||
|
'counter': counter,
|
||||||
|
'length': (Aes.BLOCK_BYTES - Aes.NONCE_BYTES) * Aes.BITS_PER_BYTE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Aes.decrypt = async function (inputText, key)
|
||||||
|
{
|
||||||
|
const inputBytes = Base64.bytes(inputText);
|
||||||
|
const nonceBytes = inputBytes.slice(0, Aes.NONCE_BYTES);
|
||||||
|
const codeBytes = inputBytes.slice(Aes.NONCE_BYTES);
|
||||||
|
const counterBytes = Aes.__newCounter(nonceBytes);
|
||||||
|
const options = Aes.__newOptions(counterBytes);
|
||||||
|
const plainBytes = await window.crypto.subtle.decrypt(options, key, codeBytes);
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const plainText = decoder.decode(plainBytes);
|
||||||
|
|
||||||
|
return plainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Base64
|
||||||
|
|
||||||
|
const Base64 = {};
|
||||||
|
|
||||||
|
Base64.bytes = function (text)
|
||||||
|
{
|
||||||
|
const values = Base64.__getValues(text);
|
||||||
|
const bytes = Base64.__getBytes(values);
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Base64.__getValues = function (text)
|
||||||
|
{
|
||||||
|
const values = new Uint8Array(text.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; ++i) {
|
||||||
|
const code = text.charCodeAt(i);
|
||||||
|
|
||||||
|
values[i] = Base64.__getValue(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
Base64.__getValue = function (code)
|
||||||
|
{
|
||||||
|
if (code === 45) {
|
||||||
|
return 63;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code < 58) {
|
||||||
|
return code - 48;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code < 91) {
|
||||||
|
return code - 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 95) {
|
||||||
|
return 62;
|
||||||
|
}
|
||||||
|
|
||||||
|
return code - 87;
|
||||||
|
}
|
||||||
|
|
||||||
|
Base64.__getBytes = function (values)
|
||||||
|
{
|
||||||
|
const length = Math.floor(3 * values.length / 4);
|
||||||
|
const bytes = new Uint8Array(length);
|
||||||
|
|
||||||
|
for (let i = 0, j = 0; i < values.length; ++i) {
|
||||||
|
const r = 2 * (i % 4);
|
||||||
|
|
||||||
|
if (r !== 0) {
|
||||||
|
bytes[j++] = ((values[i-1] << r) & 0xff) | (values[i] >> (6 - r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Base64.text = function (bytes)
|
||||||
|
{
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < bytes.length; ++i) {
|
||||||
|
const r = i % 3;
|
||||||
|
|
||||||
|
if (r === 0) {
|
||||||
|
text += Base64.__getSymbol(bytes[i] >> 2) + Base64.__getSymbol(((bytes[i] << 4) & 63) | (bytes[i+1] >> 4));
|
||||||
|
} else if (r === 1) {
|
||||||
|
text += Base64.__getSymbol(((bytes[i] << 2) & 63) | (bytes[i+1] >> 6));
|
||||||
|
} else {
|
||||||
|
text += Base64.__getSymbol(bytes[i] & 63);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Base64.__getSymbol = function (value)
|
||||||
|
{
|
||||||
|
if (value < 10) {
|
||||||
|
return String.fromCharCode(48 + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 36) {
|
||||||
|
return String.fromCharCode(87 + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 62) {
|
||||||
|
return String.fromCharCode(29 + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 62) {
|
||||||
|
return '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// AesLinkCoder
|
||||||
|
|
||||||
|
function AesLinkCoder (key)
|
||||||
|
{
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
AesLinkCoder.prototype.decode = function (node)
|
||||||
|
{
|
||||||
|
Aes.decrypt(node.getAttribute('href'), this.key)
|
||||||
|
.then(node.setAttribute.bind(node, 'href'));
|
||||||
|
}
|
||||||
|
|
||||||
|
AesLinkCoder.prototype.encode = function (node)
|
||||||
|
{
|
||||||
|
Aes.encrypt(node.getAttribute('href'), this.key)
|
||||||
|
.then(node.setAttribute.bind(node, 'href'));
|
||||||
|
}
|
Reference in New Issue
Block a user