diff --git a/.gitignore b/.gitignore index 144585f..be8362d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.gz + # ---> Node # Logs logs diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7e5d770 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "bracketSpacing": true, + "printWidth": 80, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "none", + "useTabs": true +} diff --git a/README.md b/README.md index 772c13c..8240922 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,122 @@ -# pem.js +# @root/pem -Lightweight, Zero-Dependency, PEM (RFC 7468) encoder and decoder. \ No newline at end of file +Lightweight, Zero-Dependency PEM encoder and decoder. + +| ~300b gzipped +| ~650b minified +| ~1k full +| + +- [x] VanillaJS +- [x] Zero-Dependency +- [x] Universal Support + - [x] Node.js + - [x] Browsers + +# Support + +This library supports PEM, which is pretty boring on its own. + +Most likely you are also interested in some of the following: + +- [keypairs.js](https://git.rootprojects.org/root/keypairs.js) + - RSA + - EC / ECDSA +- [x509.js](https://git.rootprojects.org/root/x509.js) +- [asn1.js](https://git.rootprojects.org/root/asn1.js) + +# Usage + +- PEM.parseBlock(str) +- PEM.packBlock(options) + +Parsing + +```js +var PEM = require('@root/pem/parser'); + +var block = PEM.parseBlock( + '-----BEGIN Type-----\nSGVsbG8sIOS4lueVjCE=\n-----END Type-----\n' +); +``` + +```js +{ + bytes: `<48 65 6c 6c 6f 2c 20 e4 b8 96 e7 95 8c 21>`; +} +``` + +Packing + +```js +var PEM = require('@root/pem/packer'); + +var block = PEM.packBlock({ + type: 'Type', + // Buffer or Uint8Array or Array + bytes: [0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c, 0x21] +); +``` + +```txt +-----BEGIN Type----- +SGVsbG8sIOS4lueVjCE= +-----END Type----- +``` + +# Install + +## Node / Webpack + +```js +npm install -g @root/pem +``` + +## Browsers + +```html + +``` + +```html + +``` + +# A PEM Block + +A Block represents a PEM encoded structure. + +The encoded form is: + +```txt +-----BEGIN Type----- +Headers +base64-encoded Bytes +-----END Type----- +``` + +where Headers is a possibly empty sequence of Key: Value lines. + +(credit: https://golang.org/pkg/encoding/pem/) + +# PEM History + +PEM was introduced in 1993 via RFC 1421, but not formally +standardized until RFC 7468 in April of 2015. + +It has served as the _de facto_ standard for a variety of +DER-encoded X509 schemas of ASN.1 data for cryptographic +keys and certificates such as: + +- [x] PKCS#10 (Certificate Signing Request [CSR]) +- [x] X509 Certificate (fullchain.pem, site.crt) +- [x] PKIX (cert.pem, privkey.pem, priv.key) + - [x] PKCS#1 (RSA Public and Private Keys) + - [x] PKCS#8 (RSA and ECDSA Keypairs) +- [x] SEC#1 (ECDSARSA Public and Private Keys) + +# Legal + +MPL-2.0 | +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/browser/native.js b/browser/native.js new file mode 100644 index 0000000..6d41385 --- /dev/null +++ b/browser/native.js @@ -0,0 +1,23 @@ +'use strict'; + +// "A little copying is better than a little dependency" - Rob Pike +var Enc = module.exports; + +Enc.bufToBase64 = function(u8) { + var bin = ''; + // map is not part of u8 + u8.forEach(function(i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); +}; + +Enc.base64ToBuf = function(b64) { + return Uint8Array.from( + atob(b64) + .split('') + .map(function(ch) { + return ch.charCodeAt(0); + }) + ); +}; diff --git a/browser/parser.js b/browser/parser.js new file mode 100644 index 0000000..e27248f --- /dev/null +++ b/browser/parser.js @@ -0,0 +1,33 @@ +'use strict'; + +var Enc = require('@root/encoding/base64'); +var PEM = module.exports; + +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 + + '-----' + ); +}; + +// don't replace the full parseBlock, if it exists +PEM.parseBlock = + PEM.parseBlock || + function(str) { + var der = str + .split(/\n/) + .filter(function(line) { + return !/-----/.test(line); + }) + .join(''); + return { bytes: Enc.base64ToBuf(der) }; + }; diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..2624b3c --- /dev/null +++ b/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# TODO convert to JS +cat browser/native.js parser.js packer.js > all.tmp.js +sed -i '' '/use strict/d' all.tmp.js +sed -i '' '/require/d' all.tmp.js +sed -i '' '/exports/d' all.tmp.js +echo ';(function () {' > all.js +echo "'use strict';" >> all.js +echo "var PEM = window.PEM = {};" >> all.js +echo "var Enc = {};" >> all.js +cat all.tmp.js >> all.js +rm all.tmp.js +echo '}());' >> all.js + +mv all.js dist/pem.all.js +uglifyjs dist/pem.all.js > dist/pem.all.min.js diff --git a/dist/pem.all.js b/dist/pem.all.js new file mode 100644 index 0000000..4238650 --- /dev/null +++ b/dist/pem.all.js @@ -0,0 +1,56 @@ +;(function () { +'use strict'; +var PEM = window.PEM = {}; +var Enc = {}; + +// "A little copying is better than a little dependency" - Rob Pike + +Enc.bufToBase64 = function(u8) { + var bin = ''; + // map is not part of u8 + u8.forEach(function(i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); +}; + +Enc.base64ToBuf = function(b64) { + return Uint8Array.from( + atob(b64) + .split('') + .map(function(ch) { + return ch.charCodeAt(0); + }) + ); +}; + + + +PEM.parseBlock = function(str) { + var der = str + .split(/\n/) + .filter(function(line) { + return !/-----/.test(line); + }) + .join(''); + return { bytes: Enc.base64ToBuf(der) }; +}; + + + +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 + + '-----' + ); +}; +}()); diff --git a/dist/pem.all.min.js b/dist/pem.all.min.js new file mode 100644 index 0000000..bb4d0bc --- /dev/null +++ b/dist/pem.all.min.js @@ -0,0 +1 @@ +(function(){"use strict";var PEM=window.PEM={};var Enc={};Enc.bufToBase64=function(u8){var bin="";u8.forEach(function(i){bin+=String.fromCharCode(i)});return btoa(bin)};Enc.base64ToBuf=function(b64){return Uint8Array.from(atob(b64).split("").map(function(ch){return ch.charCodeAt(0)}))};PEM.parseBlock=function(str){var der=str.split(/\n/).filter(function(line){return!/-----/.test(line)}).join("");return{bytes:Enc.base64ToBuf(der)}};PEM.packBlock=function(opts){return"-----BEGIN "+opts.type+"-----\n"+Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join("\n")+"\n"+"-----END "+opts.type+"-----"}})(); diff --git a/index.js b/index.js new file mode 100644 index 0000000..46a3084 --- /dev/null +++ b/index.js @@ -0,0 +1,7 @@ +'use strict'; + +var PEM = require('./parser.js'); +var PEMPacker = require('./packer.js'); +PEM.packBlock = PEMPacker.packBlock; + +module.exports = PEM; diff --git a/node/native.js b/node/native.js new file mode 100644 index 0000000..c2f7951 --- /dev/null +++ b/node/native.js @@ -0,0 +1,12 @@ +'use strict'; + +// "A little copying is better than a little dependency" - Rob Pike +var Enc = module.exports; + +Enc.bufToBase64 = function(buf) { + return Buffer.from(buf).toString('base64'); +}; + +Enc.base64ToBuf = function(b64) { + return Buffer.from(b64, 'base64'); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..771ec6b --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "@root/pem", + "version": "1.0.0", + "description": "VanillaJS, Lightweight, Zero-Dependency, PEM encoder and decoder.", + "main": "index.js", + "browser": { + "./node/native.js": "./browser/native.js" + }, + "scripts": { + "test": "node tests" + }, + "repository": { + "type": "git", + "url": "https://git.rootprojects.org/root/pem.js.git" + }, + "keywords": [ + "PEM", + "x509", + "ASN.1", + "asn1", + "RFC7468", + "RFC1421", + "RFC", + "7468", + "1421" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0" +} diff --git a/packer.js b/packer.js new file mode 100644 index 0000000..f41937a --- /dev/null +++ b/packer.js @@ -0,0 +1,22 @@ +'use strict'; + +var PEM = module.exports; + +//var Enc = require('@root/encoding/base64'); +var Enc = require('./node/native.js'); + +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 + + '-----' + ); +}; diff --git a/parser.js b/parser.js new file mode 100644 index 0000000..3064a5b --- /dev/null +++ b/parser.js @@ -0,0 +1,15 @@ +'use strict'; + +var PEM = module.exports; + +var Enc = require('./node/native.js'); + +PEM.parseBlock = function(str) { + var der = str + .split(/\n/) + .filter(function(line) { + return !/-----/.test(line); + }) + .join(''); + return { bytes: Enc.base64ToBuf(der) }; +}; diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..354ae32 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,36 @@ +'use strict'; + +var PEM = require('../'); +//console.log(PEM); + +// "Hello, δΈ–η•Œ!"; +var contents = 'SGVs\nbG8sIOS4\nlueVjCE='; +var pem = '-----BEGIN Type-----\n' + contents + '\n-----END Type-----\n'; +var block = PEM.parseBlock(pem); + +if (14 !== block.bytes.byteLength) { + throw new Error('should be 14 bytes'); +} + +if (0x48 !== block.bytes[0]) { + throw new Error('first byte should be 0x48 ("H")'); +} + +if (0x8c !== block.bytes[12]) { + throw new Error('13th byte should be 0x8c (3rd byte of "η•Œ")'); +} + +console.info("PASS: decodes 'bytes' field correctly"); + +var pem2 = + '-----BEGIN Type-----\n' + + contents.replace(/\n/g, '') + + '\n-----END Type-----'; + +block.type = 'Type'; +if (pem2 !== PEM.packBlock(block)) { + console.debug(PEM.packBlock(block)); + throw new Error('should pack PEM correctly'); +} + +console.info("PASS: encodes 'bytes' and 'type' fields correctly");