From 240f82cfefc1e9b32ff37cfa831fe7b82a107430 Mon Sep 17 00:00:00 2001 From: Christof Rath Date: Tue, 3 Sep 2024 12:46:15 +0200 Subject: [PATCH] Squashed 'asn1decode/asn1js/' changes from 502ccfe..1383174 1383174 Merge pull request #1 from lapo-luchini/trunk 88cb856 propagate from branch 'it.lapo.asn1js.github-87' (head 1c8c4956cd564008f7eed9da4b4c5339c41877ab) to branch 'it.lapo.asn1js' (head 9f75cfa39a7b636c5f645b78d7c1ead1ca738473) 440b6c9 Recover dark theme changes from GitHub ea3c28fdcc467f2f8c20dd97747e27f3add2605b 1a42055 Use className/classList. 7e187c8 Drop empty lines. 28d859d `btnHideTree` is no longer needed. f4df0c8 Zoom fix via css media query instead of JS 85c143f Drop overridden dependency. (else `npm` is broken; this way `pnpm` has a warning but still works) e1b6d4d `npx svgo *.svg` (except "src" favicon, that could need to be edited) dc0eeba Fix dark collapse icon. 0d4d50a Use a code style closer to trunk branch. 4c1706f propagate from branch 'it.lapo.asn1js' (head 2836ee93ead827f06d611376950b3de3012c96e1) to branch 'it.lapo.asn1js.github-87' (head e3919c3c3e940015d73441c4379c5337ed084457) f2a2ddd Forbid trailing spaces. f03d403 propagate from branch 'it.lapo.asn1js.github-90' (head 82116fdcf1c3b63bcde16e03867bdcad1eb2d051) to branch 'it.lapo.asn1js' (head cc6d74d0a3dc372a270fac7069edd9c55bd4cac9) 9a0a01e Small theme refactoring. 3fd828a Import module natively, not in the HTML. c921739 Fix peer dependencies. 06b6f3c theme.js added to lint 37883bf theme-color set for iOS devices 041d7b6 Theme support for single file 844d85b Theme moved into own js file as of performance issues c1554d2 Common css instead of css replacement 6c01961 Update tags. a490f08 Version 2.0.4. 5930b5b Add CLI binary. ea72cc2 Improve single-file mode. 8e1cfed Add links. aba062a Update tags. 06d6fcc Add instruction for local usage. 51fb9c5 propagate from branch 'it.lapo.asn1js.esm' (head 38124bce4601268d8065c1abd3114977330f32c9) to branch 'it.lapo.asn1js' (head 5ce4e6aff2b2fefa6f9db72f33d8632fb6959607) c4b53e5 Version 2.0.3. a71445e Add defs to npm package. Linting. 5caf633 merge of '7612a59a7e8328fb3cdd183616a8455c779d2aa2' and 'a773502d82c5ca9f65a9d2026a029ad908bcd34c' 8e1cc32 Update require-ESM docs. 7a4d218 Improve ESM usage examples. af73d5d Fix nodejs example usage. 260a7a5 Drop dead code. b7e8538 Force favicon as dataURI in "local" file. f20564c eslint. 9679f24 Add `pnpm` support. (why doesn't it support `npm` override format too?) e6a247e Update ignore files. e3dd726 Advertise now single-file HTML. 5cda4f7 Since ESM is not useable on `file:` protocol, use Vite to generate a single-file `index-local.html`. f4fc630 Only associate buttons' `onClick` when they exist. (to allow removing them) 8dc103c Only install when needed by `lint`. ee8643f Install listed `eslint` to avoid auto-installing a `9.x` version which has breaking changes. 76b5048 Squashed rebase of changes from https://github.com/lapo-luchini/asn1js/pull/87 f2f3e2a Export decode functions. (needed by VSCode extension) 826f20c Add encrypted file example. 7cec5a1 Update tags. 6ae6072 Version 2.0.2. 6f4b911 Improve optional type matching. 89a776d Fix `NULL` support further. 57b9ce4 `NULL` is a built-in type. 7a871a8 Upgrade actions from deprecated Node 16 to 20. 5e7deb8 Install listed `eslint` to avoid auto-installing `9.1.0` which breaks with our configuration format. fd1b6d0 Add new test to check for defs regressions. 8400fa7 Allow specifying expected type. 3f5e5fa Add support for data URI input. 20c1581 propagate from branch 'it.lapo.asn1js' (head 2af3ae813887be59bf3e2e123a12e77592921fa8) to branch 'it.lapo.asn1js.esm' (head 95bda12bb582ab39529f02626bab93554a4dd214) 6fa69ff Add X.509 SubjectPublicKeyInfo to default types. 3bc6092 Add missing types to RFC parser. 6052316 Improve hover highlight. b6c0059 Use `classList` instead of manipulating `className`. bbd561f Fix sha256sums. ce7a05a Lint new file too. 513085b propagate from branch 'it.lapo.asn1js' (head 2c2471b462a45fe5bbd48c9d9347f2df5ab039d9) to branch 'it.lapo.asn1js.esm' (head 8fc43cda40cb66f028d050df54b1e506736d094a) ad3d920 `eslint --fix` cce28c5 Print defs also in pretty tring mode. dc57878 Drop old forgotten "collapse" icon. e41f826 Add context menu also on the tree on the left. 270561b Rename context menu values. Add B64. Put optional one last to keep button positions. 5908ead Avoid inline javascript. (for future usage with CSP header) 4a03d93 Refactor context menu to use existing methods. (and avoid re-parsing parsed data) fe04d97 Use blocks instead of `
` in context menu. 0eb4fcf Improve hex dump capabilities of ASN.1 nodes. 3291c10 File forgot in previous (refactor) commit. 0c2dc0c Refactor contextual menu in a different module. c2ebbf4 propagate from branch 'it.lapo.asn1js.esm' (head 1f81e568f0723f8ce942b435f81d205e157789ce) to branch 'it.lapo.asn1js.github-82' (head 7107a48abf688173dcdf7bd716d415c8edd85f46) e3fca65 Update credits. be8d97a propagate from branch 'it.lapo.asn1js' (head dd18afd1c4344811526fc928f3975fe414584018) to branch 'it.lapo.asn1js.github-82' (head 27ed876d23a39e4ec2bfd2f6233f38610720add2) 1fcc683 Force examples permissions. aa1f1c3 propagate from branch 'it.lapo.asn1js' (head 741a5e9cfd4ef4e45f1e5b505e02c4142e82dd8d) to branch 'it.lapo.asn1js.esm' (head 4d3ff39de9922b95cbd11c2534c9188ff1db72f0) 2654e9c Add very basic PKCS#1 support and RSA key examples. 5027c72 propagate from branch 'it.lapo.asn1js' (head a9524436000d6561058ba0ae8e2a58a6c49837fd) to branch 'it.lapo.asn1js.esm' (head a6b77044e2cf0f91cb663d9d1fbfbca7f66c2d21) 8f048ac When trying to parse encapsulated values, check that the content can be parsed as well. 405c036 Drop dead code. 7a51b8a Fix tests. 26d3a72 Throw exceptions, not strings. 9ff23e1 Use HTTPS links. c894e6a Index only needs to import a single module. 8663924 Version 2.0.1. 70426d2 Update tags. 65c222d Update browser support warnings. b7f47f6 Improve README. f220f66 propagate from branch 'it.lapo.asn1js' (head 263fa0cb3936ec99c2be2c51a2265cadfbded687) to branch 'it.lapo.asn1js.esm' (head c360c489896f2e9ec2c280e6d55b03a0581252c2) 690364a Improve type matching. ae4458e Update example usage. 1b3078e Version 2.0.0 in order to signal this is a breaking change. ae7ee7b Unbreak tests. 72c71f1 Update copyright. c1f4dff Convert to ES6 Modules (ESM). 429f752 Indention fixed 1d7f90b Fix: Show context menu at correct position e6dbc08 Negative list for set and sequence dcf1b13 New: Align context menu to left af5e9e7 New: Copy as pretty d5465fc Fix: Copy the string to clipboard e7d2bfd Context menu initially hidden 17dd53a Context menu and copyAsString added c7cf816 hex copy on click 7c2a63b Update `eslint` rules a bit. git-subtree-dir: asn1decode/asn1js git-subtree-split: 138317433032d91debc2eea51536beadb757835d --- .github/workflows/node.js.yml | 8 +- .gitignore | 2 + .mtn-ignore | 2 + LICENSE | 2 +- README.md | 71 +++-- asn1.js | 81 +++-- base64.js | 16 +- check.sh | 2 +- context.js | 53 ++++ defs.js | 51 +-- dom.js | 81 +++-- dumpASN1.js | 55 ++-- examples/cms-password.p7m | 9 + examples/pkcs1.pem | 18 ++ examples/pkcs8-rsa.pem | 18 ++ favicon.svg | 3 +- hex.js | 16 +- index-dark.css | 42 --- index.css | 206 ++++++++++-- index.html | 48 +-- index.js | 111 +++---- int10.js | 16 +- oids.js | 10 +- package.json | 65 +++- parseRFC.js | 34 +- release.sh | 23 +- rfcdef.js | 579 ++++++++++++++++++++++++++++++++-- tags.js | 33 +- test.js | 30 +- testDefs.js | 32 ++ theme.js | 37 +++ tree-icon-dark.svg | 1 + tree-icon-light.svg | 1 + updateOID.sh | 10 +- updateRFC.sh | 13 +- vite.config.js | 37 +++ 36 files changed, 1350 insertions(+), 466 deletions(-) create mode 100644 context.js create mode 100644 examples/cms-password.p7m create mode 100644 examples/pkcs1.pem create mode 100644 examples/pkcs8-rsa.pem delete mode 100644 index-dark.css create mode 100755 testDefs.js create mode 100644 theme.js create mode 100644 tree-icon-dark.svg create mode 100644 tree-icon-light.svg create mode 100644 vite.config.js 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]|