Browse Source

Refactor to factor out random and string scrambling functions.

feature/save-game
Daniel Perelman 3 years ago
parent
commit
c14dd920b4
5 changed files with 192 additions and 159 deletions
  1. +3
    -1
      www/mastermind/cache.appcache
  2. +7
    -25
      www/mastermind/index.html
  3. +3
    -133
      www/mastermind/mastermind.js
  4. +31
    -0
      www/random.js
  5. +148
    -0
      www/scramblestring.js

+ 3
- 1
www/mastermind/cache.appcache View File

@@ -1,10 +1,12 @@
CACHE MANIFEST
# Last updated: 2019-02-13 10:28 PST
# Last updated: 2019-02-13 16:14 PST

CACHE:
index.html
main.css
mastermind.js
../random.js
../scramblestring.js
../wordlist/wordlist.js
../wordlist/en_50k.txt
../wordlist/typo.js


+ 7
- 25
www/mastermind/index.html View File

@@ -6,30 +6,12 @@
<title>Anagram Mastermind</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="main.css" />
<script src="../random.js"></script>
<script src="../scramblestring.js"></script>
<script type="text/javascript" src="../wordlist/typo.js"></script>
<script src="../wordlist/wordlist.js"></script>
<script src="./mastermind.js"></script>
<script >
// From https://stackoverflow.com/a/2450976
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;

// While there remain elements to shuffle...
while (0 !== currentIndex) {

// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;

// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}

return array;
}

function clearElement(element) {
// From https://stackoverflow.com/a/3955238
while (element.firstChild) {
@@ -450,9 +432,7 @@
this.game = game;
var ui = this;

game.getFragment('A')
.catch(_ => game.getFragment('B'))
.then(fragment => {
scrambleString(game.getFragment(), ['A', 'B']).then(fragment => {
document.getElementById('permalink').href = '#' + fragment;
document.getElementById('permalink_input').value
= window.location.href.split('#')[0] + '#' + fragment;
@@ -529,8 +509,10 @@
topWordsPromise
.then(_ => {
let fragment = window.location.hash.substring(1);
history.pushState("", document.title, window.location.pathname + window.location.search);
return Mastermind.fromFragment(fragment);
return unscrambleString(fragment).then(unscrambled => {
history.pushState("", document.title, window.location.pathname + window.location.search);
return Mastermind.fromFragment(unscrambled);
});
})
.then(game => new MastermindUI(game)));
}


+ 3
- 133
www/mastermind/mastermind.js View File

@@ -1,111 +1,5 @@
'use strict';

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
function getRandomLetter() {
return (10 + getRandomInt(26)).toString(36);
}

// From https://stackoverflow.com/a/34310051
function toHexString(buffer) {
let byteArray = new Uint8Array(buffer);
return Array.from(byteArray, function(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
function fromHexString(str) {
let buffer = new ArrayBuffer(str.length / 2);
let byteArray = new Uint8Array(buffer);
var i = 0;
for (var idx = 0; idx < str.length; idx += 2) {
let hex = str.substring(idx, idx+2);
byteArray[i++] = parseInt(hex, 16);
}
return buffer;
}

// "Encrypt" a string and include the key just as a way of scrambling it
// to make it hard to read for a human.
async function getDefaultKeyAndIv() {
let key = await window.crypto.subtle.importKey('raw', new ArrayBuffer(32),
{ name: "AES-GCM" }, false, ['encrypt', 'decrypt']);
let iv = new Uint8Array(12);
return [key, iv];
}
async function encryptStringNoKey(plaintext) {
let encoded = new TextEncoder().encode(plaintext);
let [key, iv] = await getDefaultKeyAndIv();
let ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
key,
encoded
);
return toHexString(ciphertext);
}
async function decryptStringNoKey(ciphertextStr) {
let [key, iv] = await getDefaultKeyAndIv();
let ciphertext = fromHexString(ciphertextStr);

let decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
key,
ciphertext
);
let plaintext = new TextDecoder().decode(decrypted);
return plaintext;
}
async function encryptString(plaintext) {
let encoded = new TextEncoder().encode(plaintext);
let key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
let iv = await window.crypto.getRandomValues(new Uint8Array(12));
let ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
key,
encoded
);
let keyBuffer = await window.crypto.subtle.exportKey('raw', key);
return toHexString(keyBuffer) + ','
+ toHexString(iv) + ','
+ toHexString(ciphertext);
}
async function decryptString(str) {
let [keyStr, ivStr, ciphertextStr] = str.split(',');
let keyBuffer = fromHexString(keyStr);
let iv = fromHexString(ivStr);
let ciphertext = fromHexString(ciphertextStr);

let key = await window.crypto.subtle.importKey('raw', keyBuffer,
{ name: "AES-GCM" }, false, ['decrypt']);
let decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
key,
ciphertext
);
let plaintext = new TextDecoder().decode(decrypted);
return plaintext;
}

const LetterGuessResult = Object.freeze({
MATCH: Symbol("match"),
UNUSED: Symbol("unused"),
@@ -197,15 +91,7 @@ class Mastermind {

static fromFragment(fragment) {
try {
if (fragment[0] == 'B') {
return this.fromFragment(atob(fragment.substring(1)));
} else if (fragment[0] == 'E') {
return decryptString(fragment.substring(1))
.then(decrypted => this.fromFragment(decrypted));
} else if (fragment[0] == 'A') {
return decryptStringNoKey(fragment.substring(1))
.then(decrypted => this.fromFragment(decrypted));
} else if (!fragment.includes(',')) {
if (!fragment.includes(',')) {
return null;
}

@@ -217,7 +103,7 @@ class Mastermind {
if (!isValidWord(word)) return null;
if (word.length < 2) return null;

return Promise.resolve(new Mastermind(word, availableLetters));
return new Mastermind(word, availableLetters);
}
catch(e) {
return null;
@@ -225,23 +111,7 @@ class Mastermind {
}

getFragment(kind) {
if (kind[0] == 'B') {
return this.getFragment(kind.substring(1))
.then(str => 'B' + btoa(str));
} else if (kind[0] == 'E') {
return this.getFragment(kind.substring(1))
.then(str => encryptString(str))
.then(enc => 'E' + enc);
} else if (kind[0] == 'A') {
return this.getFragment(kind.substring(1))
.then(str => encryptStringNoKey(str))
.then(enc => 'A' + enc);
} else if(!kind) {
return Promise.resolve(this.word + ','
+ this.availableLetters.sort().join(''));
} else {
throw new Exception("Unknown fragment kind: " + kind);
}
return this.word + ',' + this.availableLetters.sort().join('');
}

isValidGuess(guess) {


+ 31
- 0
www/random.js View File

@@ -0,0 +1,31 @@
'use strict';

/* Utilities for random numbers. */

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
function getRandomLetter() {
return (10 + getRandomInt(26)).toString(36);
}

// From https://stackoverflow.com/a/2450976
function shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex;

// While there remain elements to shuffle...
while (0 !== currentIndex) {

// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;

// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}

return array;
}

+ 148
- 0
www/scramblestring.js View File

@@ -0,0 +1,148 @@
'use strict';

/* Utilities for scrambling strings to obscure puzzle answers. Not
* intended to be secure. */

// From https://stackoverflow.com/a/34310051
function toHexString(buffer) {
let byteArray = new Uint8Array(buffer);
return Array.from(byteArray, function(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
function fromHexString(str) {
let buffer = new ArrayBuffer(str.length / 2);
let byteArray = new Uint8Array(buffer);
let i = 0;
for (let idx = 0; idx < str.length; idx += 2) {
let hex = str.substring(idx, idx+2);
byteArray[i++] = parseInt(hex, 16);
}
return buffer;
}

// "Encrypt" a string and include the key just as a way of scrambling it
// to make it hard to read for a human.
async function getDefaultKeyAndIv() {
let key = await window.crypto.subtle.importKey('raw', new ArrayBuffer(32),
{ name: "AES-GCM" }, false, ['encrypt', 'decrypt']);
let iv = new Uint8Array(12);
return [key, iv];
}
async function encryptStringNoKey(plaintext) {
let encoded = new TextEncoder().encode(plaintext);
let [key, iv] = await getDefaultKeyAndIv();
let ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
key,
encoded
);
return toHexString(ciphertext);
}
async function decryptStringNoKey(ciphertextStr) {
let [key, iv] = await getDefaultKeyAndIv();
let ciphertext = fromHexString(ciphertextStr);

let decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
key,
ciphertext
);
let plaintext = new TextDecoder().decode(decrypted);
return plaintext;
}
async function encryptString(plaintext) {
let encoded = new TextEncoder().encode(plaintext);
let key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
let iv = await window.crypto.getRandomValues(new Uint8Array(12));
let ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
key,
encoded
);
let keyBuffer = await window.crypto.subtle.exportKey('raw', key);
return toHexString(keyBuffer) + ','
+ toHexString(iv) + ','
+ toHexString(ciphertext);
}
async function decryptString(str) {
let [keyStr, ivStr, ciphertextStr] = str.split(',');
let keyBuffer = fromHexString(keyStr);
let iv = fromHexString(ivStr);
let ciphertext = fromHexString(ciphertextStr);

let key = await window.crypto.subtle.importKey('raw', keyBuffer,
{ name: "AES-GCM" }, false, ['decrypt']);
let decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
key,
ciphertext
);
let plaintext = new TextDecoder().decode(decrypted);
return plaintext;
}

async function _scrambleString(str, kind) {
if (kind[0] == 'B') {
return 'B' + btoa(await _scrambleString(str, kind.substring(1)));
} else if (kind[0] == 'E') {
return 'E' + await encryptString(
await _scrambleString(str, kind.substring(1)));
} else if (kind[0] == 'A') {
return 'A' + await encryptStringNoKey(
await _scrambleString(str, kind.substring(1)));
} else if(!kind) {
return str;
} else {
throw new Exception("Unknown scramble kind: " + kind);
}
}
function scrambleString(str, scrambleKinds) {
let res = null;
for (let i in scrambleKinds) {
let kind = scrambleKinds[i];
if (res === null) {
res = _scrambleString(str, kind);
} else {
res = res.catch(_ => _scrambleString(str, kind));
}
}
return res;
}
async function unscrambleString(str) {
try {
if (str[0] == 'B') {
return await unscrambleString(atob(str.substring(1)));
} else if (str[0] == 'E') {
return await unscrambleString(
await decryptString(str.substring(1)));
} else if (str[0] == 'A') {
return await unscrambleString(
await decryptStringNoKey(str.substring(1)));
} else {
return str;
}
}
catch(e) {
return null;
}
}

Loading…
Cancel
Save