v1.0.0: An ASN.1 packer in <100 lines of Vanilla JavaScript, part of the BlueCrypt suite

This commit is contained in:
AJ ONeal 2018-11-28 02:32:25 -07:00
commit d68230ce10
4 changed files with 313 additions and 0 deletions

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# Bluecrypt ASN.1 Packer
An ASN.1 builder in less than 100 lines of Vanilla JavaScript,
part of the Bluecrypt suite.
<br>
<small>(< 150 with newlines and comments)</small>
# Features
| <100 lines of code | <1k gzipped | 1.8k minified | 3.3k with comments |
* [x] Complete ASN.1 packer
* [x] Parses x.509 certificates
* [x] PEM (base64-encoded DER)
* [x] VanillaJS, Zero Dependencies
* [x] Browsers (even old ones)
* [x] Online [Demo](https://coolaj86.com/demos/asn1-packer/)
* [ ] Node.js (built, publishing soon)
# Demo
<https://coolaj86.com/demos/asn1-packer/>
<img border="1" src="https://i.imgur.com/phGeuMw.png" />
# Usage
```html
<script src="https://git.coolaj86.com/coolaj86/asn1-packer.js/raw/branch/master/asn1-packer.js"></script>
```
```js
'use strict';
var ASN1 = window.ASN1 // 62 lines
var Enc = window.Enc // 27 lines
var PEM = window.PEM // 6 lines
var arr = [
0x30,
[
[ 0x30, [ [ 0x06, "2a8648ce3d0201" ], [ 0x06, "2a8648ce3d030107" ] ] ],
[ 0x03, "04213d5258bc6c69c3e2139675ea3928a409fcffef39acc8e0c82a24ae78c37ede98fd89c0e00e74c997bb0a716ca9e0dca673dbb9b3fa72962255c9debcd218ca" ]
]
];
var hex = ASN1.pack(arr);
var buf = Enc.hexToBuf(hex);
var pem = PEM.packBlock({ type: "PUBLIC KEY", bytes: buf });
console.log(pem);
```
```
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85
rMjgyCokrnjDft6Y/YnA4A50yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
-----END PUBLIC KEY-----
```
### Simple Packing
All types are handled by the same rules except for Integer (0x02)
and Bit String (0x03), which require special padding.
This is sufficient for RSA and EC keypairs, and CSRs.
I don't know of any format for which it is not sufficient,
but if you find one I'd like to know about it.
### Zero Dependencies
> A little copying is better than a little dependency - Golang Proverbs by Rob Pike
Rather than requiring hundreds (or thousands) of lines of dependencies,
this library takes the approach of including from other libraries in its suite
to produce a small, focused file that does exactly what it needs to do.
# Legal
[Bluecrypt VanillaJS ASN.1 Packer](https://git.coolaj86.com/coolaj86/asn1-packer.js) |
MPL-2.0

127
asn1-packer.js Normal file
View File

@ -0,0 +1,127 @@
;(function (exports) {
'use strict';
if (!exports.ASN1) { exports.ASN1 = {}; }
if (!exports.Enc) { exports.Enc = {}; }
if (!exports.PEM) { exports.PEM = {}; }
var ASN1 = exports.ASN1;
var Enc = exports.Enc;
var PEM = exports.PEM;
//
// Packer
//
// Almost every ASN.1 type that's important for CSR
// can be represented generically with only a few rules.
exports.ASN1 = function ASN1(/*type, hexstrings...*/) {
var args = Array.prototype.slice.call(arguments);
var typ = args.shift();
var str = args.join('').replace(/\s+/g, '').toLowerCase();
var len = (str.length/2);
var lenlen = 0;
var hex = typ;
// We can't have an odd number of hex chars
if (len !== Math.round(len)) {
throw new Error("invalid hex");
}
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
// The second byte is either the size of the value, or the size of its size
// 1. If the second byte is < 0x80 (128) it is considered the size
// 2. If it is > 0x80 then it describes the number of bytes of the size
// ex: 0x82 means the next 2 bytes describe the size of the value
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
if (len > 127) {
lenlen += 1;
while (len > 255) {
lenlen += 1;
len = len >> 8;
}
}
if (lenlen) { hex += Enc.numToHex(0x80 + lenlen); }
return hex + Enc.numToHex(str.length/2) + str;
};
// The Integer type has some special rules
ASN1.UInt = function UINT() {
var str = Array.prototype.slice.call(arguments).join('');
var first = parseInt(str.slice(0, 2), 16);
// If the first byte is 0x80 or greater, the number is considered negative
// Therefore we add a '00' prefix if the 0x80 bit is set
if (0x80 & first) { str = '00' + str; }
return ASN1('02', str);
};
// The Bit String type also has a special rule
ASN1.BitStr = function BITSTR() {
var str = Array.prototype.slice.call(arguments).join('');
// '00' is a mask of how many bits of the next byte to ignore
return ASN1('03', '00' + str);
};
ASN1.pack = function (arr) {
var typ = Enc.numToHex(arr[0]);
var str = '';
if (Array.isArray(arr[1])) {
arr[1].forEach(function (a) {
str += ASN1.pack(a);
});
} else if ('string' === typeof arr[1]) {
str = arr[1];
} else {
throw new Error("unexpected array");
}
if ('03' === typ) {
return ASN1.BitStr(str);
} else if ('02' === typ) {
return ASN1.UInt(str);
} else {
return ASN1(typ, str);
}
};
Object.keys(ASN1).forEach(function (k) {
exports.ASN1[k] = ASN1[k];
});
ASN1 = exports.ASN1;
PEM.packBlock = function (opts) {
// TODO allow for headers?
return '-----BEGIN ' + opts.type + '-----\n'
+ Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----'
;
};
Enc.bufToBase64 = function (u8) {
var bin = '';
u8.forEach(function (i) {
bin += String.fromCharCode(i);
});
return btoa(bin);
};
Enc.hexToBuf = function (hex) {
var arr = [];
hex.match(/.{2}/g).forEach(function (h) {
arr.push(parseInt(h, 16));
});
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
};
Enc.numToHex = function (d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
}
return d;
};
}('undefined' !== typeof window ? window : module.exports));

75
index.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<title>ASN.1 Packer - Bluecrypt</title>
<style>
textarea {
width: 42em;
height: 10em;
}
</style>
</head>
<body>
<h1>Bluecrypt ASN.1 Packer</h1>
<input type="text" class="js-type" placeholder="PEM header (i.e. PUBLIC KEY)" value="PUBLIC KEY">
<br>
<textarea class="js-input" placeholder="Paste a PEM here">[
48,
[
[
48,
[
[
6,
"2a8648ce3d0201"
],
[
6,
"2a8648ce3d030107"
]
]
],
[
3,
"04213d5258bc6c69c3e2139675ea3928a409fcffef39acc8e0c82a24ae78c37ede98fd89c0e00e74c997bb0a716ca9e0dca673dbb9b3fa72962255c9debcd218ca"
]
]
]</textarea>
<pre><code class="js-hex"> </code></pre>
<pre><code class="js-pem"> </code></pre>
<br>
<p>Made with <a href="https://git.coolaj86.com/coolaj86/asn1-packer/">asn1-packer.js</a></p>
<script src="./asn1-packer.js"></script>
<script>
var $input = document.querySelector('.js-input');
function convert() {
console.log('keyup');
var json;
try {
var typ = document.querySelector('.js-type').value;
var text = document.querySelector('.js-input').value;
var arr = JSON.parse(text);
var hex = ASN1.pack(arr);
var buf = Enc.hexToBuf(hex);
var pem = PEM.packBlock({ type: typ, bytes: buf });
document.querySelector('.js-hex').innerText = hex
.match(/.{2}/g).join(' ').match(/.{1,24}/g).join(' ').match(/.{1,50}/g).join('\n');
document.querySelector('.js-pem').innerText = pem;
} catch(e) {
pem = { error: { message: e.message } };
document.querySelector('.js-pem').innerText = JSON.stringify(pem);
}
}
$input.addEventListener('keyup', convert);
convert();
</script>
</body>
</html>

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "asn1-packer",
"version": "1.0.0",
"description": "An ASN.1 packer in less than 100 lines of Vanilla JavaScript, part of the Bluecrypt suite.",
"homepage": "https://git.coolaj86.com/coolaj86/asn1-packer.js",
"main": "asn1-packer.js",
"scripts": {
"prepare": "uglifyjs asn1-packer.js > asn1-packer.min.js"
},
"directories": {
"lib": "lib"
},
"repository": {
"type": "git",
"url": "https://git.coolaj86.com/coolaj86/asn1-packer.js"
},
"keywords": [
"zero-dependency",
"ASN.1",
"X.509",
"PEM",
"DER",
"asn1",
"x509",
"VanillaJS"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0"
}