Skip to content

Commit 585c825

Browse files
committed
update to ScyWeb
1 parent 6fe367b commit 585c825

11 files changed

Lines changed: 607 additions & 25 deletions
12.9 MB
Loading
1.31 MB
Loading

Big_Buck_Bunny_1080_10s_30MB.mp4

29.3 MB
Binary file not shown.

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,25 @@ A scytale is an ancient Greek transposition cipher that utilizes a rod and parch
77
## Why do we need it?
88
Browsers have become state of the art applications. Many apps available on app stores can be made with wider compatibility for fractions of the code-base and energy. ScyWeb is only 345 lines of code at its base. Cybersecurity is getting harder and harder to achieve and an offline web app makes hacking virtually impossible, a breath of fresh air in a time of insecurity.
99

10+
## Image scrambling
11+
Lossless compression is not used on unscrambled images so the original image might be much smaller when compared to the new unscrambled image. Preliminary checks have identified scrambled images without lossless compression substantially smaller than images encrypted to plain text. A scrambled image is a much better option than encrypting images and a more secure option than encrypting an entire database or container of images and decrypting that database or container on application boot up. Image scrambling can be used in conjunction with current compression algorithms without causing issues. Unfortunately, the default mode does not show a proof of concept for a smaller scrambled file size when compared to an encrypted string but the alternative mode (under development) does. The important alternative method metrics have been listed below:
12+
13+
![Image Scrambling Metrics - Scrambling versus Encryption](https://raw.githubusercontent.com/mdbench/ScyWeb/master/Screenshot-2026-03-29-7.57.16-PM.png)
14+
15+
An encrypted string of the original image [glacier_og_image_4503x3002.jpg](https://raw.githubusercontent.com/mdbench/ScyWeb/refs/heads/main/glacier_og_image_4503x3002.jpg) is 2.3MBs. You can see the methodology used to convert images to encrypted strings by visiting [the image to string converter demo](https://demos.matthewbenchimol.com/ScyWeb/ScyWebConverter.html). Without lossless compression, a scrambled version of the original image is 12.9MBs. With lossless compression, a scrambled version of the original image is 1.3MBs. This was surprising, as it indicated a scrambled image file size could actually be lower than the original copy. The alternative method uses block-chunking using similar nearest neighbors enhancing compression algorithims. It might even be better to use the block-chunking approach if you are going to compress images. More testing is needed.
16+
17+
18+
## Movie scrambling
19+
Lossless compression is not used on unscrambled movies so the original movie might be much smaller when compared to the new unscrambled movie. Scrambled movies are much bigger in their scrambled form than movies that use pixel information sharing, as this gets lost in the enciphering process. This is an area for further development. It means encrypted movies are smaller in size than scrambled movies. Below is a demo showing image scrambling being used on Big Buck Bunny at 1080 and then being converted back to unscrambled. There is no visual difference between the movies before and after scrambling and unscrambling them.
20+
- [Demo](https://demos.matthewbenchimol.com/ScyWeb/ScyWebVideoTester.html)
21+
- [Original Big Buck Bunny](https://raw.githubusercontent.com/mdbench/ScyWeb/refs/heads/main/Big_Buck_Bunny_1080_10s_30MB.mp4)
22+
- [Unscrambled Big Buck Bunny](https://raw.githubusercontent.com/mdbench/ScyWeb/refs/heads/main/enc_BBB_Unscrambled.mp4)
23+
1024
## Due-Outs
25+
- Add checkbox for default versus alternative method option
26+
- Fix pixel re-replication issue on alternative method
1127
- Add bulk image download as .zip option
28+
- Integrate image scrambling into HelioWeb
1229

1330
## Want to contribute?
1431
Send me a pull request.
70.6 KB
Loading

ScyWeb.html

Lines changed: 236 additions & 25 deletions
Large diffs are not rendered by default.

ScyWebConverter.html

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Image Encryptor & Decryptor</title>
7+
<style>
8+
:root { --purple: #9c27b0; --dark-bg: #121212; --dark-card: #1e1e1e; --text-light: #e0e0e0; }
9+
body { font-family: 'Segoe UI', sans-serif; background-color: var(--dark-bg); color: var(--text-light); padding: 20px; display: flex; flex-direction: column; align-items: center; }
10+
.container { background-color: var(--dark-card); padding: 30px; border-radius: 10px; width: 100%; max-width: 600px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
11+
h1 { color: var(--purple); text-align: center; }
12+
.preview-area { margin: 20px 0; text-align: center; }
13+
#imagePreview { max-width: 100%; max-height: 300px; border: 2px dashed var(--purple); border-radius: 8px; display: none; margin: 0 auto; }
14+
.placeholder { width: 100%; height: 200px; background: rgba(156,39,176,0.1); border: 2px dashed var(--purple); display: flex; justify-content: center; align-items: center; }
15+
input, button { padding: 12px; margin-bottom: 15px; border-radius: 5px; border: none; width: 100%; box-sizing: border-box; }
16+
input { background: #2a2a2a; color: white; }
17+
button { background: var(--purple); color: white; cursor: pointer; font-weight: bold; }
18+
button:hover { background: #7b1fa2; }
19+
.status { padding: 10px; border-radius: 5px; display: none; text-align: center; margin-top: 10px; }
20+
.success { background: rgba(76,175,80,0.2); color: #4CAF50; }
21+
.error { background: rgba(244,67,54,0.2); color: #F44336; }
22+
.tab-buttons { display: flex; gap: 10px; margin-bottom: 20px; }
23+
.tab-btn { background: #333; flex: 1; color: white; border-radius: 5px; border: none; cursor: pointer; padding: 10px; }
24+
.tab-btn.active { background: var(--purple); }
25+
</style>
26+
</head>
27+
<body>
28+
29+
<div class="container">
30+
<h1>Secure Image Tool</h1>
31+
<div class="tab-buttons">
32+
<button class="tab-btn active" id="tabEnc">Encrypt</button>
33+
<button class="tab-btn" id="tabDec">Decrypt</button>
34+
</div>
35+
36+
<div id="encryptSection">
37+
<input type="file" id="imageUpload" accept="image/*">
38+
<div class="preview-area">
39+
<div class="placeholder" id="placeholder">Image Preview</div>
40+
<img id="imagePreview">
41+
</div>
42+
<input type="password" id="encPassword" placeholder="Set Encryption Password">
43+
<button id="encryptBtn">Encrypt & Download (.txt)</button>
44+
</div>
45+
46+
<div id="decryptSection" style="display:none;">
47+
<input type="file" id="fileUpload" accept=".txt">
48+
<input type="password" id="decPassword" placeholder="Enter Password to Decrypt">
49+
<button id="decryptBtn">Decrypt & View Image</button>
50+
</div>
51+
52+
<div class="status" id="status"></div>
53+
</div>
54+
55+
<script>
56+
const statusDiv = document.getElementById('status');
57+
const imagePreview = document.getElementById('imagePreview');
58+
const placeholder = document.getElementById('placeholder');
59+
60+
function switchTab(tab) {
61+
document.getElementById('encryptSection').style.display = tab === 'encrypt' ? 'block' : 'none';
62+
document.getElementById('decryptSection').style.display = tab === 'decrypt' ? 'block' : 'none';
63+
document.getElementById('tabEnc').classList.toggle('active', tab === 'encrypt');
64+
document.getElementById('tabDec').classList.toggle('active', tab === 'decrypt');
65+
statusDiv.style.display = 'none';
66+
}
67+
68+
document.getElementById('tabEnc').onclick = () => switchTab('encrypt');
69+
document.getElementById('tabDec').onclick = () => switchTab('decrypt');
70+
71+
function showStatus(msg, type) {
72+
statusDiv.innerText = msg;
73+
statusDiv.className = 'status ' + type;
74+
statusDiv.style.display = 'block';
75+
}
76+
77+
// Stack-safe conversion using native browser APIs
78+
async function bufferToBase64(buffer) {
79+
const blob = new Blob([buffer]);
80+
return new Promise((resolve, reject) => {
81+
const reader = new FileReader();
82+
reader.onload = () => resolve(reader.result.split(',')[1]);
83+
reader.onerror = reject;
84+
reader.readAsDataURL(blob);
85+
});
86+
}
87+
88+
function base64ToBuffer(b64) {
89+
const bin = atob(b64);
90+
const bytes = new Uint8Array(bin.length);
91+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
92+
return bytes;
93+
}
94+
95+
async function deriveKey(password, salt, usage) {
96+
const encoder = new TextEncoder();
97+
const baseKey = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveKey"]);
98+
return crypto.subtle.deriveKey(
99+
{ name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },
100+
baseKey, { name: "AES-GCM", length: 256 }, false, [usage]
101+
);
102+
}
103+
104+
document.getElementById('imageUpload').onchange = e => {
105+
const file = e.target.files[0];
106+
if (file) {
107+
const reader = new FileReader();
108+
reader.onload = e => {
109+
imagePreview.src = e.target.result;
110+
imagePreview.style.display = 'block';
111+
placeholder.style.display = 'none';
112+
};
113+
reader.readAsDataURL(file);
114+
}
115+
};
116+
117+
document.getElementById('encryptBtn').onclick = async () => {
118+
const fileInput = document.getElementById('imageUpload');
119+
const pass = document.getElementById('encPassword').value;
120+
if (!fileInput.files[0] || !pass) return showStatus("Missing file or password", "error");
121+
122+
showStatus("Encrypting...", "success");
123+
try {
124+
const rawBuffer = await fileInput.files[0].arrayBuffer();
125+
const salt = crypto.getRandomValues(new Uint8Array(16));
126+
const iv = crypto.getRandomValues(new Uint8Array(12));
127+
const key = await deriveKey(pass, salt, "encrypt");
128+
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, rawBuffer);
129+
130+
const combined = new Uint8Array(28 + encrypted.byteLength);
131+
combined.set(salt, 0);
132+
combined.set(iv, 16);
133+
combined.set(new Uint8Array(encrypted), 28);
134+
135+
const b64 = await bufferToBase64(combined);
136+
const blob = new Blob([b64], { type: "text/plain" });
137+
const url = URL.createObjectURL(blob);
138+
const a = document.createElement('a');
139+
a.href = url;
140+
a.download = `secure_${fileInput.files[0].name}.txt`;
141+
a.click();
142+
showStatus("Success! File downloaded.", "success");
143+
} catch (err) { showStatus("Encryption failed", "error"); }
144+
};
145+
146+
document.getElementById('decryptBtn').onclick = async () => {
147+
const fileInput = document.getElementById('fileUpload');
148+
const pass = document.getElementById('decPassword').value;
149+
if (!fileInput.files[0] || !pass) return showStatus("Missing file or password", "error");
150+
151+
try {
152+
const b64Data = await fileInput.files[0].text();
153+
const combined = base64ToBuffer(b64Data);
154+
const salt = combined.slice(0, 16);
155+
const iv = combined.slice(16, 28);
156+
const ciphertext = combined.slice(28);
157+
158+
const key = await deriveKey(pass, salt, "decrypt");
159+
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
160+
161+
imagePreview.src = URL.createObjectURL(new Blob([decrypted]));
162+
imagePreview.style.display = 'block';
163+
placeholder.style.display = 'none';
164+
switchTab('encrypt');
165+
showStatus("Decrypted successfully!", "success");
166+
} catch (err) { showStatus("Wrong password or corrupted file", "error"); }
167+
};
168+
</script>
169+
</body>
170+
</html>

ScyWebVideoTester.html

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Video Comparison</title>
7+
<style>
8+
:root { --p: #9c27b0; --pl: #ba68c8; --bg: #0b0b0b; --s: #1e1e1e; --t: #e1e1e1; }
9+
body { font-family: 'Segoe UI', sans-serif; background: var(--bg); color: var(--t); margin: 0; padding: 20px; }
10+
.container { max-width: 1200px; margin: 0 auto; }
11+
12+
/* VIEWERS */
13+
.comparison-viewer { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; overflow: hidden; border-radius: 8px; cursor: crosshair; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
14+
.layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
15+
#w2 { width: 50%; overflow: hidden; border-right: 2px solid var(--p); z-index: 2; }
16+
#diff-canvas { z-index: 3; pointer-events: none; display: none; width: 100%; height: 100%; }
17+
video { width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
18+
19+
/* GRAPH & STATS */
20+
.analytics-panel { display: grid; grid-template-columns: 2fr 1fr; gap: 15px; margin-top: 20px; }
21+
.card { background: var(--s); padding: 15px; border-radius: 8px; position: relative; }
22+
#psnr-graph { width: 100%; height: 120px; background: #000; border-radius: 4px; border: 1px solid #333; }
23+
.psnr-value { font-size: 24px; color: #4caf50; font-weight: bold; margin-bottom: 5px; }
24+
25+
/* UI */
26+
.controls { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0; justify-content: center; align-items: center; background: var(--s); padding: 15px; border-radius: 8px; }
27+
button, select { background: var(--p); color: #fff; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; transition: 0.2s; }
28+
button:hover { background: var(--pl); }
29+
.heat-btn.active { background: #f44336; outline: 2px solid white; }
30+
input[type="range"] { accent-color: var(--p); }
31+
.label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; }
32+
</style>
33+
</head>
34+
<body>
35+
36+
<div class="container">
37+
<h2 style="text-align:center; color:var(--p)">Video Comparison</h2>
38+
39+
<div class="comparison-viewer" id="viewer">
40+
<div class="layer" id="w1"><video id="v1" src="Big_Buck_Bunny_1080_10s_30MB.mp4" crossorigin="anonymous" muted loop></video></div>
41+
<div class="layer" id="w2"><video id="v2" src="enc_BBB_Unscrambled.mp4" crossorigin="anonymous" muted loop style="width:1200px;"></video></div>
42+
<canvas id="diff-canvas"></canvas>
43+
</div>
44+
45+
<div class="controls">
46+
<button id="play">Play / Pause</button>
47+
<button id="next">Next Frame</button>
48+
<button id="heatmapBtn" class="heat-btn">Heatmap View</button>
49+
50+
<div style="display:flex; flex-direction:column; width: 150px;">
51+
<input type="range" id="threshold" min="0" max="100" value="20">
52+
<span class="label">Difference Sensitivity</span>
53+
</div>
54+
55+
<select id="speed"><option value="0.25">0.25x</option><option value="0.5">0.5x</option><option value="1" selected>1.0x</option></select>
56+
<button id="reset" style="background:#444">Reset</button>
57+
</div>
58+
59+
<div class="analytics-panel">
60+
<div class="card">
61+
<span class="label">Live PSNR History (Higher is Better)</span>
62+
<canvas id="psnr-graph"></canvas>
63+
</div>
64+
<div class="card" style="text-align: center;">
65+
<span class="label">Current Quality</span>
66+
<div id="psnr-val" class="psnr-value">-- dB</div>
67+
<div id="psnr-status" style="font-size: 12px; color: #888;">Standby...</div>
68+
</div>
69+
</div>
70+
</div>
71+
72+
<script>
73+
document.addEventListener('DOMContentLoaded', () => {
74+
const v1 = document.getElementById('v1'), v2 = document.getElementById('v2');
75+
const viewer = document.getElementById('viewer'), w2 = document.getElementById('w2');
76+
const canvas = document.getElementById('diff-canvas'), ctx = canvas.getContext('2d', {willReadFrequently: true});
77+
const gCanvas = document.getElementById('psnr-graph'), gCtx = gCanvas.getContext('2d');
78+
const psnrText = document.getElementById('psnr-val');
79+
80+
let isHeatmap = false;
81+
let psnrHistory = new Array(100).fill(0);
82+
const hiddenC = new OffscreenCanvas(1,1);
83+
const hiddenCtx = hiddenC.getContext('2d', {willReadFrequently: true});
84+
85+
// 1. Slider Logic
86+
viewer.onmousemove = (e) => {
87+
const rect = viewer.getBoundingClientRect();
88+
w2.style.width = ((e.clientX - rect.left) / rect.width * 100) + '%';
89+
};
90+
91+
// 2. Core Comparison Engine (Heatmap + PSNR)
92+
function processFrames() {
93+
if (v1.videoWidth === 0) return;
94+
if (canvas.width !== v1.videoWidth) {
95+
canvas.width = hiddenC.width = v1.videoWidth;
96+
canvas.height = hiddenC.height = v1.videoHeight;
97+
gCanvas.width = gCanvas.clientWidth;
98+
gCanvas.height = gCanvas.clientHeight;
99+
}
100+
101+
hiddenCtx.drawImage(v1, 0, 0);
102+
const d1 = hiddenCtx.getImageData(0, 0, canvas.width, canvas.height).data;
103+
hiddenCtx.drawImage(v2, 0, 0);
104+
const d2 = hiddenCtx.getImageData(0, 0, canvas.width, canvas.height).data;
105+
106+
let mse = 0;
107+
const out = ctx.createImageData(canvas.width, canvas.height);
108+
const dOut = out.data;
109+
const t = document.getElementById('threshold').value;
110+
111+
for (let i = 0; i < d1.length; i += 4) {
112+
const rE = d1[i] - d2[i], gE = d1[i+1] - d2[i+1], bE = d1[i+2] - d2[i+2];
113+
mse += (rE*rE + gE*gE + bE*bE);
114+
115+
if (isHeatmap) {
116+
const diff = Math.abs(rE) + Math.abs(gE) + Math.abs(bE);
117+
if (diff > t) { dOut[i]=255; dOut[i+1]=0; dOut[i+2]=150; dOut[i+3]=200; }
118+
}
119+
}
120+
121+
// Finalize Heatmap
122+
if (isHeatmap) ctx.putImageData(out, 0, 0);
123+
124+
// Finalize PSNR
125+
mse = mse / (canvas.width * canvas.height * 3);
126+
const psnr = mse === 0 ? 100 : 10 * Math.log10((255 * 255) / mse);
127+
updatePSNR(psnr);
128+
}
129+
130+
function updatePSNR(val) {
131+
psnrHistory.push(val);
132+
psnrHistory.shift();
133+
psnrText.textContent = val >= 100 ? "∞ dB" : val.toFixed(2) + " dB";
134+
135+
// Render Graph
136+
gCtx.clearRect(0,0, gCanvas.width, gCanvas.height);
137+
gCtx.strokeStyle = "#9c27b0";
138+
gCtx.lineWidth = 2;
139+
gCtx.beginPath();
140+
for(let i=0; i<psnrHistory.length; i++) {
141+
const x = (i / (psnrHistory.length-1)) * gCanvas.width;
142+
const y = gCanvas.height - (psnrHistory[i] / 60 * gCanvas.height); // Scale 0-60dB
143+
i === 0 ? gCtx.moveTo(x, y) : gCtx.lineTo(x, y);
144+
}
145+
gCtx.stroke();
146+
}
147+
148+
// 3. UI Hooks
149+
const loop = () => {
150+
if (!v1.paused || isHeatmap) {
151+
processFrames();
152+
setTimeout(() => requestAnimationFrame(loop), 100); // 10fps analysis for performance
153+
}
154+
};
155+
156+
document.getElementById('play').onclick = () => {
157+
if (v1.paused) { v1.play(); v2.play(); loop(); }
158+
else { v1.pause(); v2.pause(); }
159+
};
160+
161+
document.getElementById('heatmapBtn').onclick = (e) => {
162+
isHeatmap = !isHeatmap;
163+
e.target.classList.toggle('active');
164+
canvas.style.display = isHeatmap ? 'block' : 'none';
165+
processFrames();
166+
};
167+
168+
document.getElementById('next').onclick = () => {
169+
v1.currentTime += 1/30; v2.currentTime = v1.currentTime;
170+
setTimeout(processFrames, 150);
171+
};
172+
173+
document.getElementById('reset').onclick = () => {
174+
v1.currentTime = v2.currentTime = 0;
175+
psnrHistory.fill(0);
176+
processFrames();
177+
};
178+
179+
document.getElementById('speed').onchange = (e) => v1.playbackRate = v2.playbackRate = e.target.value;
180+
});
181+
</script>
182+
</body>
183+
</html>

enc_BBB_Unscrambled.mp4

81.4 MB
Binary file not shown.

glacier_og_image_4503x3002.jpg

1.74 MB
Loading

0 commit comments

Comments
 (0)