The Morse Code is an effective means with which to change ASCII data into binary bits. We assume that a "dot" is a zero and a "dash" is a one. Translating an ASCII character into its binary value is accomplished with a lookup table.
It is not enough to convert from ASCII to binary; we must further convert from binary into hexadecimal. This reduces the length of the encoded string, and effectively hides the content of the original plaintext.
This is still not enough. We need a means of converting from a string of hexadecimal digits back to the original plaintext. This adds overhead to the final encoded string, but experience has shown this makes the encoding that much more formidable.
We could have used SHA-1 and AES for true encryption, but that would be overkill.
I am the only person I know who uses JavaScript on both the client and the server. All of the script for the Morse Encoder is written in JavaScript, so if you wish to use PHP, Perl or some other server-side language, you will have to translate it from JavaScript.
- 1
- How It Works
This discussion uses the name of my beloved cat, Phoebe, as the plaintext. We begin with the outcome of the encoding process.
It may seem odd to start with an encoded string, but this allows us to explain how we arrived at a given point. The complete code is included, but we will refrain from showing code during this segment.
Morse Encoding "Phoebe" results in:
DF 49 CE 69 68 06 E4 88 25 D0
Phoebe is hiding in there, but she is not readily visible. Let's find out why.
In order to make the components of the encoding visible, we convert from hexadecimal into binary:
11011111010010011100111001
While we find this beautiful, it hard to fathom. Let's add some meaning to the (apparent) chaos:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: | Checksum | Align bytes:7 | Type | Pad each character:0=no | | | | LSB Len:6 MSB Len:0 | | | | | [------------------------ Plaintext -----------------------] | | | | | | Len:4 Len:4 Len:3 Len:1 Len:4 Len:1 Align | +--------------+ | | | | | Ucase | Lcase | Lcase | Lcase | Lcase | Lcase Padding | +---------+ | | | | | | | | | | | | | | | | | | | +----+ | | | | | | | | P | | h | | o | | e | | b | | e | | |___ |___ |___ |___ |___ |__ | |___ |__ | |___ |__ | |___ |__ | |__ |__ | | |__ | |__ |__ | | |______ ___| 1101 1111 0100 1001 1100 111 0 0110 100 1 0110 100 0 0000 011 0 111 001 0 0 100 0 1000 001 0 0 1011101 0000 kkkk kkkk kkkk kkkk tttt aaa p oooo bbb c dddd bbb c dddd bbb c ddd bbb c d bbb c dddd bbb c d eeeeeee xxxx Where: k ... FCS-16 checksum (4 bytes) t ... Encryption type (always '1100') a ... # pad bytes (the number of binary digits must be a multiple of 4; this is the number of padding bits to ensure this) p ... Pad each character (0 = no, 1 = yes) *** This example does not pad the individual characters *** o ... Plaintext length (LSB) b ... Length of encoded character c ... Case (0 = lowercase, 1 = UPPERCASE) d ... Binary-encoded Morse code character (optionally padded to 7 chars) e ... Word boundary pad chars x ... Plaintext length (MSB) |
Combining all of the binary digits yields our binary string, shown here in 4-bit chunks:
1101 1111 0100 1001 1100 1110 0110 1001 0110 1000 0000 0110 1110 0100 1000 1000 0010 0101 1101 0000
Further converting this into hexadecimal, we return to the final result:
DF 49 CE 69 68 06 E4 88 25 D0
- 2
- Unravelling an Encoded String
We strip off the checksum and check the type. If it does not match, we complain and stop the decoding process.
We strip off the type and save the ALIGN and PAD indicators. We will call these, "A" and "P", respectively.
We save the least significant 4 bits (LSB) of the plaintext length (0110) and prepend the most significant 4 bits (MSB) of the encoded string (0000) to create an 8-bit value (0000_0110b = 06h = 6). We will call this value, "L".
We strip off the next 8 bytes and arrive at the length of the first Morse character. We save this value as "D".
The next bit indicates UPPER or lower case (1 or 0).
Next, we lookup the Morse character in our lookup table and return the ASCII character to which it maps. We apply the proper case using the case flag and add this character to the output string.
We bump through each succeeding character until we have reached the number of characters specified in "L", at which time we are through decoding.
- 3
- Once Again, From The Top
Now that we've seen the result of encoding, let's walk through the process from the beginning. We will use "Phoebe" as our plaintext.
We first determine the length of the plaintext and convert the value into a 8-bit binary equivalent. The length of "Phoebe" is 6, so we convert to binary and arrive at "0000_0110".
For those among you who do not grok binary, here are the numbers from 0 to 15 in decimal (d), hexadecimal (h) and binary (b):
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: | d h b - -- ---- 0 00 0000 1 01 0001 2 02 0010 3 03 0011 4 04 0100 5 05 0101 6 06 0110 - the length of "Phoebe" 7 07 0111 8 08 1000 9 09 1001 10 0A 1010 11 0B 1011 12 0C 1100 13 0D 1101 14 0E 1110 15 0F 1111 |
The binary values are stored in an array. We push the type indicator, "1100" into the array, follwed by a placeholder entry that will be updated later.
We next push the low-order bits of the binary length ("0110") into the array. The high-order bits ("0000") will be the final entry in the array.
Next, we find the Morse Code value for a character in the plaintext. The first one is "P", and the Morse Code equivalent is "0110". That this is the same binary value as the length of "Phoebe" is coincidental.
If the plaintext character is in upper case, we set a flag to "1", otherwise the flag is reset to "0".
We convert the length (number of bits) of the Morse Code into three binary digits. Why three digits? Because the maximum length of any single Morse Code item is seven, and seven can be represented in three bits: "111".
Now we push the coding information for this plaintext character into the array:
"010" + "1" + "0110" reulting in "01010110".
This equates to [Binary Length] + [Case Flag] + [Binary Morse Code] = [Encoding Information]
This information is pushed into the array, and we continue for each character of the plaintext.
- 4
- Cleaning Up
The array of binary values is converted into a string of some number of binary digits. We need to have a number of digits that is a multiple of eight. We pad the string with random binary digits until the length meets our requirements. When we convert from this binary nightmare to ASCII, we will need to know the number of bits used for padding, so we push the value into the arrray. In this case, the number of bits is seven, so we push "0111" into the placeholder array entry (entry # 1).
Finally, we terminate the string with the high-order bits of the plaintext length ("0000"). We push this value into the array.
We are finished with the endoding, so now we join the array into a string of binary digits, and then convert that into a string of hexadecimal characters..
- 5
- A Pointless Excercise
The next step is to prepend the hexadecimal value of the CRC of the output string to the output string.
This step is optional. I did not include the code to do this, so unless you have a burning desire to mimic my work, just skip this step.
Note that when un-encoding, we still expect the 16-bit value to be on the front of the encoded string (see the notes in the attached JavaScript during the encoding process for details).
- 6
- Now We Are Done
The encoding is complete. We return both the hexadecimal and binary values to the caller.
- 7
- Usage
Copy the code that follows into file "morse_code.js" and save it. Then, in your HTML file, add this line:
1: 2: | <script type="text/javascript" src="morse_code.js"></script> |
That will make the X_MORSE object available.
To encode some plaintext, create a JSON object and submit it:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
var job = {
'mode':'e',
'text':Enter your plaintext here' + '\t' + 'tabs are OK' + '\n' + 'and so are newlines',
'pad:'y'
}
var result = X_MORSE.code(job); // Encode our plaintext
alert(result.hex + '\t\n\n' + result.bin); // Show the results of encoding |
To unencode, we can use the same object, but we have to move things around a bit:
1: 2: 3: 4: 5: 6: 7: | job.mode = 'd'; // Decoding now job.text = result.hex; // We unencode the hex-coded string var result = X_MORSE.code(job); // Decode our encoded string alert(result.hex + '\t\n\n'); // Show the decoded plaintext |
And there you have it: encoding - and decoding - plaintext using Morse Code.
- 8
- Show Me the JavaScript
The Morse Encoder is an object. It exposes a few methods, but the bulk of it is private. It is beyond the scope of this article to delve into the structure of the Encoder; visit Douglas Crockford's website http://www.crockford.com/ for more information.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: |
/*
######################################
Encryption method based on morse code
Public methods
------------------------------------
code Encoder/Decoder
######################################
*/
var X_MORSE = (function() {
var CHR_VALID = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,?-=:;()"$\'^_@!*+~#&/ \\`%[]{}|<>' + '\n\t\r',
crc = '',
//
MORSE = [
/*
The first line is the Morse code for the ASCII character, where '0' = 'DIT' and '1' = 'DAH'.
The second line is the ASCII character (for reference only).
The third line is a composite number (currently unused): the length of the Morse code followed
by the decimal equivalent of the binary value of the Morse code.
*/
'01','1000','1010','100','0','0010','110','0000','00','0111','101','0100','11',
// A B C D E F G H I J K L M
// 21 48 410 34 10 42 36 40 20 47 35 44 23
'10','111','0110','1101','010','000','1','001','0001','011','1001','1011','1100',
// N O P Q R S T U V W X Y Z
// 22 37 46 413 32 30 11 31 41 33 49 411 412
'11111','01111','00111','00011','00001','00000','10000','11000','11100','11110',
// 0 1 2 3 4 5 6 7 8 9
// 531 515 57 53 51 50 516 524 528 530
'010101','110011','001100','100001','10001','111000','101010','10110','101101','010010','0001001',
// . , ? - = : ; ( ) " $
// 621 635 612 633 517 656 642 522 645 618 79
'011110','010100','001101','011010','1110','101011','01010','01000','000101','0100011','10010',
// ' ^ _ @ ! * + ~ # & /
// 630 620 613 626 414 643 510 58 63 735 518
'000000','0100000','00010','0101010','1000101','110101','1001001','0110110','1110110','1110111',
// (sp) \ ` % [ ] { } | <
// 60 732 52 742 769 653 773 754 7118 7119
'0001000','1100111','1100110','1100100'
// > \n \t \r
// 78 7103 7102 7100
],
//
exknob;
function get_random_binary_string(strlen) {
var retval = '';
while (retval.length < strlen) {
retval += (String(Math.floor(Math.random() * 10)) < 5) ? "0" : "1";
}
return retval;
}
/*
---------------------------------------
Min bits per char: 5 (not padded)
Min bits per char: 11 (padded)
Format of encrypted string (spaces for clarity):
**********************************
No nonsense, characters not padded
**********************************
Checksum Nonsense:0
| | Align bytes:2
| Type | | Pad each character:0
| | | | | LSB Len:6 MSB Len:0
| | | | | | [------------------------ Plaintext -----------------------] |
| | | | | | Len:4 Len:4 Len:3 Len:1 Len:4 Len:1 Align |
+--------------+ | | | | | | Ucase | Lcase | Lcase | Lcase | Lcase | Lcase Padding |
+---------+ | | | | | | | | | | | | | | | | | | | |
+----+ | | | | | | | | | P | | h | | o | | e | | b | | e | |
|___ |___ |___ |___ |__ | _|_ | |___ |__ | |___ |__ | |___ |__ | |__ |__ | | |__ | |__ |__ | | |______ ___|
1101 1111 0100 1001 110 0 111 0 0110 100 1 0110 100 0 0000 011 0 111 001 0 0 100 0 1000 001 0 0 1011101 0000
kkkk kkkk kkkk kkkk ttt n aaa p oooo bbb c dddd bbb c dddd bbb c ddd bbb c d bbb c dddd bbb c d ee xxxx
Where:
k ... FCS-16 checksum (4 bytes)
t ... Encryption type (always '110')
n ... Nonsense present (0 = no, 1 = yes)
a ... # pad bytes (to align word boundary)
p ... Pad each character (0 = no, 1 = yes)
o ... Plaintext length (LSB)
b ... Length of encoded character
c ... Case (0 = UPPER, 1 = lower)
d ... Binary-encoded Morse code character (optionally padded to 7 chars)
e ... Word boundary pad chars
x ... Plaintext length (MSB)
Combining all of the binary digits yields:
1101 1111 0100 1001 1100 1110 0110 1001 0110 1000 0000 0110 1110 0100 1000 1000 0010 0101 1101 0000
Or, to be more concise:
DF 49 CE 69 68 06 E4 88 25 D0
*/
function asc_to_morse(pfx, ptxt, bin_len, padded) {
var retval = [],
s = pfx + ptxt,
S = s.toUpperCase(),
m = '',
t = '',
u = '',
charIndex = 0,
//
exknob;
retval.push('1100'); // Nonsense always disabled
// Replaced after encoding is complete
retval.push('0000');
// Plaintext length (LSB)
retval.push(bin_len.substr(4));
for (var i = 0; i < S.length; i++) {
charIndex = CHR_VALID.indexOf(S.substr(i, 1));
// UPPER(1) or lower(0) case
u = (s.substr(i, 1) === S.substr(i, 1)) ? '1' : '0';
m = String(MORSE[charIndex]); // Binary Morse code equivalent
t = '000' + m.length.toString(2);
t = t.substr(t.length - 3) + u + m; // LEN + CASE + CHAR
if (padded === '1') {
if (t.length < 11) {
t += get_random_binary_string(11 - t.length);
}
}
retval.push(t);
}
// Pad 'n' bytes for the word boundary
charIndex = 8 - ((retval.join('') + '1111').length % 8);
if (charIndex !== 8) {
retval.push(get_random_binary_string(charIndex));
}
t = '000' + charIndex.toString(2) + padded;
// # of word align bits + padding indicator
retval[1] = t.substr(t.length - 4);
// Plaintext length (MSB)
retval.push(bin_len.substr(0, 4));
return retval.join('');
}
// ------------------------------------
function binary_to_hex(s) {
var retval = [];
var h = 0;
for (var i = 0; i < s.length; i += 4) {
h = parseInt(s.substr(i, 4), 2);
retval.push(h.toString(16));
}
return retval.join('');
}
// ------------------------------------
function hex_to_binary(h) {
var b = '';
var n = 0;
var retval = [];
for (var i = 0; i < h.length; i++) {
n = parseInt('0' + h.substr(i, 1), 16);
b = '0000' + n.toString(2);
retval.push(b.substr(b.length - 4));
}
return retval.join('');
}
// ------------------------------------
function morse_to_asc(b) {
var i = 0,
j = 0,
retval = [],
x = 0,
charIndex = 0,
pad_it = b.substr(7, 1),
pbyt = binary_to_hex('0' + b.substr(4, 3)), // Pad binary digits
plen = parseInt(pbyt, 16), // 10);
hlen = binary_to_hex(b.substr(b.length - 4) + b.substr(8, 4)), // Length, in hex
tlen = (parseInt(hlen.substr(0,1),16)*16) + parseInt(hlen.substr(1,1),16),
t = '',
o = 0,
jwok = 0,
//
exknob;
// Move to the encoded text
b = b.substr(12, b.length - (16 + plen));
while (b !== '') {
i = parseInt(b.substr(0, 3), 2);
t = b.substr(4, i);
// Morse code to ASCII
j = -1;
x = 0;
while (x < MORSE.length && j < 0) {
if (t === MORSE[x]) {
j = x;
}
x += 1;
}
if (j < 0) {
return '*** t = ' + t + ' - Invalid crypt text ***';
}
else {
t = CHR_VALID.substr(j, 1);
if (b.substr(3, 1) === '0') { t = t.toLowerCase(); }
retval.push(t);
if (pad_it === '1') {
b = b.substr(11);
}
else {
b = b.substr(i + 4);
}
}
}
return retval.join('');
}
// --------------------------------
function morse(how) {
//
// how.mode : "d" = decode, "e" = encode
// how.text : plaintext
// how.pad : "y" = pad each Morse char
// how.wok : "y" = add "nonsense" text to output
var retval = '',
p = 0,
bin='',
ipfx = '',
crc = '',
b = '',
x = '',
tlen = 0,
clen = CHR_VALID.length,
pad_it = '',
//
exknob;
switch (how.mode) {
case 'd':
crc = how.text.substr(0,4);
how.text = how.text.substr(4);
// If you did not calculate the CRC, skip this test
if (X_CRC.Fcs16(how.text) !== crc) {
return ['Invalid format - cannot decode'];
}
bin = hex_to_binary(how.text);
retval = morse_to_asc(bin);
break;
case 'e':
pad_it = (how.pad === 'y') ? '1' : '0';
// Create IV
tlen = how.text.length;
p = tlen;
var ilen = hex_to_binary(p.toString(16));
while (ilen.length < 8) { ilen = '0' + ilen; }
bin = asc_to_morse('', how.text, ilen, pad_it);
x = binary_to_hex(bin).toUpperCase();
// You may omit this step, but we still need
// a 16-bit hexadecimal value to take the place of the CRC value
// (e.g. FACE)
retval = X_CRC.Fcs16(x) + x; // crc is first 2 bytes
bin = hex_to_binary(retval);
break;
}
how.hex = retval; // Hexadecimal value
how.bin = bin; // Binary value
return how;
}
// ------------------------------------
// Exposed methods
return {
'bin_to_hex' :function(arg) { return binary_to_hex(arg); },
'hex_to_bin' :function(arg) { return hex_to_binary(arg); },
'code' :function(how) { return morse(how); },
//
'empty' :null /* dummy object closer */
};
// ------------------------------------
})(); |
http://www.over-engineerin
should anyone be interested...