diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 0bb55ff..1f460e7 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -12,14 +12,16 @@ jobs: strategy: matrix: - node-version: [ 6.4.0, latest ] + node-version: [ 12.20.0, latest ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm test all + - run: npm install + if: matrix.node-version == 'latest' - run: npm run lint if: matrix.node-version == 'latest' diff --git a/.gitignore b/.gitignore index f7aad76..6369b69 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,11 @@ _MTN/ .DS_Store/ .vscode/ node_modules/ +dist/ package-lock.json pnpm-lock.yaml # Artifacts from release.sh +index-local.html sha256sums.asc asn1js.zip # Artifacts from mirror_to_github.sh diff --git a/.mtn-ignore b/.mtn-ignore index 97ac908..e908f9b 100644 --- a/.mtn-ignore +++ b/.mtn-ignore @@ -1,9 +1,11 @@ [.]DS_Store$ [.]vscode$ node_modules$ +dist$ package-lock[.]json pnpm-lock[.]yaml # Artifacts from release.sh +index-local.html sha256sums[.]asc asn1js[.]zip # Artifacts from mirror_to_github.sh diff --git a/LICENSE b/LICENSE index cd83836..52c9a87 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2008-2022 Lapo Luchini +Copyright (c) 2008-2024 Lapo Luchini Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/README.md b/README.md index 21eb133..167852e 100644 --- a/README.md +++ b/README.md @@ -5,54 +5,83 @@ asn1js is a JavaScript generic ASN.1 parser/decoder that can decode any valid AS An example page that can decode Base64-encoded (raw base64, PEM armoring and `begin-base64` are recognized) or Hex-encoded (or local files with some browsers) is included and can be used both [online on the official website](https://lapo.it/asn1js/) or [offline (ZIP file)](https://lapo.it/asn1js/asn1js.zip). -Usage with `npm` / `yarn` -------------------------- +Usage with `nodejs` +------------------- This package can be installed with either npm or yarn via the following commands: ```sh npm install @lapo/asn1js -# or with yarn +# or other tools +pnpm install @lapo/asn1js yarn add @lapo/asn1js ``` -Assuming a standard javascript bundler is setup you can import it like so: +You can import the classes like this: ```js -const ASN1 = require('@lapo/asn1js'); -// or with ES modules -import ASN1 from '@lapo/asn1js'; +import { ASN1 } from '@lapo/asn1js'; ``` A submodule of this package can also be imported: ```js -const Hex = require('@lapo/asn1js/hex'); -// or with ES modules -import Hex from '@lapo/asn1js/hex'; +import { Hex } from '@lapo/asn1js/hex.js'; ``` -Usage with RequireJS +If your code is still not using ES6 Modules (and is using CommonJS) you can `require` it normally [since NodeJS 22](https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/) (with parameter `--experimental-require-module`): + +```js +const + { ASN1 } = require('@lapo/asn1js'), + { Hex } = require('@lapo/asn1js/hex.js'); +console.log(ASN1.decode(Hex.decode('06032B6570')).content()); +``` + +On older NodeJS you instead need to use async `import`: + +```js +async function main() { + const + { ASN1 } = await import('@lapo/asn1js'), + { Hex } = await import('@lapo/asn1js/hex.js'); + console.log(ASN1.decode(Hex.decode('06032B6570')).content()); +} +main(); +``` + +Usage on the web -------------------- -Can be [tested on JSFiddle](https://jsfiddle.net/lapo/tmdq35ug/). +Can be [tested on JSFiddle](https://jsfiddle.net/lapo/y6t2wo7q/). ```html - ``` +Local usage +-------------------- + +Since unfortunately ESM modules are not working on `file:` protocol due to [CORS issues](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_standard_scripts), there is a bundled [single-file version working locally](https://asn1js.eu/index-local.html). It doesn't work online (due to [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) restrictions about inline content) but can be saved locally and opened in a browser. ([known bug](https://github.com/lapo-luchini/asn1js/issues/89): dark mode is currently broken in this mode) + +Usage from CLI +-------------------- + +You can dump an ASN.1 structure from the command line using the following command (no need to even install it): + +```sh +npx @lapo/asn1js ed25519.cer +``` + ISC license ----------- -ASN.1 JavaScript decoder Copyright (c) 2008-2023 Lapo Luchini +ASN.1 JavaScript decoder Copyright (c) 2008-2024 Lapo Luchini Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. @@ -66,7 +95,7 @@ credits - extended tag support added by [Péter Budai](https://www.peterbudai.eu/) - patches by [Gergely Nagy](https://github.com/ngg) - Relative OID support added by [Mistial Developer](https://github.com/mistial-dev) -- dark mode support added by [Oliver Burgmaier](https://github.com/olibu/) +- dark mode and other UI improvements by [Oliver Burgmaier](https://github.com/olibu/) - patches by [Nicolai Søborg](https://github.com/NicolaiSoeborg) links diff --git a/asn1.js b/asn1.js index a1d755c..55a5a2a 100644 --- a/asn1.js +++ b/asn1.js @@ -1,10 +1,10 @@ // ASN.1 JavaScript decoder -// Copyright (c) 2008-2023 Lapo Luchini +// Copyright (c) 2008-2024 Lapo Luchini // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. -// +// // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -13,15 +13,10 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -(typeof define != 'undefined' ? define : function (factory) { 'use strict'; - if (typeof module == 'object') module.exports = factory(function (name) { return require(name); }); - else window.asn1 = factory(function (name) { return window[name.substring(2)]; }); -})(function (require) { -'use strict'; +import { Int10 } from './int10.js'; +import { oids } from './oids.js'; const - Int10 = require('./int10'), - oids = require('./oids'), ellipsis = '\u2026', reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|(-(?:0\d|1[0-2])|[+](?:0\d|1[0-4]))([0-5]\d)?)?$/, reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|(-(?:0\d|1[0-2])|[+](?:0\d|1[0-4]))([0-5]\d)?)?$/, @@ -43,7 +38,7 @@ const ['', ''], ['OUou', 'ŐŰőű'], // Double Acute ['AEIUaeiu', 'ĄĘĮŲąęįų'], // Ogonek - ['CDELNRSTZcdelnrstz', 'ČĎĚĽŇŘŠŤŽčďěľňřšťž'] // Caron + ['CDELNRSTZcdelnrstz', 'ČĎĚĽŇŘŠŤŽčďěľňřšťž'], // Caron ]; function stringCut(str, len) { @@ -77,17 +72,21 @@ class Stream { if (pos === undefined) pos = this.pos++; if (pos >= this.enc.length) - throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length; + throw new Error('Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length); return (typeof this.enc == 'string') ? this.enc.charCodeAt(pos) : this.enc[pos]; } hexByte(b) { return hexDigits.charAt((b >> 4) & 0xF) + hexDigits.charAt(b & 0xF); } - hexDump(start, end, raw) { + /** Hexadecimal dump. + * @param type 'raw', 'byte' or 'dump' */ + hexDump(start, end, type = 'dump') { let s = ''; for (let i = start; i < end; ++i) { + if (type == 'byte' && i > start) + s += ' '; s += this.hexByte(this.get(i)); - if (raw !== true) + if (type == 'dump') switch (i & 0xF) { case 0x7: s += ' '; break; case 0xF: s += '\n'; break; @@ -195,7 +194,7 @@ class Stream { let s = this.parseStringISO(start, end).str, m = (shortYear ? reTimeS : reTimeL).exec(s); if (!m) - return 'Unrecognized time: ' + s; + throw new Error('Unrecognized time: ' + s); if (shortYear) { // to avoid querying the timer, use the fixed range [1970, 2069] // it will conform with ITU X.400 [-10, +40] sliding window until 2030 @@ -250,7 +249,7 @@ class Stream { parseBitString(start, end, maxLength) { let unusedBits = this.get(start); if (unusedBits > 7) - throw 'Invalid BitString with unusedBits=' + unusedBits; + throw new Error('Invalid BitString with unusedBits=' + unusedBits); let lenBit = ((end - start - 1) << 3) - unusedBits, s = ''; for (let i = start + 1; i < end; ++i) { @@ -371,9 +370,9 @@ class ASN1Tag { } } -class ASN1 { +export class ASN1 { constructor(stream, header, length, tag, tagLen, sub) { - if (!(tag instanceof ASN1Tag)) throw 'Invalid tag value.'; + if (!(tag instanceof ASN1Tag)) throw new Error('Invalid tag value.'); this.stream = stream; this.header = header; this.length = length; @@ -490,7 +489,16 @@ class ASN1 { } toPrettyString(indent) { if (indent === undefined) indent = ''; - let s = indent + this.typeName() + ' @' + this.stream.pos; + let s = indent; + if (this.def) { + if (this.def.id) + s += this.def.id + ' '; + if (this.def.name && this.def.name != this.typeName().replace(/_/g, ' ')) + s+= this.def.name + ' '; + if (this.def.mismatch) + s += '[?] '; + } + s += this.typeName() + ' @' + this.stream.pos; if (this.length >= 0) s += '+'; s += this.length; @@ -522,9 +530,12 @@ class ASN1 { posLen() { return this.stream.pos + this.tagLen; } - toHexString() { - return this.stream.hexDump(this.posStart(), this.posEnd(), true); + /** Hexadecimal dump of the node. + * @param type 'raw', 'byte' or 'dump' */ + toHexString(type = 'raw') { + return this.stream.hexDump(this.posStart(), this.posEnd(), type); } + /** Base64 dump of the node. */ toB64String() { return this.stream.b64Dump(this.posStart(), this.posEnd()); } @@ -536,7 +547,7 @@ class ASN1 { if (len === 0) // long form with length 0 is a special case return null; // undefined length if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways - throw 'Length over 48 bits not supported at position ' + (stream.pos - 1); + throw new Error('Length over 48 bits not supported at position ' + (stream.pos - 1)); buf = 0; for (let i = 0; i < len; ++i) buf = (buf * 256) + stream.get(); @@ -544,7 +555,7 @@ class ASN1 { } static decode(stream, offset, type = ASN1) { if (!(type == ASN1 || type.prototype instanceof ASN1)) - throw 'Must pass a class that extends ASN1'; + throw new Error('Must pass a class that extends ASN1'); if (!(stream instanceof Stream)) stream = new Stream(stream, offset || 0); let streamStart = new Stream(stream), @@ -560,11 +571,11 @@ class ASN1 { // definite length let end = start + len; if (end > stream.enc.length) - throw 'Container at offset ' + start + ' has a length of ' + len + ', which is past the end of the stream'; + throw new Error('Container at offset ' + start + ' has a length of ' + len + ', which is past the end of the stream'); while (stream.pos < end) sub[sub.length] = type.decode(stream); if (stream.pos != end) - throw 'Content size is not correct for container at offset ' + start; + throw new Error('Content size is not correct for container at offset ' + start); } else { // undefined length try { @@ -576,7 +587,7 @@ class ASN1 { } len = start - stream.pos; // undefined lengths are represented as negative values } catch (e) { - throw 'Exception while decoding undefined length content at offset ' + start + ': ' + e; + throw new Error('Exception while decoding undefined length content at offset ' + start + ': ' + e); } } }; @@ -588,11 +599,17 @@ class ASN1 { try { if (tag.tagNumber == 0x03) if (stream.get() != 0) - throw 'BIT STRINGs with unused bits cannot encapsulate.'; + throw new Error('BIT STRINGs with unused bits cannot encapsulate.'); getSub(); - for (let i = 0; i < sub.length; ++i) - if (sub[i].tag.isEOC()) - throw 'EOC is not supposed to be actual content.'; + for (let s of sub) { + if (s.tag.isEOC()) + throw new Error('EOC is not supposed to be actual content.'); + try { + s.content(); + } catch (e) { + throw new Error('Unable to parse content: ' + e); + } + } } catch (e) { // but silently ignore when they don't sub = null; @@ -601,14 +618,10 @@ class ASN1 { } if (sub === null) { if (len === null) - throw "We can't skip over an invalid tag with undefined length at offset " + start; + throw new Error("We can't skip over an invalid tag with undefined length at offset " + start); stream.pos = start + Math.abs(len); } return new type(streamStart, header, len, tag, tagLen, sub); } } - -return ASN1; - -}); diff --git a/base64.js b/base64.js index ebc5c9e..4e3d325 100644 --- a/base64.js +++ b/base64.js @@ -1,10 +1,10 @@ // Base64 JavaScript decoder -// Copyright (c) 2008-2023 Lapo Luchini +// Copyright (c) 2008-2024 Lapo Luchini // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. -// +// // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -13,18 +13,12 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -(typeof define != 'undefined' ? define : function (factory) { 'use strict'; - if (typeof module == 'object') module.exports = factory(); - else window.base64 = factory(); -})(function () { -'use strict'; - const haveU8 = (typeof Uint8Array == 'function'); let decoder; // populated on first usage -class Base64 { +export class Base64 { static decode(a) { let isString = (typeof a == 'string'); @@ -107,7 +101,3 @@ class Base64 { } Base64.re = /-----BEGIN [^-]+-----([A-Za-z0-9+/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+/=\s]+)====|^([A-Za-z0-9+/=\s]+)$/; - -return Base64; - -}); diff --git a/check.sh b/check.sh index d21d5ac..f66e168 100755 --- a/check.sh +++ b/check.sh @@ -1,3 +1,3 @@ #!/bin/sh -type gsha256sum >/dev/null && SHA256=gsha256sum || SHA256=sha256sum +type gsha256sum >/dev/null 2>/dev/null && SHA256=gsha256sum || SHA256=sha256sum gpg --verify -o - sha256sums.asc | $SHA256 -c --quiet diff --git a/context.js b/context.js new file mode 100644 index 0000000..fc561f9 --- /dev/null +++ b/context.js @@ -0,0 +1,53 @@ +const + id = (elem) => document.getElementById(elem), + contextMenu = id('contextmenu'), + btnCopyHex = id('btnCopyHex'), + btnCopyB64 = id('btnCopyB64'), + btnCopyTree = id('btnCopyTree'), + btnCopyValue = id('btnCopyValue'); + +export function bindContextMenu(node) { + const type = node.asn1.typeName(); + const valueEnabled = type != 'SET' && type != 'SEQUENCE'; + node.onclick = function (event) { + // do not show the menu in case of clicking the icon + if (event.srcElement.nodeName != 'SPAN') return; + contextMenu.style.left = event.pageX + 'px'; + contextMenu.style.top = event.pageY + 'px'; + contextMenu.style.visibility = 'visible'; + contextMenu.node = this; + btnCopyValue.style.display = valueEnabled ? 'block' : 'none'; + event.preventDefault(); + event.stopPropagation(); + }; +} + +function close(event) { + contextMenu.style.visibility = 'hidden'; + event.stopPropagation(); +} + +contextMenu.onmouseleave = close; + +btnCopyHex.onclick = function (event) { + navigator.clipboard.writeText(contextMenu.node.asn1.toHexString('byte')); + close(event); +}; + +btnCopyB64.onclick = function (event) { + event.stopPropagation(); + navigator.clipboard.writeText(contextMenu.node.asn1.toB64String()); + close(event); +}; + +btnCopyTree.onclick = function (event) { + event.stopPropagation(); + navigator.clipboard.writeText(contextMenu.node.asn1.toPrettyString()); + close(event); +}; + +btnCopyValue.onclick = function (event) { + event.stopPropagation(); + navigator.clipboard.writeText(contextMenu.node.asn1.content()); + close(event); +}; diff --git a/defs.js b/defs.js index 75bdaee..fa42c3e 100644 --- a/defs.js +++ b/defs.js @@ -1,10 +1,10 @@ // ASN.1 RFC definitions matcher -// Copyright (c) 2023-2023 Lapo Luchini +// Copyright (c) 2023-2024 Lapo Luchini // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. -// +// // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -13,13 +13,7 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -(typeof define != 'undefined' ? define : function (factory) { 'use strict'; - if (typeof module == 'object') module.exports = factory(function (name) { return require(name); }); - else window.defs = factory(function (name) { return window[name.substring(2)]; }); -})(function (require) { -'use strict'; - -const rfc = require('./rfcdef'); +import { rfcdef } from './rfcdef.js'; function translate(def, tn, stats) { if (def?.type == 'tag' && !def.explicit) @@ -29,7 +23,7 @@ function translate(def, tn, stats) { try { // hope current OIDs contain the type name (will need to parse from RFC itself) def = Defs.searchType(firstUpper(stats.defs[def.definedBy][1])); - } catch (e) {} + } catch (e) { /*ignore*/ } while (def?.type == 'defined' || def?.type?.type == 'defined') { const name = def?.type?.type ? def.type.name : def.name; def = Object.assign({}, def); @@ -37,7 +31,8 @@ function translate(def, tn, stats) { } if (def?.type?.name == 'CHOICE') { for (let c of def.type.content) { - c = translate(c); + if (tn != c.type.name && tn != c.name) + c = translate(c); if (tn == c.type.name || tn == c.name) { def = Object.assign({}, def); def.type = c.type.name ? c.type : c; @@ -55,20 +50,20 @@ function firstUpper(s) { return s[0].toUpperCase() + s.slice(1); } -class Defs { +export class Defs { static moduleAndType(mod, name) { return Object.assign({ module: { oid: mod.oid, name: mod.name, source: mod.source } }, mod.types[name]); } static searchType(name) { - for (const mod of Object.values(rfc)) + for (const mod of Object.values(rfcdef)) if (name in mod.types) { // console.log(name + ' found in ' + r.name); // return r.types[name]; return Defs.moduleAndType(mod, name); } - throw 'Type not found: ' + name; + throw new Error('Type not found: ' + name); } static match(value, def, stats = { total: 0, recognized: 0, defs: {} }) { @@ -95,12 +90,20 @@ class Defs { type = def.content[0]; else { let tn = subval.typeName().replaceAll('_', ' '); - do { + for (;;) { type = def.content[j++]; - // type = translate(type, tn); + if (!type || typeof type != 'object') break; if (type?.type?.type) type = type.type; - } while (type && typeof type == 'object' && ('optional' in type || 'default' in type) && type.name != 'ANY' && type.name != tn); + if (type.type == 'defined') { + let t2 = translate(type, tn); + if (t2.type.name == tn) break; // exact match + if (t2.type.name == 'ANY') break; // good enough + } + if (type.name == tn) break; // exact match + if (type.name == 'ANY') break; // good enough + if (!('optional' in type || 'default' in type)) break; + } if (type?.type == 'builtin' || type?.type == 'defined') { let v = subval.content(); if (typeof v == 'string') @@ -109,7 +112,7 @@ class Defs { } else if (type?.definedBy && stats.defs?.[type.definedBy]?.[1]) { // hope current OIDs contain the type name (will need to parse from RFC itself) try { type = Defs.searchType(firstUpper(stats.defs[type.definedBy][1])); - } catch (e) {} + } catch (e) { /*ignore*/ } } } } @@ -121,17 +124,15 @@ class Defs { } -Defs.RFC = rfc; +Defs.RFC = rfcdef; Defs.commonTypes = [ - [ 'X.509 certificate', '1.3.6.1.5.5.7.0.18', 'Certificate' ], + [ 'X.509 certificate', '1.3.6.1.5.5.7.0.18', 'Certificate' ], + [ 'X.509 public key info', '1.3.6.1.5.5.7.0.18', 'SubjectPublicKeyInfo' ], [ 'CMS / PKCS#7 envelope', '1.2.840.113549.1.9.16.0.14', 'ContentInfo' ], + [ 'PKCS#1 RSA private key', '1.2.840.113549.1.1.0.1', 'RSAPrivateKey' ], [ 'PKCS#8 encrypted private key', '1.2.840.113549.1.8.1.1', 'EncryptedPrivateKeyInfo' ], [ 'PKCS#8 private key', '1.2.840.113549.1.8.1.1', 'PrivateKeyInfo' ], [ 'PKCS#10 certification request', '1.2.840.113549.1.10.1.1', 'CertificationRequest' ], [ 'CMP PKI Message', '1.3.6.1.5.5.7.0.16', 'PKIMessage' ], -].map(arr => ({ description: arr[0], ...Defs.moduleAndType(rfc[arr[1]], arr[2]) })); - -return Defs; - -}); +].map(arr => ({ description: arr[0], ...Defs.moduleAndType(rfcdef[arr[1]], arr[2]) })); diff --git a/dom.js b/dom.js index 4c0b32d..dfe44ef 100644 --- a/dom.js +++ b/dom.js @@ -1,10 +1,10 @@ // ASN.1 JavaScript decoder -// Copyright (c) 2008-2023 Lapo Luchini +// Copyright (c) 2008-2024 Lapo Luchini // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. -// +// // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -13,22 +13,18 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -(typeof define != 'undefined' ? define : function (factory) { 'use strict'; - if (typeof module == 'object') module.exports = factory(function (name) { return require(name); }); - else window.dom = factory(function (name) { return window[name.substring(2)]; }); -})(function (require) { -'use strict'; +import { ASN1 } from './asn1.js'; +import { oids } from './oids.js'; +import { bindContextMenu } from './context.js'; const - ASN1 = require('./asn1'), - oids = require('./oids'), lineLength = 80, contentLength = 8 * lineLength, DOM = { ellipsis: '\u2026', tag: function (tagName, className, text) { let t = document.createElement(tagName); - t.className = className; + if (className) t.className = className; if (text) t.innerText = text; return t; }, @@ -52,18 +48,17 @@ const o += line; } return o; - } - } + }, + }; -class ASN1DOM extends ASN1 { +export class ASN1DOM extends ASN1 { toDOM(spaces) { spaces = spaces || ''; let isOID = (typeof oids === 'object') && (this.tag.isUniversal() && (this.tag.tagNumber == 0x06) || (this.tag.tagNumber == 0x0D)); - let node = DOM.tag('div', 'node'); + let node = DOM.tag('li'); node.asn1 = this; - let head = DOM.tag('div', 'head'); - head.appendChild(DOM.tag('span', 'spaces', spaces)); + let head = DOM.tag('span', 'head'); const typeName = this.typeName().replace(/_/g, ' '); if (this.def) { if (this.def.id) { @@ -80,7 +75,12 @@ class ASN1DOM extends ASN1 { } } head.appendChild(DOM.text(typeName)); - let content = this.content(contentLength); + let content; + try { + content = this.content(contentLength); + } catch (e) { + content = 'Cannot decode: ' + e; + } let oid; if (content !== null) { let preview = DOM.tag('span', 'preview'), @@ -110,8 +110,24 @@ class ASN1DOM extends ASN1 { content = content.replace(/'); } - node.appendChild(head); - this.node = node; + // add the li and details section for this node + let contentNode; + let childNode; + if (this.sub !== null) { + let details = DOM.tag('details'); + details.setAttribute('open', ''); + node.appendChild(details); + let summary = DOM.tag('summary', 'node'); + details.appendChild(summary); + summary.appendChild(head); + contentNode = summary; + childNode = details; + } else { + contentNode = node; + contentNode.classList.add('node'); + contentNode.appendChild(head); + } + this.node = contentNode; this.head = head; let value = DOM.tag('div', 'value'); let s = 'Offset: ' + this.stream.pos + '
'; @@ -134,29 +150,26 @@ class ASN1DOM extends ASN1 { } } value.innerHTML = s; - node.appendChild(value); - let sub = DOM.tag('div', 'sub'); + contentNode.appendChild(value); if (this.sub !== null) { + let sub = DOM.tag('ul'); + childNode.appendChild(sub); spaces += '\xA0 '; for (let i = 0, max = this.sub.length; i < max; ++i) sub.appendChild(this.sub[i].toDOM(spaces)); } - node.appendChild(sub); - head.onclick = function () { - node.className = (node.className == 'node collapsed') ? 'node' : 'node collapsed'; - }; + bindContextMenu(node); return node; } fakeHover(current) { - this.node.className += ' hover'; + this.node.classList.add('hover'); if (current) - this.head.className += ' hover'; + this.head.classList.add('hover'); } fakeOut(current) { - let re = / ?hover/; - this.node.className = this.node.className.replace(re, ''); + this.node.classList.remove('hover'); if (current) - this.head.className = this.head.className.replace(re, ''); + this.head.classList.remove('hover'); } toHexDOM_sub(node, className, stream, start, end) { if (start >= end) @@ -171,13 +184,14 @@ class ASN1DOM extends ASN1 { this.head.onmouseover = function () { this.hexNode.className = 'hexCurrent'; }; this.head.onmouseout = function () { this.hexNode.className = 'hex'; }; node.asn1 = this; - node.onmouseover = function () { + node.onmouseover = function (event) { let current = !root.selected; if (current) { root.selected = this.asn1; this.className = 'hexCurrent'; } this.asn1.fakeHover(current); + event.stopPropagation(); }; node.onmouseout = function () { let current = (root.selected == this.asn1); @@ -187,6 +201,7 @@ class ASN1DOM extends ASN1 { this.className = 'hex'; } }; + bindContextMenu(node); if (root == node) { let lineStart = this.posStart() & 0xF; if (lineStart != 0) { @@ -231,7 +246,3 @@ class ASN1DOM extends ASN1 { } } - -return ASN1DOM; - -}); diff --git a/dumpASN1.js b/dumpASN1.js index dd02546..29fdae1 100755 --- a/dumpASN1.js +++ b/dumpASN1.js @@ -1,14 +1,15 @@ #!/usr/bin/env node -'use strict'; + +import * as fs from 'node:fs'; +import { Base64 } from './base64.js'; +import { ASN1 } from './asn1.js'; +import { Defs } from './defs.js'; const - fs = require('fs'), - Base64 = require('./base64'), - ASN1 = require('./asn1'), - Defs = require('./defs'), colYellow = '\x1b[33m', colBlue = '\x1b[34m', - colReset = '\x1b[0m'; + colReset = '\x1b[0m', + reDataURI = /^data:(?:[a-z-]+[/][a-z.+-]+;)?base64,([A-Za-z0-9+/=\s]+)$/; function print(value, indent) { if (indent === undefined) indent = ''; @@ -40,7 +41,11 @@ function print(value, indent) { return s; } -let content = fs.readFileSync(process.argv[2]); +const filename = process.argv[2]; +const match = reDataURI.exec(filename); +let content = match + ? Buffer.from(match[1]) + : fs.readFileSync(filename); try { // try PEM first content = Base64.unarmor(content); } catch (e) { // try DER/BER then @@ -48,22 +53,26 @@ try { // try PEM first let result = ASN1.decode(content); content = null; const t0 = performance.now(); -const types = Defs.commonTypes - .map(type => { - const stats = Defs.match(result, type); - return { type, match: stats.recognized / stats.total }; - }) - .sort((a, b) => b.match - a.match); -const t1 = performance.now(); -console.log('Parsed in ' + (t1 - t0).toFixed(2) + ' ms; possible types:'); -for (const t of types) - console.log((t.match * 100).toFixed(2).padStart(6) + '% ' + t.type.description); -Defs.match(result, types[0].type); -// const stats = Defs.match(result, types[0].type); -// console.log('Stats:', stats); -console.log('Parsed as:', result.def); -// const type = searchType(process.argv[2]); -// const stats = applyDef(result, type); +if (process.argv.length == 5) { + Defs.match(result, Defs.moduleAndType(Defs.RFC[process.argv[3]], process.argv[4])); +} else { + const types = Defs.commonTypes + .map(type => { + const stats = Defs.match(result, type); + return { type, match: stats.recognized / stats.total }; + }) + .sort((a, b) => b.match - a.match); + const t1 = performance.now(); + console.log('Parsed in ' + (t1 - t0).toFixed(2) + ' ms; possible types:'); + for (const t of types) + console.log((t.match * 100).toFixed(2).padStart(6) + '% ' + t.type.description); + Defs.match(result, types[0].type); + // const stats = Defs.match(result, types[0].type); + // console.log('Stats:', stats); + console.log('Parsed as:', result.def); + // const type = searchType(process.argv[2]); + // const stats = applyDef(result, type); +} console.log(print(result)); // console.log('Stats:', (stats.recognized * 100 / stats.total).toFixed(2) + '%'); // // print(result, searchType(process.argv[2]), stats); diff --git a/examples/cms-password.p7m b/examples/cms-password.p7m new file mode 100644 index 0000000..a91273f --- /dev/null +++ b/examples/cms-password.p7m @@ -0,0 +1,9 @@ +This is a PKCS#7/CMS encrypted with passwod. +$ echo content | openssl cms -encrypt -pwri_password test -aes256 -outform pem -out examples/cms-password.p7m +-----BEGIN CMS----- +MIHYBgkqhkiG9w0BBwOggcowgccCAQMxgYOjgYACAQCgGwYJKoZIhvcNAQUMMA4E +CED/DSxXMtH6AgIIADAsBgsqhkiG9w0BCRADCTAdBglghkgBZQMEASoEEDIQbJMC +Sfb3LpwHduj/meQEMKwrwq5M4V0stztm6OUTAsFY2zKDY20SApwSEeEcAh9TM42E +1palnHeqHTBpC8pIpjA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBByt+scPrdM +giR7WUOJyB3hgBDcD3UDMtZSep8X/3yy1/Yq +-----END CMS----- diff --git a/examples/pkcs1.pem b/examples/pkcs1.pem new file mode 100644 index 0000000..7077a97 --- /dev/null +++ b/examples/pkcs1.pem @@ -0,0 +1,18 @@ +PKCS#1 RSA key +$ openssl genrsa -out examples/pkcs8-rsa.pem 1024 +$ openssl rsa -in examples/pkcs8-rsa.pem -out examples/pkcs1.pem -traditional +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQCmy23ifN9pi5LO4MR3LUhU0v+LZmv78H+jd+R6kFcWZf1qW4yf +KTDkryjjLlIhYqxmzXCqGyaIjj7uJoorWf7KfkxpOuJrh4swJ/WGhCn9i+voW/7T +sOXfDp1yqrEhaQKwdPot1ZAB78TNsecwX/SODTEMCk95jvx1j5cDxPlskwIDAQAB +AoGBAINn4bp+BsVwYMj768y4sDOjyBBbMNfcMbLn0el9rh7HW09fsPnzycFg/iV9 +aNdEle6oDAr4OPN8nbeiRVjCHijEnVdHCwAtkKODyuu1ghpZWD0VUC8AEskjX4Bs +Ysl/HjyvvHIRj89gdDFoElgB4GzHKTzeZNJBM5qtUW57zBCBAkEA0A6N5l98MglL +cypWKM7+3DXteWt86mKXYUVF33HY28Z+oUVlU0v8m8XxpoAjkicYnC1JOSSlvWRk +EWlTMgHW5QJBAM06yIHMR6p3apgpwOUp49DbtaQ8NmhCV4NBoFHa+vT2Fk8twOcq +O9OzP4svhKbPNfB4HnxGbmd/+OVT3lySxhcCQHRPPpqD1K0wLwKxrzrfBPDcIOaY +5VsuRIw3KqmQPngWTiIf5lYbi5sVnFLFHZ2Nx58/XcjZKOJopdxp8f1ps9UCQQC3 +rOqSsF9bg3DVKllHQAxyepDAolsXSHjGMk/nspJz9mLVDl/dBAFzYLN4QFj6ae0e +gILYOrjIzNHXfQ4/z+SVAkBPebkAzpGFgzVzu6VOGx0Vft/ow3/DKNJSDM58yASp +ootY2TdibrrV/ellNLvuTiku6AEM/8jbHlRsmfxRe0xn +-----END RSA PRIVATE KEY----- diff --git a/examples/pkcs8-rsa.pem b/examples/pkcs8-rsa.pem new file mode 100644 index 0000000..39829b5 --- /dev/null +++ b/examples/pkcs8-rsa.pem @@ -0,0 +1,18 @@ +PKCS#8 RSA key +$ openssl genrsa -out examples/pkcs8-rsa.pem 1024 +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKbLbeJ832mLks7g +xHctSFTS/4tma/vwf6N35HqQVxZl/WpbjJ8pMOSvKOMuUiFirGbNcKobJoiOPu4m +iitZ/sp+TGk64muHizAn9YaEKf2L6+hb/tOw5d8OnXKqsSFpArB0+i3VkAHvxM2x +5zBf9I4NMQwKT3mO/HWPlwPE+WyTAgMBAAECgYEAg2fhun4GxXBgyPvrzLiwM6PI +EFsw19wxsufR6X2uHsdbT1+w+fPJwWD+JX1o10SV7qgMCvg483ydt6JFWMIeKMSd +V0cLAC2Qo4PK67WCGllYPRVQLwASySNfgGxiyX8ePK+8chGPz2B0MWgSWAHgbMcp +PN5k0kEzmq1RbnvMEIECQQDQDo3mX3wyCUtzKlYozv7cNe15a3zqYpdhRUXfcdjb +xn6hRWVTS/ybxfGmgCOSJxicLUk5JKW9ZGQRaVMyAdblAkEAzTrIgcxHqndqmCnA +5Snj0Nu1pDw2aEJXg0GgUdr69PYWTy3A5yo707M/iy+Eps818HgefEZuZ3/45VPe +XJLGFwJAdE8+moPUrTAvArGvOt8E8Nwg5pjlWy5EjDcqqZA+eBZOIh/mVhuLmxWc +UsUdnY3Hnz9dyNko4mil3Gnx/Wmz1QJBALes6pKwX1uDcNUqWUdADHJ6kMCiWxdI +eMYyT+eyknP2YtUOX90EAXNgs3hAWPpp7R6Agtg6uMjM0dd9Dj/P5JUCQE95uQDO +kYWDNXO7pU4bHRV+3+jDf8Mo0lIMznzIBKmii1jZN2JuutX96WU0u+5OKS7oAQz/ +yNseVGyZ/FF7TGc= +-----END PRIVATE KEY----- diff --git a/favicon.svg b/favicon.svg index 86b4e77..a43f4cf 100644 --- a/favicon.svg +++ b/favicon.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/hex.js b/hex.js index 3675e7a..de3ee49 100644 --- a/hex.js +++ b/hex.js @@ -1,10 +1,10 @@ // Hex JavaScript decoder -// Copyright (c) 2008-2023 Lapo Luchini +// Copyright (c) 2008-2024 Lapo Luchini // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. -// +// // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -13,18 +13,12 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -(typeof define != 'undefined' ? define : function (factory) { 'use strict'; - if (typeof module == 'object') module.exports = factory(); - else window.hex = factory(); -})(function () { -'use strict'; - const haveU8 = (typeof Uint8Array == 'function'); let decoder; // populated on first usage -class Hex { +export class Hex { /** * Decodes an hexadecimal value. @@ -73,7 +67,3 @@ class Hex { } } - -return Hex; - -}); diff --git a/index-dark.css b/index-dark.css deleted file mode 100644 index eb4097e..0000000 --- a/index-dark.css +++ /dev/null @@ -1,42 +0,0 @@ -:root { - --main-bg-color: #0d1116; - --main-text-color: #f8f8f2; - --headline-text-color: #8be9fd; - --button-border-color: #505050; - --button-bg-color: #303030; - --button-bghover-color: #404040; - --input-border-color: #505050; - --input-bg-color: #0c0e11; - --link-color: #58a6ff; - --link-hover-color: #9b9bea; - --header-bg-color: #161b22; - --page-bg-color: #000000; - --license-bg-color: #4b4a4a; - --license-border-color: black; - --sub-border-color: #383838; - --preview-bg-color: #989797; - --preview-border-color: #b5b3b3; - --dump-bg-color: #0c0e11; - --dump-border-color: #505050; -} -h1 { - font-weight: 200; -} -.license .hidden { - background-color: #4b4a4a; /*minimal support for IE11*/ - background-color: var(--license-bg-color); -} -.value { - background-color: #303030; /*minimal support for IE11*/ - background-color: var(--button-bg-color); -} -#dump .tag { color: #58a6ff; } -#dump .dlen { color: darkcyan; } -#dump .ulen { color: #00b6b6; } -#dump .intro { color: #58a6ff; } -#dump .outro { color: #00b6b6; } -#dump .skip { color: #707070; background-color: #222222; } -#dump .hexCurrent { background-color: #727272; } -#dump .hexCurrent .hex { background-color: #474747; } -#dump .hexCurrent .tag { color: #6db0fc; } -#dump .hexCurrent .dlen { color: #00b6b6; } diff --git a/index.css b/index.css index c57b12d..e0e81a8 100644 --- a/index.css +++ b/index.css @@ -1,4 +1,4 @@ -:root { +html { --main-bg-color: #C0C0C0; --main-text-color: #000000; --headline-text-color: #8be9fd; @@ -18,6 +18,52 @@ --preview-border-color: #505050; --dump-bg-color: #C0C0C0; --dump-border-color: #E0E0E0; + --dump-tag: blue; + --dump-dlen: darkcyan; + --dump-ulen: darkgreen; + --dump-intro: blue; + --dump-outro: darkgreen; + --dump-skip: #666666; + --dump-skip-bg: #C0C0C0; + --dump-hex-current: #808080; + --dump-hex-current-hex: #A0A0A0; + --dump-hex-current-dlen: #004040; + --hover-bg-color: #E0E0E0; + --tree-zoom-fix: -1px; + --tree-line: #999; +} +html[data-theme="dark"] { + --main-bg-color: #0d1116; + --main-text-color: #f8f8f2; + --headline-text-color: #8be9fd; + --button-border-color: #505050; + --button-bg-color: #303030; + --button-bghover-color: #404040; + --input-border-color: #505050; + --input-bg-color: #0c0e11; + --link-color: #58a6ff; + --link-hover-color: #9b9bea; + --header-bg-color: #161b22; + --page-bg-color: #000000; + --license-bg-color: #4b4a4a; + --license-border-color: black; + --sub-border-color: #383838; + --preview-bg-color: #989797; + --preview-border-color: #b5b3b3; + --dump-bg-color: #0c0e11; + --dump-border-color: #505050; + --dump-tag: #58a6ff; + --dump-dlen: darkcyan; + --dump-ulen: #00b6b6; + --dump-intro: #58a6ff; + --dump-outro: #00b6b6; + --dump-skip: #707070; + --dump-skip-bg: #222222; + --dump-hex-current: #727272; + --dump-hex-current-hex: #474747; + --dump-hex-current-dlen: #00b6b6; + --hover-bg-color: #505050; + --tree-line: #333; } html, body { background-color: var(--page-bg-color); @@ -83,7 +129,7 @@ header { #main-page { background-color: var(--main-bg-color); border: 0px; - padding: 15px; + padding: 5px; } #main-page > div { position: relative; @@ -115,29 +161,13 @@ header { /*display: block;*/ visibility: visible; } -.node { - position: relative; -} -.sub { - padding-left: 1.5em; - border-left: solid 1px var(--sub-border-color); -} .head { height: 1em; white-space: nowrap; } -.head:hover::before { - position: absolute; - content: '-'; - color: red; - border: solid 1px red; - border-radius: 20px; - background-color: black; - /*TODO: use vars instead of hex*/ -} .node:hover > .head, .node.hover > .head { color: var(--link-color); - font-weight: bold; + background-color: var(--hover-bg-color); } .node:hover > .head:hover, .node.hover > .head.hover { color: var(--link-hover-color); @@ -163,7 +193,7 @@ header { position: absolute; z-index: 2; top: 1.2em; - left: 0; + left: 30px; background-color: #efefef; /*minimal support for IE11*/ background-color: var(--button-bg-color); border: solid 1px var(--button-border-color); @@ -201,14 +231,132 @@ header { white-space: pre; padding: 5px; } -#dump .tag { color: blue; } -#dump .dlen { color: darkcyan; } -#dump .ulen { color: darkgreen; } -#dump .intro { color: blue; } -#dump .outro { color: darkgreen; } -#dump .skip { color: #666666; background-color: #C0C0C0; } -#dump .hexCurrent { background-color: #808080; } -#dump .hexCurrent .hex { background-color: #A0A0A0; } -#dump .hexCurrent .dlen { color: #004040; } +#dump .tag { color: var(--dump-tag); } +#dump .dlen { color: var(--dump-dlen); } +#dump .ulen { color: var(--dump-ulen); } +#dump .intro { color: var(--dump-intro); } +#dump .outro { color: var(--dump-outro); } +#dump .skip { color: var(--dump-skip); background-color: var(--dump-skip-bg); } +#dump .hexCurrent { background-color: var(--dump-hex-current); } +#dump .hexCurrent .hex { background-color: var(--dump-hex-current-hex); } +#dump .hexCurrent .dlen { color: var(--dump-hex-current-dlen); } #file { display: none; } #area { width: 100%; } +#contextmenu { + position: absolute; + visibility: hidden; + top: 0; + left: 0; + padding: 2px; + background-color: var(--button-bg-color); + border: 1px solid var(--button-bg-color); + z-index: 2; +} +#contextmenu > button { + display: block; + width: 120px; + background-color: var(--button-bg-color); + color: var(--main-text-color); + border: 1px solid var(--button-border-color); + text-align: left; +} +#contextmenu > button:hover { + background-color: var(--button-bghover-color); +} + +.treecollapse { + --spacing: 1.5rem; + --radius: 7px; + padding-inline-start: 0px; +} +.treecollapse li{ + display: block; + position: relative; + padding-left: calc(2 * var(--spacing) - var(--radius) - 2px); +} +.treecollapse ul{ + padding-left: 0; + margin-left: calc(var(--radius) - var(--spacing)); +} +.treecollapse ul li{ + border-left: 1px solid var(--tree-line); +} +.treecollapse ul li:last-child{ + border-color: transparent; +} +.treecollapse ul li::before{ + content: ''; + display: block; + position: absolute; + top: calc(var(--spacing) / -1.6); + left: var(--tree-zoom-fix); + width: calc(var(--spacing) + 2px); + height: calc(var(--spacing) + 1px); + border: solid var(--tree-line); + border-width: 0 0 1px 1px; +} +.treecollapse summary{ + display : block; + cursor : pointer; +} +.treecollapse summary::marker, +.treecollapse summary::-webkit-details-marker{ + display : none; +} +.treecollapse summary:focus{ + outline : none; +} +.treecollapse summary:focus-visible{ + outline : 1px dotted #000; +} +.treecollapse summary::before{ + content: ''; + display: block; + position: absolute; + top: calc(var(--spacing) / 2 - var(--radius)); + left: calc(var(--spacing) - var(--radius) - 1px); + width: calc(2 * var(--radius)); + height: calc(2 * var(--radius)); +} +.treecollapse summary::before{ + z-index: 1; + top: 1px; + background: url('tree-icon-light.svg'); +} +html[data-theme="dark"] .treecollapse summary::before{ + background: url('tree-icon-dark.svg'); +} +.treecollapse details[open] > summary::before{ + background-position : calc(-2 * var(--radius)) 0; +} + +/* +Zoom fix to have straight lines in treeview +Zoom level and dpi resolution: +- 175%: 336dpi +- 150%: 288dpi +- 110%: 212dpi +- 100%: 192dpi +- 90%: 173dpi +- 80%: 154dpi +*/ +@media (resolution <= 154dpi) { + :root{ + --tree-zoom-fix: -0.6px; + } +} +@media (155dpi <= resolution < 192dpi) { + :root{ + --tree-zoom-fix: -0.7px; + } +} +@media (192dpi <= resolution < 336dpi) { + :root{ + --tree-zoom-fix: -1px; + } +} +@media (336dpi <= resolution) { + :root{ + --tree-zoom-fix: -0.9px; + } +} diff --git a/index.html b/index.html index 7b7a9f3..974a304 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,21 @@ - + - + + + ASN.1 JavaScript decoder - + +
+ + + + +

ASN.1 JavaScript decoder

@@ -39,13 +47,16 @@

ASN.1 JavaScript decoder



-
Drag or load file:
Load examples: +
Load examples: @@ -59,17 +70,17 @@

ASN.1 JavaScript decoder

Instructions

This page contains a JavaScript generic ASN.1 parser that can decode any valid ASN.1 DER or BER structure whether Base64-encoded (raw base64, PEM armoring and begin-base64 are recognized) or Hex-encoded.

-

This tool can be used online at the address http://lapo.it/asn1js/ or offline, unpacking the ZIP file in a directory and opening index.html in a browser

+

This tool can be used online at the address https://asn1js.eu/ or offline, unpacking the ZIP file in a directory and opening index.html in a browser

On the left of the page will be printed a tree representing the hierarchical structure, on the right side an hex dump will be shown.
Hovering on the tree highlights ancestry (the hovered node and all its ancestors get colored) and the position of the hovered node gets highlighted in the hex dump (with header and content in a different colors).
Clicking a node in the tree will hide its sub-nodes (collapsed nodes can be noticed because they will become italic).

-

WARNING: starting from 2023-02-26 this website is using some ES6 features, which can break it for older browsers (though it is still working on IE11).
- You can access last version before ES6 on githack.

+

WARNING: starting from 2024-03-28 this website is using ES6 features (and modules), which can break it for very old browsers.
+ You can access last version before ES6 on githack (which still works on IE11).

Copyright

Copyright

ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

-

ASN.1 JavaScript decoder Copyright © 2008-2023 Lapo Luchini; released as opensource under the ISC license.

+

ASN.1 JavaScript decoder Copyright © 2008-2024 Lapo Luchini; released as opensource under the ISC license.

-

OBJECT IDENTIFIER values are recognized using data taken from Peter Gutmann's dumpasn1 program.

+

OBJECT IDENTIFIER values are recognized using data taken from Peter Gutmann's dumpasn1 program.

Links

- - - - - - - - - - - + diff --git a/index.js b/index.js index fc468e0..4c4bbd2 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,11 @@ -(typeof define != 'undefined' ? define : function (factory) { 'use strict'; - if (typeof module == 'object') factory(function (name) { return require(name); }); - else factory(function (name) { return window[name.substring(2)]; }); -})(function (require) { -'use strict'; +import './theme.js'; +import { ASN1DOM } from './dom.js'; +import { Base64 } from './base64.js'; +import { Hex } from './hex.js'; +import { Defs } from './defs.js'; +import { tags } from './tags.js'; const - ASN1DOM = require('./dom'), - Base64 = require('./base64'), - Hex = require('./hex'), - Defs = require('./defs'), - tags = require('./tags'), maxLength = 10240, reHex = /^\s*(?:[0-9A-Fa-f][0-9A-Fa-f]\s*)+$/, tree = id('tree'), @@ -20,7 +16,6 @@ const area = id('area'), file = id('file'), examples = id('examples'), - selectTheme = id('theme-select'), selectDefs = id('definitions'), selectTag = id('tags'); @@ -46,10 +41,13 @@ function checkbox(name) { function show(asn1) { tree.innerHTML = ''; dump.innerHTML = ''; - tree.appendChild(asn1.toDOM()); + let ul = document.createElement('ul'); + ul.className = 'treecollapse'; + tree.appendChild(ul); + ul.appendChild(asn1.toDOM()); if (wantHex.checked) dump.appendChild(asn1.toHexDOM(undefined, trimHex.checked)); } -function decode(der, offset) { +export function decode(der, offset) { offset = offset || 0; try { const asn1 = ASN1DOM.decode(der, offset); @@ -108,7 +106,7 @@ function decode(der, offset) { text(tree, e); } } -function decodeText(val) { +export function decodeText(val) { try { let der = reHex.test(val) ? Hex.decode(val) : Base64.unarmor(val); decode(der); @@ -117,7 +115,7 @@ function decodeText(val) { dump.innerHTML = ''; } } -function decodeBinaryString(str) { +export function decodeBinaryString(str) { let der; try { if (reHex.test(str)) der = Hex.decode(str); @@ -130,58 +128,39 @@ function decodeBinaryString(str) { } } // set up buttons -id('butDecode').onclick = function () { - decodeText(area.value); -}; -id('butClear').onclick = function () { - area.value = ''; - file.value = ''; - tree.innerHTML = ''; - dump.innerHTML = ''; - hash = window.location.hash = ''; -}; -id('butExample').onclick = function () { - console.log('Loading example:', examples.value); - let request = new XMLHttpRequest(); - request.open('GET', 'examples/' + examples.value, true); - request.onreadystatechange = function () { - if (this.readyState !== 4) return; - if (this.status >= 200 && this.status < 400) { - area.value = this.responseText; - decodeText(this.responseText); - } else { - console.log('Error loading example.'); - } - }; - request.send(); +const butClickHandlers = { + butDecode: () => { + decodeText(area.value); + }, + butClear: () => { + area.value = ''; + file.value = ''; + tree.innerHTML = ''; + dump.innerHTML = ''; + selectDefs.innerHTML = ''; + hash = window.location.hash = ''; + }, + butExample: () => { + console.log('Loading example:', examples.value); + let request = new XMLHttpRequest(); + request.open('GET', 'examples/' + examples.value, true); + request.onreadystatechange = function () { + if (this.readyState !== 4) return; + if (this.status >= 200 && this.status < 400) { + area.value = this.responseText; + decodeText(this.responseText); + } else { + console.log('Error loading example.'); + } + }; + request.send(); + }, }; -// set dark theme depending on OS settings -function setTheme() { - let storedTheme = localStorage.getItem('theme'); - let theme = 'os'; - if (storedTheme) - theme = storedTheme; - selectTheme.value = theme; - if (theme == 'os') { - let prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); - theme = prefersDarkScheme.matches ? 'dark': 'light'; - } - if (theme == 'dark') { - const css1 = id('theme-base'); - const css2 = css1.cloneNode(); - css2.id = 'theme-override'; - css2.href = 'index-' + theme + '.css'; - css1.parentElement.appendChild(css2); - } else { - const css2 = id('theme-override'); - if (css2) css2.remove(); - } +for (const [name, onClick] of Object.entries(butClickHandlers)) { + let elem = id(name); + if (elem) + elem.onclick = onClick; } -setTheme(); -selectTheme.addEventListener('change', function () { - localStorage.setItem('theme', selectTheme.value); - setTheme(); -}); // this is only used if window.FileReader function read(f) { area.value = ''; // clear text area, will get b64 content @@ -235,5 +214,3 @@ selectTag.onchange = function (ev) { let tag = ev.target.selectedOptions[0].value; window.location.href = 'https://rawcdn.githack.com/lapo-luchini/asn1js/' + tag + '/index.html'; }; - -}); diff --git a/int10.js b/int10.js index 981e797..d2da3cd 100644 --- a/int10.js +++ b/int10.js @@ -1,10 +1,10 @@ // Big integer base-10 printing library -// Copyright (c) 2008-2023 Lapo Luchini +// Copyright (c) 2008-2024 Lapo Luchini // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. -// +// // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -13,15 +13,9 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -(typeof define != 'undefined' ? define : function (factory) { 'use strict'; - if (typeof module == 'object') module.exports = factory(); - else window.int10 = factory(); -})(function () { -'use strict'; - let max = 10000000000000; // biggest 10^n integer that can still fit 2^53 when multiplied by 256 -class Int10 { +export class Int10 { /** * Arbitrary length base-10 value. * @param {number} value - Optional initial value (will be 0 otherwise). @@ -110,7 +104,3 @@ class Int10 { } } - -return Int10; - -}); diff --git a/oids.js b/oids.js index 06fb616..28d147b 100644 --- a/oids.js +++ b/oids.js @@ -2,12 +2,7 @@ // which is made by Peter Gutmann and whose license states: // You can use this code in whatever way you want, // as long as you don't try to claim you wrote it. -(typeof define != "undefined" ? define : function (factory) { "use strict"; - if (typeof module == "object") module.exports = factory(); - else window.oids = factory(); -})(function () { -"use strict"; -return { +export const oids = { "0.2.262.1.10": { "d": "Telesec", "c": "Deutsche Telekom" }, "0.2.262.1.10.0": { "d": "extension", "c": "Telesec" }, "0.2.262.1.10.1": { "d": "mechanism", "c": "Telesec" }, @@ -2727,5 +2722,4 @@ return { "1.3.6.1.4.1.40869.1.1.22.3": { "d": "TWCA EV policy", "c": "TWCA Root Certification Authority" }, "2.16.840.1.113733.1.7.23.6": { "d": "VeriSign EV policy", "c": "VeriSign Class 3 Public Primary Certification Authority" }, "2.16.840.1.114171.500.9": { "d": "Wells Fargo EV policy", "c": "Wells Fargo WellsSecure Public Root Certificate Authority" }, -"END": "" -};}); +}; diff --git a/package.json b/package.json index 5b07679..9057665 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "@lapo/asn1js", - "version": "1.3.0", + "version": "2.0.4", "description": "Generic ASN.1 parser/decoder that can decode any valid ASN.1 DER or BER structures.", + "type": "module", "main": "asn1.js", "repository": { "type": "git", @@ -12,48 +13,75 @@ "license": "ISC", "bugs": { "url": "https://github.com/lapo-luchini/asn1js/issues" }, "homepage": "https://lapo.it/asn1js/", - "files": [ "asn1.js", "base64.js", "hex.js", "int10.js", "oids.js" ], + "files": [ "asn1.js", "base64.js", "hex.js", "int10.js", "dom.js", "defs.js", "oids.js", "rfcdef.js", "dumpASN1.js" ], "scripts": { - "lint": "npx eslint asn1.js base64.js hex.js int10.js oids.js tags.js index.js parseRFC.js dumpASN1.js", + "lint": "npx eslint asn1.js base64.js hex.js int10.js dom.js defs.js oids.js rfcdef.js tags.js context.js index.js parseRFC.js dumpASN1.js test.js testDefs.js vite.config.js theme.js", "lint-action": "npx @action-validator/cli .github/workflows/node.js.yml", + "build": "vite build", "serve": "echo 'Connect to http://localhost:3000/' ; npx statik --port 3000 .", - "test": "node test" + "test": "node test", + "testdefs": "node testDefs" + }, + "bin": { + "dumpASN1": "./dumpASN1.js" }, "engines": { - "node": ">=6.4.0" + "node": ">=12.20.0" }, "devDependencies": { - "eslint": "^8.34.0" + "@rollup/wasm-node": "^4.17.2", + "eslint": "^8.34.0", + "htmlparser2": "^9.1.0", + "vite": "^5.2.10", + "vite-plugin-dom": "^1.0.3", + "vite-plugin-singlefile": "^2.0.1" + }, + "overrides": { + "rollup": "npm:@rollup/wasm-node" + }, + "pnpm": { + "overrides": { + "rollup": "npm:@rollup/wasm-node" + } }, "eslintConfig": { "env": { "es6": true, - "amd": true, "browser": true, "node": true }, + "parserOptions": { + "ecmaVersion": 2015, + "sourceType": "module" + }, "extends": [ "eslint:recommended" ], "globals": { "Uint8Array": "readonly" }, "rules": { "strict": [ "error", "function" ], - "indent": [ "error", 4, { "ignoredNodes": [ "Program > ExpressionStatement > CallExpression > FunctionExpression > BlockStatement > ExpressionStatement[directive='use strict']:first-child" ] } ], + "indent": [ "error", 4 ], + "no-trailing-spaces": [ "error" ], "linebreak-style": [ "error", "unix" ], + "eol-last": [ "error", "always" ], "semi": [ "warn", "always" ], "quotes": [ "error", "single", { "avoidEscape": true } ], "no-var": [ "warn" ], - "comma-dangle": [ "error", "never" ] + "comma-dangle": [ "error", "always-multiline" ] }, "overrides": [ { + "files": [ "defs.js" ], + "parserOptions": { + "ecmaVersion": 2020 + } + }, { "files": [ "test.js", "parseRFC.js", "dumpASN1.js" ], "parserOptions": { "ecmaVersion": 2021 }, "rules": { - "strict": [ "error", "global" ], - "comma-dangle": [ "error", "always-multiline" ] + "strict": [ "error", "global" ] } }, { "files": [ "oids.js" ], @@ -62,11 +90,22 @@ "quotes": [ "warn", "double" ] } }, { - "files": [ "tags.js" ], + "files": [ "tags.js", "rfcdef.js" ], "rules": { - "comma-dangle": [ "error", "always-multiline" ], + "indent": [ "error", 2, { "ignoredNodes": [ "Program > ExpressionStatement > CallExpression > FunctionExpression > BlockStatement > ExpressionStatement[directive='use strict']:first-child" ] } ], + "comma-dangle": "off", "quotes": [ "warn", "double" ] } + }, { + "files": [ "defs.js" ], + "parserOptions": { + "ecmaVersion": 2021 + } + }, { + "files": [ "testDefs.js" ], + "parserOptions": { + "ecmaVersion": 2022 + } } ] } diff --git a/parseRFC.js b/parseRFC.js index 41e256c..2d75588 100755 --- a/parseRFC.js +++ b/parseRFC.js @@ -1,8 +1,8 @@ #! /usr/bin/env node -'use strict'; + +import * as fs from 'node:fs'; const - fs = require('fs'), patches = { // to fix some known RFCs' ASN.1 syntax errors 0: [ [ /\n\n[A-Z].*\n\f\n[A-Z].*\n\n/g, '' ], // page change @@ -45,6 +45,17 @@ const 4210: [ [ /^\s+-- .*\r?\n/mg, '' ], // comments ], + 8017: [ // this RFC uses a lot of currently unsupported syntax + [ /ALGORITHM-IDENTIFIER ::= CLASS[^-]+--/, '--' ], + [ /\n +\S+ +ALGORITHM-IDENTIFIER[^\n]+(\n {6}[^\n]+)+\n {3}[}]/g, '' ], + [ /AlgorithmIdentifier [{] ALGORITHM-IDENTIFIER:InfoObjectSet [}] ::=(\n {6}[^\n]+)+\n {3}[}]/, 'AlgorithmIdentifier ::= ANY'], + [ /algorithm +id-[^,\n]+,/g, 'algorithm ANY,' ], + [ / (sha1 {4}HashAlgorithm|mgf1SHA1 {4}MaskGenAlgorithm|pSpecifiedEmpty {4}PSourceAlgorithm|rSAES-OAEP-Default-Identifier {4}RSAES-AlgorithmIdentifier|rSASSA-PSS-Default-Identifier {4}RSASSA-AlgorithmIdentifier) ::= [{](\n( {6}[^\n]+)?)+\n {3}[}]/g, '' ], + [ / ::= AlgorithmIdentifier [{]\s+[{][^}]+[}]\s+[}]/g, ' ::= AlgorithmIdentifier' ], + [ /OCTET STRING[(]SIZE[(]0..MAX[)][)]/g, 'OCTET STRING' ], + [ /emptyString {4}EncodingParameters ::= ''H/g, '' ], + [ /[(]CONSTRAINED BY[^)]+[)]/g, '' ], + ], }; // const reWhitespace = /(?:\s|--(?:[}-]?[^\n}-])*(?:\n|--))*/y; @@ -52,7 +63,7 @@ const reWhitespace = /(?:\s|--(?:-?[^\n-])*(?:\n|--))*/my; const reIdentifier = /[a-zA-Z](?:[-]?[a-zA-Z0-9])*/y; const reNumber = /0|[1-9][0-9]*/y; const reToken = /[(){},[\];]|::=|OPTIONAL|DEFAULT|NULL|TRUE|FALSE|\.\.|OF|SIZE|MIN|MAX|DEFINED BY|DEFINITIONS|TAGS|BEGIN|EXPORTS|IMPORTS|FROM|END/y; -const reType = /ANY|BOOLEAN|INTEGER|(?:BIT|OCTET)\s+STRING|OBJECT\s+IDENTIFIER|SEQUENCE|SET|CHOICE|ENUMERATED|(?:Generalized|UTC)Time|(?:BMP|General|Graphic|IA5|ISO64|Numeric|Printable|Teletex|T61|Universal|UTF8|Videotex|Visible)String/y; +const reType = /ANY|NULL|BOOLEAN|INTEGER|(?:BIT|OCTET)\s+STRING|OBJECT\s+IDENTIFIER|SEQUENCE|SET|CHOICE|ENUMERATED|(?:Generalized|UTC)Time|(?:BMP|General|Graphic|IA5|ISO64|Numeric|Printable|Teletex|T61|Universal|UTF8|Videotex|Visible)String/y; const reTagClass = /UNIVERSAL|APPLICATION|PRIVATE|/y; const reTagType = /IMPLICIT|EXPLICIT|/y; const reTagDefault = /(AUTOMATIC|IMPLICIT|EXPLICIT) TAGS|/y; @@ -213,6 +224,7 @@ class Parser { if (this.tryToken('DEFINED BY')) x.definedBy = this.parseIdentifier(); break; + case 'NULL': case 'BOOLEAN': case 'OCTET STRING': case 'OBJECT IDENTIFIER': @@ -277,12 +289,15 @@ class Parser { this.expectToken(')'); } break; + case 'UTCTime': + case 'GeneralizedTime': + break; default: - x.content = 'TODO:unknown'; + x.warning = 'type unknown'; } } catch (e) { console.log('[debug] parseBuiltinType content', e); - x.content = 'TODO:exception'; + x.warning = 'type exception'; } return x; } @@ -349,8 +364,11 @@ class Parser { } else { if (id in currentMod.values) // defined in local module val = currentMod.values[id].value; - else + else try { val = searchImportedValue(id); + } catch (e) { + this.exception(e.message); + } } } if (v.length) v += '.'; @@ -501,10 +519,10 @@ while ((m = reModuleDefinition.exec(s))) { asn1[currentMod.oid] = currentMod; } /*asn1 = Object.keys(asn1).sort().reduce( - (obj, key) => { + (obj, key) => { obj[key] = asn1[key]; return obj; - }, + }, {} );*/ fs.writeFileSync(process.argv[3], JSON.stringify(asn1, null, 2) + '\n', 'utf8'); diff --git a/release.sh b/release.sh index 37b4c1a..fd511b8 100755 --- a/release.sh +++ b/release.sh @@ -2,12 +2,13 @@ set -e FILES=" asn1.js oids.js defs.js base64.js hex.js int10.js dom.js rfcdef.js test.js tags.js - index.css index-dark.css index.js index.html favicon.svg + context.js index.css index-dark.css index.js index.html index-local.html + favicon.svg tree-icon-light.svg tree-icon-dark.svg README.md LICENSE updateOID.sh check.sh - examples + examples/* " -mtn automate tags it.lapo.asn1js | \ +mtn automate tags 'it.lapo.asn1js{,.*}' | \ awk '/^revision/ { print substr($2, 2, length($2) - 2)}' | \ while read rev; do mtn automate certs $rev | awk -v q='"' ' @@ -19,17 +20,15 @@ mtn automate tags it.lapo.asn1js | \ ' done | sort -r | awk -v q='"' ' BEGIN { - print "(typeof define != " q "undefined" q " ? define : function (factory) { " q "use strict" q ";"; - print " if (typeof module == " q "object" q ") module.exports = factory();"; - print " else window.tags = factory();"; - print "})(function () {"; - print q "use strict" q ";"; - print "return {" + print "export const tags = {" } - { print " " q $2 q ":" q $1 q "," } - END { print "};});" } + { print " " q $2 q ": " q $1 q "," } + END { print "};" } ' > tags.js -type gsha256sum >/dev/null && SHA256=gsha256sum || SHA256=sha256sum +chmod 644 examples/* +type gsha256sum >/dev/null 2>/dev/null && SHA256=gsha256sum || SHA256=sha256sum +pnpm build +cp dist/index.html index-local.html $SHA256 -t $FILES | gpg --clearsign > sha256sums.asc 7z a -tzip -mx=9 asn1js.zip $FILES sha256sums.asc rsync -Pvrtz asn1js.zip $FILES lapo.it:www/asn1js/ diff --git a/rfcdef.js b/rfcdef.js index 4283ba7..6a870b8 100644 --- a/rfcdef.js +++ b/rfcdef.js @@ -1,14 +1,9 @@ -// content parsed from ASN.1 definitions as found in the following RFCs: 5280 5208 3369 3161 2986 4211 4210 +// content parsed from ASN.1 definitions as found in the following RFCs: 5280 5208 3369 3161 2986 4211 4210 8017 // Copyright (C) The IETF Trust (2008) // as far as I can tell this file is allowed under the following clause: // It is acceptable under the current IETF rules (RFC 5378) to modify extracted code if necessary. // https://trustee.ietf.org/about/faq/#reproducing-rfcs -(typeof define != "undefined" ? define : function (factory) { "use strict"; - if (typeof module == "object") module.exports = factory(); - else window.rfcdef = factory(); -})(function () { -"use strict"; -return { +export const rfcdef = { "1.3.6.1.5.5.7.0.18": { "name": "PKIX1Explicit88", "oid": "1.3.6.1.5.5.7.0.18", @@ -1611,14 +1606,12 @@ return { { "id": "utcTime", "name": "UTCTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" }, { "id": "generalTime", "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" } ] } @@ -3387,8 +3380,7 @@ return { "name": "", "type": { "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" } } ], @@ -3405,8 +3397,7 @@ return { "name": "", "type": { "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" } } ], @@ -4499,8 +4490,7 @@ return { "name": "InvalidityDate", "type": { "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" } } } @@ -5571,7 +5561,6 @@ return { "id": "date", "name": "GeneralizedTime", "type": "builtin", - "content": "TODO:unknown", "optional": true }, { @@ -5634,7 +5623,6 @@ return { "id": "date", "name": "GeneralizedTime", "type": "builtin", - "content": "TODO:unknown", "optional": true }, { @@ -6147,14 +6135,12 @@ return { { "id": "utcTime", "name": "UTCTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" }, { "id": "generalTime", "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" } ] } @@ -6669,8 +6655,7 @@ return { { "id": "genTime", "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" }, { "id": "accuracy", @@ -7469,7 +7454,7 @@ return { "name": "", "type": { "name": "NULL", - "type": "defined" + "type": "builtin" } } ] @@ -8350,8 +8335,7 @@ return { "name": "", "type": { "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" } } ], @@ -9654,14 +9638,12 @@ return { { "id": "willBeRevokedAt", "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" }, { "id": "badSinceDate", "name": "GeneralizedTime", - "type": "builtin", - "content": "TODO:unknown" + "type": "builtin" }, { "id": "crlDetails", @@ -9729,7 +9711,7 @@ return { "name": "PKIConfirmContent", "type": { "name": "NULL", - "type": "defined" + "type": "builtin" } }, "InfoTypeAndValue": { @@ -9861,6 +9843,535 @@ return { } } } + }, + "1.2.840.113549.1.1.0.1": { + "name": "PKCS-1", + "oid": "1.2.840.113549.1.1.0.1", + "source": "rfc8017.txt", + "tagDefault": "EXPLICIT", + "imports": { + "2.16.840.1.101.3.4.2": { + "name": "NIST-SHA2", + "oid": "2.16.840.1.101.3.4.2", + "types": [ + "id-sha224", + "id-sha256", + "id-sha384", + "id-sha512", + "id-sha512-224", + "id-sha512-256" + ] + } + }, + "values": { + "pkcs-1": { + "name": "pkcs-1", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1" + }, + "rsaEncryption": { + "name": "rsaEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.1" + }, + "id-RSAES-OAEP": { + "name": "id-RSAES-OAEP", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.7" + }, + "id-pSpecified": { + "name": "id-pSpecified", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.9" + }, + "id-RSASSA-PSS": { + "name": "id-RSASSA-PSS", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.10" + }, + "md2WithRSAEncryption": { + "name": "md2WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.2" + }, + "md5WithRSAEncryption": { + "name": "md5WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.4" + }, + "sha1WithRSAEncryption": { + "name": "sha1WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.5" + }, + "sha224WithRSAEncryption": { + "name": "sha224WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.14" + }, + "sha256WithRSAEncryption": { + "name": "sha256WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.11" + }, + "sha384WithRSAEncryption": { + "name": "sha384WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.12" + }, + "sha512WithRSAEncryption": { + "name": "sha512WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.13" + }, + "sha512-224WithRSAEncryption": { + "name": "sha512-224WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.15" + }, + "sha512-256WithRSAEncryption": { + "name": "sha512-256WithRSAEncryption", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.16" + }, + "id-sha1": { + "name": "id-sha1", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.3.14.3.2.26" + }, + "id-md2": { + "name": "id-md2", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.2.2" + }, + "id-md5": { + "name": "id-md5", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.2.5" + }, + "id-mgf1": { + "name": "id-mgf1", + "type": { + "name": "OBJECT IDENTIFIER", + "type": "builtin" + }, + "value": "1.2.840.113549.1.1.8" + } + }, + "types": { + "AlgorithmIdentifier": { + "name": "AlgorithmIdentifier", + "type": { + "name": "ANY", + "type": "builtin" + } + }, + "HashAlgorithm": { + "name": "HashAlgorithm", + "type": { + "name": "AlgorithmIdentifier", + "type": "defined" + } + }, + "SHA1Parameters": { + "name": "SHA1Parameters", + "type": { + "name": "NULL", + "type": "builtin" + } + }, + "MaskGenAlgorithm": { + "name": "MaskGenAlgorithm", + "type": { + "name": "AlgorithmIdentifier", + "type": "defined" + } + }, + "EncodingParameters": { + "name": "EncodingParameters", + "type": { + "name": "OCTET STRING", + "type": "builtin" + } + }, + "PSourceAlgorithm": { + "name": "PSourceAlgorithm", + "type": { + "name": "AlgorithmIdentifier", + "type": "defined" + } + }, + "RSAPublicKey": { + "name": "RSAPublicKey", + "type": { + "name": "SEQUENCE", + "type": "builtin", + "content": [ + { + "id": "modulus", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "publicExponent", + "name": "INTEGER", + "type": "builtin" + } + ] + } + }, + "RSAPrivateKey": { + "name": "RSAPrivateKey", + "type": { + "name": "SEQUENCE", + "type": "builtin", + "content": [ + { + "id": "version", + "name": "Version", + "type": "defined" + }, + { + "id": "modulus", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "publicExponent", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "privateExponent", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "prime1", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "prime2", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "exponent1", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "exponent2", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "coefficient", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "otherPrimeInfos", + "name": "OtherPrimeInfos", + "type": "defined", + "optional": true + } + ] + } + }, + "Version": { + "name": "Version", + "type": { + "name": "INTEGER", + "type": "builtin", + "content": { + "two-prime": 0, + "multi": 1 + } + } + }, + "OtherPrimeInfos": { + "name": "OtherPrimeInfos", + "type": { + "name": "SEQUENCE", + "type": "builtin", + "typeOf": 1, + "size": [ + 1, + "MAX" + ], + "content": [ + { + "name": "OtherPrimeInfo", + "type": "defined" + } + ] + } + }, + "OtherPrimeInfo": { + "name": "OtherPrimeInfo", + "type": { + "name": "SEQUENCE", + "type": "builtin", + "content": [ + { + "id": "prime", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "exponent", + "name": "INTEGER", + "type": "builtin" + }, + { + "id": "coefficient", + "name": "INTEGER", + "type": "builtin" + } + ] + } + }, + "RSAES-OAEP-params": { + "name": "RSAES-OAEP-params", + "type": { + "name": "SEQUENCE", + "type": "builtin", + "content": [ + { + "id": "hashAlgorithm", + "name": "[0]", + "type": "tag", + "class": "CONTEXT", + "explicit": true, + "content": [ + { + "name": "", + "type": { + "name": "HashAlgorithm", + "type": "defined" + } + } + ], + "default": "sha1" + }, + { + "id": "maskGenAlgorithm", + "name": "[1]", + "type": "tag", + "class": "CONTEXT", + "explicit": true, + "content": [ + { + "name": "", + "type": { + "name": "MaskGenAlgorithm", + "type": "defined" + } + } + ], + "default": "mgf1SHA1" + }, + { + "id": "pSourceAlgorithm", + "name": "[2]", + "type": "tag", + "class": "CONTEXT", + "explicit": true, + "content": [ + { + "name": "", + "type": { + "name": "PSourceAlgorithm", + "type": "defined" + } + } + ], + "default": "pSpecifiedEmpty" + } + ] + } + }, + "RSAES-AlgorithmIdentifier": { + "name": "RSAES-AlgorithmIdentifier", + "type": { + "name": "AlgorithmIdentifier", + "type": "defined" + } + }, + "RSASSA-PSS-params": { + "name": "RSASSA-PSS-params", + "type": { + "name": "SEQUENCE", + "type": "builtin", + "content": [ + { + "id": "hashAlgorithm", + "name": "[0]", + "type": "tag", + "class": "CONTEXT", + "explicit": true, + "content": [ + { + "name": "", + "type": { + "name": "HashAlgorithm", + "type": "defined" + } + } + ], + "default": "sha1" + }, + { + "id": "maskGenAlgorithm", + "name": "[1]", + "type": "tag", + "class": "CONTEXT", + "explicit": true, + "content": [ + { + "name": "", + "type": { + "name": "MaskGenAlgorithm", + "type": "defined" + } + } + ], + "default": "mgf1SHA1" + }, + { + "id": "saltLength", + "name": "[2]", + "type": "tag", + "class": "CONTEXT", + "explicit": true, + "content": [ + { + "name": "", + "type": { + "name": "INTEGER", + "type": "builtin" + } + } + ], + "default": 20 + }, + { + "id": "trailerField", + "name": "[3]", + "type": "tag", + "class": "CONTEXT", + "explicit": true, + "content": [ + { + "name": "", + "type": { + "name": "TrailerField", + "type": "defined" + } + } + ], + "default": "trailerFieldBC" + } + ] + } + }, + "TrailerField": { + "name": "TrailerField", + "type": { + "name": "INTEGER", + "type": "builtin", + "content": { + "trailerFieldBC": 1 + } + } + }, + "RSASSA-AlgorithmIdentifier": { + "name": "RSASSA-AlgorithmIdentifier", + "type": { + "name": "AlgorithmIdentifier", + "type": "defined" + } + }, + "DigestInfo": { + "name": "DigestInfo", + "type": { + "name": "SEQUENCE", + "type": "builtin", + "content": [ + { + "id": "digestAlgorithm", + "name": "DigestAlgorithm", + "type": "defined" + }, + { + "id": "digest", + "name": "OCTET STRING", + "type": "builtin" + } + ] + } + }, + "DigestAlgorithm": { + "name": "DigestAlgorithm", + "type": { + "name": "AlgorithmIdentifier", + "type": "defined" + } + } + } } } -;}); +; diff --git a/tags.js b/tags.js index b3d23ab..bd93442 100644 --- a/tags.js +++ b/tags.js @@ -1,16 +1,17 @@ -(typeof define != "undefined" ? define : function (factory) { "use strict"; - if (typeof module == "object") module.exports = factory(); - else window.tags = factory(); -})(function () { -"use strict"; -return { - "1.2.4":"2022-11-14", - "1.2.3":"2021-10-21", - "1.2.2":"2021-10-21", - "1.2.1":"2020-09-06", - "1.2.0":"2020-07-20", - "1.1.0":"2019-07-13", - "1.0.2":"2018-08-23", - "1.0.1":"2018-08-14", - "1.0.0":"2018-08-14", -};}); +export const tags = { + "2.0.4": "2024-05-08", + "2.0.3": "2024-05-06", + "2.0.2": "2024-04-20", + "2.0.1": "2024-03-28", + "2.0.0": "2024-03-26", + "1.3.0": "2024-03-26", + "1.2.4": "2022-11-14", + "1.2.3": "2021-10-21", + "1.2.2": "2021-10-21", + "1.2.1": "2020-09-06", + "1.2.0": "2020-07-20", + "1.1.0": "2019-07-13", + "1.0.2": "2018-08-23", + "1.0.1": "2018-08-14", + "1.0.0": "2018-08-14", +}; diff --git a/test.js b/test.js index c4945f7..884125a 100755 --- a/test.js +++ b/test.js @@ -1,9 +1,9 @@ #!/usr/bin/env node -'use strict'; + +import { ASN1 } from './asn1.js'; +import { Hex } from './hex.js'; const - Hex = require('./hex.js'), - ASN1 = require('./asn1.js'), all = (process.argv[2] == 'all'); const tests = [ @@ -36,7 +36,7 @@ const tests = [ ['170D3931303530363233343534305A', '1991-05-06 23:45:40 UTC', 'ntop, utc time: UTC'], ['17113931303530363136343534302D30373030', '1991-05-06 16:45:40 UTC-07:00', 'ntop, utc time: PDT'], // inspired by http://luca.ntop.org/Teaching/Appunti/asn1.html - ['0304086E5DC0', 'Exception:\nInvalid BitString with unusedBits=8', 'bit string: invalid unusedBits'], + ['0304086E5DC0', 'Exception:\nError: Invalid BitString with unusedBits=8', 'bit string: invalid unusedBits'], // http://msdn.microsoft.com/en-us/library/windows/desktop/aa379076(v=vs.85).aspx ['30820319308202820201003023310F300D0603550403130654657374434E3110300E060355040A1307546573744F726730819F300D06092A864886F70D010101050003818D00308189028181008FE2412A08E851A88CB3E853E7D54950B3278A2BCBEAB54273EA0257CC6533EE882061A11756C12418E3A808D3BED931F3370B94B8CC43080B7024F79CB18D5DD66D82D0540984F89F970175059C89D4D5C91EC913D72A6B309119D6D442E0C49D7C9271E1B22F5C8DEEF0F1171ED25F315BB19CBC2055BF3A37424575DC90650203010001A08201B4301A060A2B0601040182370D0203310C160A362E302E353336312E323042060A2B0601040182370D0201313430321E260043006500720074006900660069006300610074006500540065006D0070006C0061007400651E080055007300650072305706092B0601040182371514314A30480201090C237669636833642E6A646F6D6373632E6E74746573742E6D6963726F736F66742E636F6D0C154A444F4D4353435C61646D696E6973747261746F720C07636572747265713074060A2B0601040182370D0202316630640201011E5C004D006900630072006F0073006F0066007400200045006E00680061006E006300650064002000430072007900700074006F0067007200610070006800690063002000500072006F00760069006400650072002000760031002E003003010030818206092A864886F70D01090E31753073301706092B0601040182371402040A1E08005500730065007230290603551D2504223020060A2B0601040182370A030406082B0601050507030406082B06010505070302300E0603551D0F0101FF0404030205A0301D0603551D0E041604143C0F73DAF8EF41D83AEABE922A5D2C966A7B9454300D06092A864886F70D01010505000381810047EB995ADF9E700DFBA73132C15F5C24C2E0BFC624AF15660EB86A2EAB2BC4971FE3CBDC63A525ECC7B428616636A1311BBFDDD0FCBF1794901DE55EC7115EC9559FEBA33E14C799A6CBBAA1460F39D444C4C84B760E205D6DA9349ED4D58742EB2426511490B40F065E5288327A9520A0FDF7E57D60DD72689BF57B058F6D1E', '(3 elem)', 'PKCS#10 request'], @@ -86,6 +86,8 @@ const tests = [ ['0420041EE4E3B7ED350CC24D034E436D9A1CB15BB1E328D37062FB82E84618AB0A3C', '(32 byte)\n041EE4E3B7ED350CC24D034E436D9A1CB15BB1E328D37062FB82E84618AB0A3C', 'Do not mix encapsulated and structured octet strings'], // GitHub issue #47 ['181531393835313130363231303632372E332D31323334', '1985-11-06 21:06:27.3 UTC-12:34', 'UTC offsets with minutes'], // GitHub issue #54 ['181331393835313130363231303632372E332B3134', '1985-11-06 21:06:27.3 UTC+14:00', 'UTC offset +13 and +14'], // GitHub issue #54 + ['032100171E83C1B251803F86DD01E9CFA886BE89A7316D8372649AC2231EC669F81A84', n => { if (n.sub != null) return 'Should not decode content: ' + n.sub[0].content(); }, 'Key that resembles an UTCTime'], // GitHub issue #79 + ['171E83C1B251803F86DD01E9CFA886BE89A7316D8372649AC2231EC669F81A84', /^Exception:\nError: Unrecognized time: /, 'Invalid UTCTime'], // GitHub issue #79 ]; let @@ -95,24 +97,28 @@ let tests.forEach(function (t) { const input = t[0], expected = t[1], - comment = t[2], - errorReason = t[3]; + comment = t[2]; let result; try { - result = ASN1.decode(Hex.decode(input)).content(); + let node = ASN1.decode(Hex.decode(input)); + if (typeof expected == 'function') + result = expected(node); + else + result = node.content(); //TODO: check structure, not only first level content } catch (e) { result = 'Exception:\n' + e; } + if (expected instanceof RegExp) + result = expected.test(result) ? null : 'does not match'; ++run; - if (result == expected) { + if (!result || result == expected) { if (all) console.log('\x1B[1m\x1B[32mOK \x1B[39m\x1B[22m ' + comment); - } else if (errorReason) { - ++expErr; - console.log('\x1B[1m\x1B[33mEXP\x1B[39m\x1B[22m ' + comment + ' (' + errorReason + ')' + '\n' + result); } else { ++error; - console.log('\x1B[1m\x1B[31mERR\x1B[39m\x1B[22m ' + comment + '\n' + result); + console.log('\x1B[1m\x1B[31mERR\x1B[39m\x1B[22m ' + comment); + console.log(' \x1B[1m\x1B[34mEXP\x1B[39m\x1B[22m ' + expected.toString().replace(/\n/g, '\n ')); + console.log(' \x1B[1m\x1B[33mGOT\x1B[39m\x1B[22m ' + result.replace(/\n/g, '\n ')); } }); console.log(run + ' tested, ' + expErr + ' expected, ' + error + ' errors.'); diff --git a/testDefs.js b/testDefs.js new file mode 100755 index 0000000..0a00fb5 --- /dev/null +++ b/testDefs.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import { promises as fs } from 'node:fs'; +import { ASN1 } from './asn1.js'; +import { Base64 } from './base64.js'; +import { Defs } from './defs.js'; + +const tot = []; +for await (const file of await fs.opendir('examples')) { + let content = await fs.readFile('examples/' + file.name); + try { + try { // try PEM first + content = Base64.unarmor(content); + } catch (e) { // try DER/BER then + } + let result = ASN1.decode(content); + content = null; + const types = Defs.commonTypes + .map(type => { + const stats = Defs.match(result, type); + return { type, match: stats.recognized / stats.total }; + }) + .sort((a, b) => b.match - a.match); + tot.push([ types[0].match, file.name, types[0].type.description ]); + } catch (e) { + tot.push([ 0, file.name, e.message ]); + } +} +for (const f of tot) + console.log(f[0].toFixed(3) + '\t' + f[1] + '\t' + f[2]); +const avg = tot.map(f => f[0]).reduce((sum, val) => sum + val) / tot.length; +console.log('\x1B[1m\x1B[32m' + (avg * 100).toFixed(3) + '\x1B[39m\x1B[22m%\tAVERAGE'); diff --git a/theme.js b/theme.js new file mode 100644 index 0000000..b9ac518 --- /dev/null +++ b/theme.js @@ -0,0 +1,37 @@ +// set dark theme depending on OS settings +function setTheme(theme) { + if (theme == 'os') { + let prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); + if (prefersDarkScheme.matches) { + theme = 'dark'; + } else { + theme = 'light'; + } + } + document.documentElement.style['color-scheme'] = theme; + document.querySelector('html').setAttribute('data-theme', theme); + // set the theme-color for iOS devices + let bgColor = getComputedStyle(document.documentElement).getPropertyValue('--main-bg-color'); + let metaThemeColor = document.querySelector('meta[name=theme-color]'); + metaThemeColor.setAttribute('content', bgColor); +} +// activate selected theme +let theme = 'os'; +const localStorageTheme = localStorage.getItem('theme'); +if (localStorageTheme) { + theme = localStorageTheme; +} +setTheme(theme); +// add handler to theme selection element +const selectTheme = document.getElementById('theme-select'); +if (selectTheme) { + selectTheme.addEventListener ('change', function () { + localStorage.setItem('theme', selectTheme.value); + setTheme(selectTheme.value); + }); + if (theme == 'light') { + selectTheme.selectedIndex = 2; + } else if (theme == 'dark') { + selectTheme.selectedIndex = 1; + } +} diff --git a/tree-icon-dark.svg b/tree-icon-dark.svg new file mode 100644 index 0000000..75780ca --- /dev/null +++ b/tree-icon-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tree-icon-light.svg b/tree-icon-light.svg new file mode 100644 index 0000000..2adfc4b --- /dev/null +++ b/tree-icon-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/updateOID.sh b/updateOID.sh index bf2fe0e..17eee32 100755 --- a/updateOID.sh +++ b/updateOID.sh @@ -27,12 +27,7 @@ awk -v apos="'" -v q='"' -v url="$URL" ' print "// which is made by Peter Gutmann and whose license states:"; print "// You can use this code in whatever way you want,"; print "// as long as you don" apos "t try to claim you wrote it."; - print "(typeof define != " q "undefined" q " ? define : function (factory) { " q "use strict" q ";"; - print " if (typeof module == " q "object" q ") module.exports = factory();"; - print " else window.oids = factory();"; - print "})(function () {"; - print q "use strict" q ";"; - print "return {"; + print "export const oids = {"; } /^OID/ { oid = $2; } /^Comment/ { comment = $2; } @@ -51,8 +46,7 @@ awk -v apos="'" -v q='"' -v url="$URL" ' } } END { - print "\"END\": \"\"" - print "};});" + print "};" } ' >oids.js echo Conversion completed. diff --git a/updateRFC.sh b/updateRFC.sh index 0839acc..8d7ea1f 100755 --- a/updateRFC.sh +++ b/updateRFC.sh @@ -1,5 +1,5 @@ #/bin/sh -RFCs="5280 5208 3369 3161 2986 4211 4210" +RFCs="5280 5208 3369 3161 2986 4211 4210 8017" downloadRFC() { URL="https://www.ietf.org/rfc/rfc$1.txt" if [ -x /usr/bin/fetch ]; then @@ -25,15 +25,8 @@ cd .. echo "// as far as I can tell this file is allowed under the following clause:" echo "// It is acceptable under the current IETF rules (RFC 5378) to modify extracted code if necessary." echo "// https://trustee.ietf.org/about/faq/#reproducing-rfcs" - cat - < rfcdef.js echo Conversion completed. diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..cfef56e --- /dev/null +++ b/vite.config.js @@ -0,0 +1,37 @@ +import fs from 'node:fs'; +import { defineConfig } from 'vite'; +import { viteSingleFile } from 'vite-plugin-singlefile'; +import pluginDom from 'vite-plugin-dom'; +import { DomUtils } from 'htmlparser2'; + +const removeNodes = [ 'rowExamples' ]; + +const preventSVGEmit = () => { + return { + generateBundle(opts, bundle) { + for (const key in bundle) + if (key.endsWith('.svg')) + delete bundle[key]; + }, + }; +}; + +export default defineConfig({ + plugins: [ + preventSVGEmit(), + pluginDom({ + applyOnMode: true, // all modes + handler: node => { + if (removeNodes.includes(node.attribs.id)) + DomUtils.removeElement(node); + else if (node.name == 'link' && node.attribs.rel == 'icon') + node.attribs.href = 'data:image/svg+xml;base64,' + btoa(fs.readFileSync('favicon.svg', 'ascii').replace(/^([^<]+|<[^s]|