Encoding Plaintext Using Morse Code

Published:
This began as a thought experiment while waiting for patches to install on my system.

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:

11011111010010011100111001101001011010000000011011100100100010000010010111010000


While we find this beautiful, it hard to fathom. Let's add some meaning to the (apparent) chaos:

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)

Open in new window


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):

	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

Open in new window


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:
<script type="text/javascript" src="morse_code.js"></script>

Open in new window

That will make the X_MORSE object available.

To encode some plaintext, create a JSON object and submit it:
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

Open in new window

To unencode, we can use the same object, but we have to move things around a bit:
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

Open in new window

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.

/*
                       ######################################
                      
                       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  */
                      	};
                      // ------------------------------------
                      })();

Open in new window

0
5,041 Views

Comments (1)

Top Expert 2007

Author

Commented:
There is a working example at

http://www.over-engineering.com/test_morse_code.htm

should anyone be interested...

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.