Skip to content

Commit d03798d

Browse files
authored
Merge pull request #438 from gaimerI/main
Cryptography Extension
2 parents 8b579f1 + a1e8cda commit d03798d

File tree

3 files changed

+384
-0
lines changed

3 files changed

+384
-0
lines changed

src/lib/extensions.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,15 @@ export default [
531531
creatorAlias: "gaimerI17",
532532
notes: "Extension thumbnail made by Dillon."
533533
},
534+
{
535+
name: "Cryptography",
536+
description: "Hash, encrypt and verify values using various algorithms. Actual security not guaranteed",
537+
code: "gaimerI17/crypto.js",
538+
banner: "gaimerI17/crypto.avif",
539+
creator: "gaimerI",
540+
isGitHub: true,
541+
creatorAlias: "gaimerI17",
542+
},
534543
/* these extensions are completely dead as of now
535544
{
536545
name: "Online Captcha",
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
(function(Scratch) {
2+
'use strict';
3+
4+
function bufToHex(buffer) {
5+
const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer)
6+
: buffer instanceof Uint8Array ? buffer
7+
: new Uint8Array(buffer);
8+
return Array.prototype.map.call(bytes, b => b.toString(16).padStart(2, '0')).join('');
9+
}
10+
11+
function hexToBuf(hex) {
12+
const clean = String(hex).replace(/[^0-9a-f]/gi, '').toLowerCase();
13+
if (clean.length % 2 !== 0) throw new Error('Invalid hex string');
14+
const out = new Uint8Array(clean.length / 2);
15+
for (let i = 0; i < out.length; i++) {
16+
out[i] = parseInt(clean.substr(i * 2, 2), 16);
17+
}
18+
return out.buffer;
19+
}
20+
21+
const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIiB2aWV3Qm94PSIwLDAsMzAwLDMwMCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE1MCwwKSI+PGcgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIj48cGF0aCBkPSJNMTUwLDMwMHYtMzAwaDMwMHYzMDB6IiBmaWxsPSIjNjc2NzY3IiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMC41Ii8+PHBhdGggZD0iTTE1MCwzMDB2LTMwMGgzMDBsLTExOS4wOTA1LDE3Ny4wMzY2M3oiIGZpbGw9IiM0NTQ1NDUiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIwLjUiLz48cGF0aCBkPSJNMzExLjM3NzUzLDE0OS45OTk5OWMwLC0yMS40NjQyMSAxNy40MDAxOSwtMzguODY0MzkgMzguODY0NCwtMzguODY0MzljMjEuNDY0MjEsMCAzOC44NjQ0LDE3LjQwMDE4IDM4Ljg2NDQsMzguODY0NGMwLDIxLjQ2NDIxIC0xNy40MDAxOSwzOC44NjQ0IC0zOC44NjQzOSwzOC44NjQ0Yy0yMS40NjQyMSwwIC0zOC44NjQzOSwtMTcuNDAwMTggLTM4Ljg2NDM5LC0zOC44NjQzOXpNMzUwLjI0MTk0LDE3OS4xNDE4NGMxNi4wOTQ2LDAgMjkuMTQxODUsLTEzLjA0NzI2IDI5LjE0MTg1LC0yOS4xNDE4NWMwLC0xNi4wOTQ1OSAtMTMuMDQ3MjYsLTI5LjE0MTg1IC0yOS4xNDE4NSwtMjkuMTQxODVjLTE2LjA5NDYsMCAtMjkuMTQxODUsMTMuMDQ3MjYgLTI5LjE0MTg1LDI5LjE0MTg1YzAsMTYuMDk0NTkgMTMuMDQ3MjUsMjkuMTQxODUgMjkuMTQxODUsMjkuMTQxODV6IiBmaWxsPSIjZmY5OTAwIiBzdHJva2U9IiNmZjk5MDAiIHN0cm9rZS13aWR0aD0iNyIvPjxwYXRoIGQ9Ik0yMTAuODkzNjcsMTU2LjE3NzI4di0xMi4zNTQ1N2gxMDYuODk5NDR2MTIuMzU0NTd6IiBmaWxsPSIjZmY5OTAwIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMCIvPjxwYXRoIGQ9Ik0yMTAuODkzNjcsMTc5LjEwODM0di0zNS4yODU2M2gxMy4yMzIxMXYzNS4yODU2M3oiIGZpbGw9IiNmZjk5MDAiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTI0OS41MTY3MSwxNjcuODA1NXYtMjMuMjU2NDRoOC4wMTk0NnYyMy4yNTY0NHoiIGZpbGw9IiNmZjk5MDAiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIwIi8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6MTUwOjE1MC0tPg==';
22+
23+
class Extension {
24+
getInfo() {
25+
return {
26+
id: "gaimeriCryptoExtension",
27+
name: "Cryptography",
28+
color1: "#676767",
29+
color2: "#444444",
30+
color3: "#222222",
31+
menuIconURI,
32+
blocks: [
33+
{ blockType: Scratch.BlockType.LABEL, text: 'Random' },
34+
{
35+
opcode: 'randomUUID',
36+
text: 'random UUID',
37+
blockType: Scratch.BlockType.REPORTER,
38+
disableMonitor: true
39+
},
40+
{
41+
opcode: 'randomValues',
42+
text: '[SCALE] of [AMOUNT] random values',
43+
blockType: Scratch.BlockType.REPORTER,
44+
arguments: {
45+
AMOUNT: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 },
46+
SCALE: { type: Scratch.ArgumentType.STRING, menu: 'TYPED_ARRAYS', allowReporters: false }
47+
}
48+
},
49+
50+
'---',
51+
{ blockType: Scratch.BlockType.LABEL, text: 'Hashing' },
52+
{
53+
opcode: 'digest',
54+
text: 'hash [VALUE] with [ALGORITHM]',
55+
blockType: Scratch.BlockType.REPORTER,
56+
arguments: {
57+
VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: 'apple' },
58+
ALGORITHM: { type: Scratch.ArgumentType.STRING, menu: 'ALGORITHM' }
59+
}
60+
},
61+
62+
'---',
63+
{ blockType: Scratch.BlockType.LABEL, text: 'HMAC' },
64+
{
65+
opcode: 'generateSignKey',
66+
text: 'generate HMAC key',
67+
blockType: Scratch.BlockType.REPORTER,
68+
blockShape: Scratch.BlockShape.OCTAGONAL,
69+
disableMonitor: true
70+
},
71+
{
72+
opcode: 'signValue',
73+
text: 'HMAC sign [VALUE] with key [KEY]',
74+
blockType: Scratch.BlockType.REPORTER,
75+
arguments: {
76+
VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: 'apple' },
77+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
78+
}
79+
},
80+
{
81+
opcode: 'verifySignature',
82+
text: 'HMAC verify signature [SIGNATURE] of [VALUE] with key [KEY]',
83+
blockType: Scratch.BlockType.BOOLEAN,
84+
arguments: {
85+
SIGNATURE: { type: Scratch.ArgumentType.STRING, defaultValue: 'signature' },
86+
VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: 'apple' },
87+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
88+
}
89+
},
90+
91+
'---',
92+
{ blockType: Scratch.BlockType.LABEL, text: 'AES-GCM Encryption' },
93+
{
94+
opcode: 'generateAESKey',
95+
text: 'generate AES-GCM key (256-bit)',
96+
blockType: Scratch.BlockType.REPORTER,
97+
blockShape: Scratch.BlockShape.OCTAGONAL,
98+
disableMonitor: true
99+
},
100+
{
101+
opcode: 'encryptAES',
102+
text: 'encrypt [PLAINTEXT] with AES key [KEY]',
103+
blockType: Scratch.BlockType.REPORTER,
104+
arguments: {
105+
PLAINTEXT: { type: Scratch.ArgumentType.STRING, defaultValue: 'apple' },
106+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
107+
}
108+
},
109+
{
110+
opcode: 'decryptAES',
111+
text: 'decrypt [CIPHERTEXT] with AES key [KEY]',
112+
blockType: Scratch.BlockType.REPORTER,
113+
arguments: {
114+
CIPHERTEXT: { type: Scratch.ArgumentType.STRING, defaultValue: 'encrypted' },
115+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
116+
}
117+
},
118+
119+
'---',
120+
{ blockType: Scratch.BlockType.LABEL, text: 'RSA-PSS' },
121+
{
122+
opcode: 'generateRSAKey',
123+
text: 'generate RSA-PSS keypair',
124+
blockType: Scratch.BlockType.REPORTER,
125+
blockShape: Scratch.BlockShape.OCTAGONAL,
126+
disableMonitor: true
127+
},
128+
{
129+
opcode: 'rsaSign',
130+
text: 'RSA sign [VALUE] with private key [KEY]',
131+
blockType: Scratch.BlockType.REPORTER,
132+
arguments: {
133+
VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: 'apple' },
134+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
135+
}
136+
},
137+
{
138+
opcode: 'rsaVerify',
139+
text: 'RSA verify [SIGNATURE] of [VALUE] with public key [KEY]',
140+
blockType: Scratch.BlockType.BOOLEAN,
141+
arguments: {
142+
SIGNATURE: { type: Scratch.ArgumentType.STRING, defaultValue: 'signature' },
143+
VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: 'apple' },
144+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
145+
}
146+
},
147+
{
148+
opcode: 'rsaPublicKey',
149+
text: 'RSA public key from [KEY]',
150+
blockType: Scratch.BlockType.REPORTER,
151+
blockShape: Scratch.BlockShape.OCTAGONAL,
152+
arguments: {
153+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
154+
}
155+
},
156+
{
157+
opcode: 'rsaPrivateKey',
158+
text: 'RSA private key from [KEY]',
159+
blockType: Scratch.BlockType.REPORTER,
160+
blockShape: Scratch.BlockShape.OCTAGONAL,
161+
arguments: {
162+
KEY: { type: Scratch.ArgumentType.EMPTY, shape: Scratch.BlockShape.OCTAGONAL }
163+
}
164+
},
165+
],
166+
167+
menus: {
168+
ALGORITHM: { items: ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'] },
169+
TYPED_ARRAYS: ['Uint8Array','Uint16Array','Uint32Array','Int8Array','Int16Array','Int32Array']
170+
}
171+
};
172+
}
173+
174+
randomUUID() {
175+
return (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : '';
176+
}
177+
178+
randomValues(args) {
179+
const typedArrayConstructors = {
180+
Uint8Array: Uint8Array,
181+
Uint16Array: Uint16Array,
182+
Uint32Array: Uint32Array,
183+
Int8Array: Int8Array,
184+
Int16Array: Int16Array,
185+
Int32Array: Int32Array
186+
};
187+
188+
const TypedArrayConstructor = typedArrayConstructors[args.SCALE];
189+
if (!TypedArrayConstructor) throw new Error('Unsupported TypedArray type specified.');
190+
191+
let amt = Number(args.AMOUNT) || 0;
192+
amt = Math.max(0, Math.floor(amt));
193+
if (amt > 1000000) amt = 1000000;
194+
195+
const arr = new TypedArrayConstructor(amt);
196+
crypto.getRandomValues(arr);
197+
return JSON.stringify(Array.from(arr));
198+
}
199+
200+
async digest(args) {
201+
const algo = typeof args.ALGORITHM === 'string' ? args.ALGORITHM : 'SHA-256';
202+
const bytes = new TextEncoder().encode(String(args.VALUE));
203+
const hash = await crypto.subtle.digest(algo, bytes);
204+
return bufToHex(hash);
205+
}
206+
207+
async generateSignKey() {
208+
const key = await crypto.subtle.generateKey({
209+
name: 'HMAC',
210+
hash: { name: 'SHA-512' }
211+
}, true, ['sign', 'verify']);
212+
const jwk = await crypto.subtle.exportKey('jwk', key);
213+
return JSON.stringify(jwk);
214+
}
215+
216+
async signValue(args) {
217+
let jwk;
218+
try {
219+
jwk = JSON.parse(String(args.KEY));
220+
} catch {
221+
return '';
222+
}
223+
const key = await crypto.subtle.importKey('jwk', jwk, {
224+
name: 'HMAC',
225+
hash: { name: 'SHA-512' }
226+
}, false, ['sign']);
227+
const data = new TextEncoder().encode(String(args.VALUE));
228+
const sig = await crypto.subtle.sign('HMAC', key, data);
229+
return bufToHex(sig);
230+
}
231+
232+
async verifySignature(args) {
233+
let jwk;
234+
try {
235+
jwk = JSON.parse(String(args.KEY));
236+
} catch {
237+
return '';
238+
}
239+
const key = await crypto.subtle.importKey('jwk', jwk, {
240+
name: 'HMAC',
241+
hash: { name: 'SHA-512' }
242+
}, false, ['verify']);
243+
const data = new TextEncoder().encode(String(args.VALUE));
244+
try {
245+
return await crypto.subtle.verify('HMAC', key, hexToBuf(args.SIGNATURE), data);
246+
} catch (e) {
247+
return false;
248+
}
249+
}
250+
251+
async generateAESKey() {
252+
const key = await crypto.subtle.generateKey({
253+
name: 'AES-GCM',
254+
length: 256
255+
}, true, ['encrypt', 'decrypt']);
256+
const jwk = await crypto.subtle.exportKey('jwk', key);
257+
return JSON.stringify(jwk);
258+
}
259+
260+
async encryptAES(args) {
261+
let jwk;
262+
try {
263+
jwk = JSON.parse(String(args.KEY));
264+
} catch {
265+
return '';
266+
}
267+
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'AES-GCM' }, false, ['encrypt']);
268+
const iv = crypto.getRandomValues(new Uint8Array(12));
269+
const data = new TextEncoder().encode(String(args.PLAINTEXT));
270+
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
271+
return JSON.stringify({
272+
iv: bufToHex(iv),
273+
ct: bufToHex(ciphertext)
274+
});
275+
}
276+
277+
async decryptAES(args) {
278+
let obj;
279+
try {
280+
obj = JSON.parse(String(args.CIPHERTEXT));
281+
} catch {
282+
return '';
283+
}
284+
let jwk;
285+
try {
286+
jwk = JSON.parse(String(args.KEY));
287+
} catch {
288+
return '';
289+
}
290+
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'AES-GCM' }, false, ['decrypt']);
291+
try {
292+
const plaintextBuf = await crypto.subtle.decrypt({
293+
name: 'AES-GCM',
294+
iv: new Uint8Array(hexToBuf(String(obj.iv)))
295+
}, key, hexToBuf(String(obj.ct)));
296+
return new TextDecoder().decode(plaintextBuf);
297+
} catch {
298+
return '';
299+
}
300+
}
301+
302+
async generateRSAKey() {
303+
const keyPair = await crypto.subtle.generateKey({
304+
name: 'RSA-PSS',
305+
modulusLength: 2048,
306+
publicExponent: new Uint8Array([1, 0, 1]),
307+
hash: { name: 'SHA-256' }
308+
}, true, ['sign', 'verify']);
309+
310+
const ret = {
311+
publicKey: await crypto.subtle.exportKey('jwk', keyPair.publicKey),
312+
privateKey: await crypto.subtle.exportKey('jwk', keyPair.privateKey)
313+
};
314+
return JSON.stringify(ret);
315+
}
316+
317+
async rsaSign(args) {
318+
let jwk;
319+
try {
320+
jwk = JSON.parse(String(args.KEY));
321+
} catch {
322+
return '';
323+
}
324+
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'RSA-PSS', hash: { name: 'SHA-256' } }, false, ['sign']);
325+
const data = new TextEncoder().encode(String(args.VALUE));
326+
const sig = await crypto.subtle.sign({ name: 'RSA-PSS', saltLength: 32 }, key, data);
327+
return bufToHex(sig);
328+
}
329+
330+
async rsaVerify(args) {
331+
let jwk;
332+
try {
333+
jwk = JSON.parse(String(args.KEY));
334+
} catch {
335+
return '';
336+
}
337+
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'RSA-PSS', hash: { name: 'SHA-256' } }, false, ['verify']);
338+
const data = new TextEncoder().encode(String(args.VALUE));
339+
try {
340+
return await crypto.subtle.verify({ name: 'RSA-PSS', saltLength: 32 }, key, hexToBuf(args.SIGNATURE), data);
341+
} catch {
342+
return false;
343+
}
344+
}
345+
346+
// these two key methods have more validation (i cried while making them)
347+
async rsaPublicKey(args) {
348+
if (typeof args.KEY !== 'string') return '';
349+
try {
350+
const key = JSON.parse(args.KEY);
351+
if (key && key.publicKey) {
352+
return JSON.stringify(key.publicKey);
353+
}
354+
return '';
355+
} catch (error) {
356+
return '';
357+
}
358+
}
359+
360+
async rsaPrivateKey(args) {
361+
if (typeof args.KEY !== 'string') return '';
362+
try {
363+
const key = JSON.parse(args.KEY);
364+
if (key && key.privateKey) {
365+
return JSON.stringify(key.privateKey);
366+
}
367+
return '';
368+
} catch (error) {
369+
return '';
370+
}
371+
}
372+
}
373+
374+
Scratch.extensions.register(new Extension());
375+
})(Scratch);
3.28 KB
Binary file not shown.

0 commit comments

Comments
 (0)