Skip to content

Commit

Permalink
Implemented Z85 Encoding
Browse files Browse the repository at this point in the history
The implementation follows the ZeroMQ RFC spec:
http://rfc.zeromq.org/spec:32/Z85/

Test Plan:
Added test cases for Z85-encoding from the following sources:
 * The RFC itself
 * The PyZMQ Implementation.
 * The Base85 encoding example of Thomas Hobbes' "Leviathan".
All test cases pass.
  • Loading branch information
manzdagratiano committed Aug 13, 2016
1 parent a1d5c46 commit 4cd1985
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ core_closure.js
node_modules/
doc/
doc_private/

# Swap Files #
# ########## #
~*
*.swp
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ TEST_SCRIPTS= $(TEST_COMMON) \
test/sha512_vectors_long_messages.js test/sha512_test_long_messages.js \
test/sha512_huge_test_messages.js test/sha512_huge_test.js \
test/sha512_test_brute_force.js \
test/srp_vectors.js test/srp_test.js
test/srp_vectors.js test/srp_test.js \
test/z85_vectors.js test/z85_test.js

# Run all tests in node.js.
test: sjcl.js $(TEST_SCRIPTS) test/run_tests_node.js
Expand Down
5 changes: 3 additions & 2 deletions configure
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ use strict;

my ($arg, $i, $j, $targ);

my @targets = qw/sjcl aes bitArray codecString codecHex codecBase32 codecBase64 codecBytes sha256 sha512 sha1 ccm ctr cbc ocb2 ocb2progressive gcm hmac pbkdf2 scrypt random convenience bn ecc srp ccmArrayBuffer codecArrayBuffer ripemd160/;
my @targets = qw/sjcl aes bitArray codecString codecHex codecBase32 codecBase64 codecBytes codecZ85 sha256 sha512 sha1 ccm ctr cbc ocb2 ocb2progressive gcm hmac pbkdf2 scrypt random convenience bn ecc srp ccmArrayBuffer codecArrayBuffer ripemd160/;
my %deps = ('aes'=>'sjcl',
'bitArray'=>'sjcl',
'codecString'=>'bitArray',
'codecHex'=>'bitArray',
'codecBase64'=>'bitArray',
'codecBase32'=>'bitArray',
'codecBytes'=>'bitArray',
'codecZ85'=>'bitArray',
'sha256'=>'codecString',
'sha512'=>'codecString',
'sha1'=>'codecString',
Expand Down Expand Up @@ -39,7 +40,7 @@ my $exported = 1;
my %enabled = ();
$enabled{$_} = 0 foreach (@targets);

# by default, all but codecBytes, srp, bn
# by default, all but codecBytes, codecZ85, srp, bn
$enabled{$_} = 1 foreach (qw/aes bitArray codecString codecHex codecBase32 codecBase64 sha256 ccm ocb2 gcm hmac pbkdf2 random convenience/);

# argument parsing
Expand Down
134 changes: 134 additions & 0 deletions core/codecZ85.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* @fileOverview Z85 codec implementation.
* @summary Z85 encoding is the "string-safe" ZeroMQ variant of Base85
* encoding. The character set avoids the single and double
* quotes and the backslash, making the encoded string
* safe to embed in command-line interpreters.
* Base85 uses 5 characters to encode 4 bytes of data,
* making the encoded size 1/4 larger than the original;
* this also makes it more efficient than uuencode or Base64,
* which uses 4 characters to encode 3 bytes of data, making
* the encoded size 1/3 larger than the original.
*
* @author Manjul Apratim
*/

/**
* Z85 encoding/decoding
* http://rfc.zeromq.org/spec:32/Z85/
* @namespace
*/
sjcl.codec.z85 = {
/** The Z85 alphabet.
* @private
*/
_chars: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#",

/** The decoder map (maps base 85 to base 256).
* @private
*/
_byteMap: [
0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00,
0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47,
0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00,
0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00
],

/**
* @summary Method to convert a bitArray to a Z85-encoded string.
* The bits represented by the array MUST be multiples of 4 bytes.
* @param {bitArray} arr - The input bitArray.
* @return {string} The Z85-encoded string.
*/
fromBits: function (arr) {
// Sanity checks
if (!arr) {
return null;
}
// Check we have multiples of 4 bytes (32 bits)
if (0 !== sjcl.bitArray.bitLength(arr) % 32) {
throw new sjcl.exception.invalid("Invalid bitArray length!");
}

var out = "", c = sjcl.codec.z85._chars;

// Convert sequences of 4 bytes (each word) to 5 characters.
for (var i = 0; i < arr.length; ++i) {
// Each element in the bitArray is a 32-bit (4-byte) word.
var word = arr[i];
var value = 0;
for (var j = 0; j < 4; ++j) {
// Extract each successive byte from the word from the left.
var byteChunk = (word >>> 8*(4 - j - 1)) & 0xFF;
// Accumulate in base-256
value = value*256 + byteChunk;
}
var divisor = 85*85*85*85;
while (divisor) {
out += c.charAt(Math.floor(value/divisor) % 85);
divisor = Math.floor(divisor/85);
}
}

// Sanity check - each 4-bytes (1 word) should yield 5 characters.
var encodedSize = arr.length*5;
if (out.length !== encodedSize) {
throw new sjcl.exception.invalid("Bad Z85 conversion!");
}
return out;
},

/**
* @summary Method to convert a Z85-encoded string to a bitArray.
* The length of the string MUST be a multiple of 5
* (else it is not a valid Z85 string).
* @param {string} str - A valid Z85-encoded string.
* @return {bitArray} The decoded data represented as a bitArray.
*/
toBits: function(str) {
// Sanity check
if (!str) {
return [];
}
// Accept only strings bounded to 5 bytes
if (0 !== str.length % 5) {
throw new sjcl.exception.invalid("Invalid Z85 string!");
}

var out = [], value = 0, byteMap = sjcl.codec.z85._byteMap;
var word = 0, wordSize = 0;
for (var i = 0; i < str.length;) {
// Accumulate value in base 85.
value = value * 85 + byteMap[str[i++].charCodeAt(0) - 32];
if (0 === i % 5) {
// Output value in base-256
var divisor = 256*256*256;
while (divisor) {
// The following is equivalent to a left shift by 8 bits
// followed by OR-ing; however, left shift may cause sign problems
// due to 2's complement interpretation,
// and we're operating on unsigned values.
word = (word * Math.pow(2, 8)) + (Math.floor(value/divisor) % 256);
++wordSize;
// If 4 bytes have been acumulated, push the word into the bitArray.
if (4 === wordSize) {
out.push(word);
word = 0, wordSize = 0;
}
divisor = Math.floor(divisor/256);
}
value = 0;
}
}

return out;
}
}
2 changes: 2 additions & 0 deletions test/run_tests_browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ function testCore(coreName, cb) {
"sha512_huge_test.js",
"srp_test.js",
"srp_vectors.js",
"z85_test.js",
"z85_vectors.js",
], i;

for (i=1; i<testFiles.length; i++) {
Expand Down
19 changes: 19 additions & 0 deletions test/z85_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
new sjcl.test.TestCase("Z85-encoding from rfc.zeromq.org/spec:32/Z85/", function (cb) {
if (!sjcl.codec.z85) {
this.unimplemented();
cb && cb();
return;
}

var i, kat=sjcl.test.vector.z85, p=0, f=0;
for (i=0; i<kat.length; i++) {
// Test encoding
this.require(sjcl.codec.z85.fromBits(sjcl.codec.hex.toBits(kat[i][0])) ==
kat[i][1], i);
// Test decoding
this.require(sjcl.codec.hex.fromBits(sjcl.codec.z85.toBits(kat[i][1])) ==
kat[i][0], i);
}

cb && cb();
});
39 changes: 39 additions & 0 deletions test/z85_vectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Test vector for Z85
*/
sjcl.test.vector.z85 =
[
/**
* Test case from http://rfc.zeromq.org/spec:32/Z85/:
* | 0x86 | 0x4F | 0xD2 | 0x6F | 0xB5 | 0x59 | 0xF7 | 0x5B | encodes as
* | H | e | l | l | o | W | o | r | l | d |
*/
["864fd26fb559f75b", "HelloWorld"],
/**
* Sanity tests:
*/
["00000000", "00000"],
["ffffffff", "%nSc0"],
/**
* Test cases from PyZMQ (https://github.com/zeromq/pyzmq):
*/
["bb88471d65e2659b30c55a5321cebb5aab2b70a398645c26dca2b2fcb43fc518",
"Yne@$w-vo<fVvi]a<NY6T1ed:M$fCG*[IaLV{hID"],
["7bb864b489afa3671fbe69101f94b38972f24816dfb01b51656b3fec8dfd0888",
"D:)Q[IlAW!ahhC2ac:9*A}h:p?([4%wOTJ%JR%cs"],
["54fcba24e93249969316fb617c872bb0c1d1ff14800427c594cbfacf1bc2d652",
"rq:rM>}U?@Lns47E1%kR.o@n%FcmmsL/@{H8]yf7"],
["8e0bdd697628b91d8f245587ee95c5b04d48963f79259877b49cd9063aead3b7",
"JTKVSB%%)wK0E.X)V>+}o?pNmC{O&4W4b!Ni{Lh6"],
/**
* Thomas Hobbes' "Leviathan", Part I, Chapter VI:
* (The "classic" Base85 example, also the second Wikipedia logo):
*
* "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure"
*
* The period at the end of the sentence has been omitted,
* since keeping it requires padding the Hex string to align to 4-bytes.
*/
["4d616e2069732064697374696e677569736865642c206e6f74206f6e6c792062792068697320726561736f6e2c2062757420627920746869732073696e67756c61722070617373696f6e2066726f6d206f7468657220616e696d616c732c2077686963682069732061206c757374206f6620746865206d696e642c20746861742062792061207065727365766572616e6365206f662064656c6967687420696e2074686520636f6e74696e75656420616e6420696e6465666174696761626c652067656e65726174696f6e206f66206b6e6f776c656467652c2065786365656473207468652073686f727420766568656d656e6365206f6620616e79206361726e616c20706c656173757265",
"o<}]Zx(+zcx(!xgzFa9aB7/b}efF?GBrCHty<vdjC{3^mB0bHmvrlv8efFzABrC4raARphB0bKrzFa9dvr9GfvrlH7z/cXfA=k!qz//V7AV!!dx(do{B1wCTxLy%&azC)tvixxeB95Kyw/#hewGU&7zE+pvBzb98ayYQsvixJ2A=U/nwPzi%v}u^3w/$R}y?WJ}BrCpnaARpday/tcBzkSnwN(](zE:(7zE^r<vrui@vpB4:azkn6wPzj3x(v(iz!pbczF%-nwN]B+efFIGv}xjZB0bNrwGV5cz/P}xC4Ct#zdNP{wGU]6ayPekay!&2zEEu7Abo8]B9hIm"]
];

0 comments on commit 4cd1985

Please sign in to comment.