diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..543659d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +*.js text eol=lf +*.sh text eol=lf +*.json text eol=lf +*.ts text eol=lf +*.html text eol=lf +*.min.js binary +vendor/* binary + +.github export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.npmignore export-ignore +.npmrc export-ignore +build export-ignore +tests export-ignore +playwright.config.ts export-ignore +tsconfig.json export-ignore +package-lock.json export-ignore \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..728d5f7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: brainfoolong +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8bee0a0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: JS tests +on: + push: + pull_request: +jobs: + tests: + strategy: + matrix: + node-version: [ '12.x', '14.x', '16.x', '18.x' ,'20.x' ,'latest' ] + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Run tests on ${{ matrix.node-version }} + run: node tests/test-all.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06ce7d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea +.npmrc +node_modules +package-lock.json +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0d253ae --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +* +!dist/* +!src/* +!CHANGELOG.md \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08fa700 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 BrainFooLong (Roland Eigelsreiter) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a302b0 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# JavaScript/TypeScript Implementation of Ascon + +[![Tests](https://github.com/brainfoolong/js-ascon/actions/workflows/tests.yml/badge.svg)](https://github.com/brainfoolong/js-ascon/actions/workflows/tests.yml) + +This is a JavaScript/TypeScript (JS compiled from TypeScript) implementation of Ascon v1.2, an authenticated cipher and hash function. +It allows to encrypt and decrypt any kind of message. At kind be somewhat seen as the successor to AES encryption. +Heavily inspired by the python implementation of Ascon by https://github.com/meichlseder/pyascon + +## About Ascon + +Ascon is a family of [authenticated encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) (AEAD) +and [hashing](https://en.wikipedia.org/wiki/Cryptographic_hash_function) algorithms designed to be lightweight and easy +to implement, even with added countermeasures against side-channel attacks. +It was designed by a team of cryptographers from Graz University of Technology, Infineon Technologies, and Radboud +University: Christoph Dobraunig, Maria Eichlseder, Florian Mendel, and Martin Schläffer. + +Ascon has been selected as the standard for lightweight cryptography in +the [NIST Lightweight Cryptography competition (2019–2023)](https://csrc.nist.gov/projects/lightweight-cryptography) and +as the primary choice for lightweight authenticated encryption in the final portfolio of +the [CAESAR competition (2014–2019)](https://competitions.cr.yp.to/caesar-submissions.html). + +Find more information, including the specification and more implementations here: + +https://ascon.iaik.tugraz.at/ + +## About me + +I have made library for AES PHP/JS encryption already in the past. Bit juggling is somewhat cool, in a really nerdy way. +I like the Ascon implementation and it at the time of writing, a JS implementation was missing. So i made one. Would be +cool if you leave a follow or spend some virtual coffee. + +## Usage + +For more demos see in folder `demo`. + +```js +``` + +See `tests/performance.html` for some tests with various message data size. + +``` +# no scientific tests, just executed on my local machine, results depend on your machine +# a "cycle" is one encryption and one decryption + +### 10 cycles with 32 byte message data and 128 byte associated data ### +Total Time: 0.080 seconds + +### 10 cycles with 128 byte message data and 512 byte associated data ### +Total Time: 0.260 seconds + +### 10 cycles with 1024 byte message data and 2048 byte associated data ### +Total Time: 1.370 seconds + +### 10 cycles with 4096 byte message data and 0 byte associated data ### +Total Time: 2.869 seconds +``` + +## Implemented Algorithms + +This is a simple reference implementation of Ascon v1.2 as submitted to the NIST LWC competition that includes + +* Authenticated encryption/decryption with the following 3 variants: + + - `Ascon-128` + - `Ascon-128a` + - `Ascon-80pq` + +* Hashing algorithms including 4 hash function variants with fixed 256-bit (`Hash`) or variable (`Xof`) output lengths: + + - `Ascon-Hash` + - `Ascon-Hasha` + - `Ascon-Xof` + - `Ascon-Xofa` + +* Message authentication codes including 5 MAC variants (from https://eprint.iacr.org/2021/1574, not part of the LWC + proposal) with fixed 128-bit (`Mac`) or variable (`Prf`) output lengths, including a variant for short messages of up + to 128 bits (`PrfShort`). + + - `Ascon-Mac` + - `Ascon-Maca` + - `Ascon-Prf` + - `Ascon-Prfa` + - `Ascon-PrfShort` \ No newline at end of file diff --git a/build/dist.js b/build/dist.js new file mode 100644 index 0000000..c0062f0 --- /dev/null +++ b/build/dist.js @@ -0,0 +1,17 @@ +// create all required dist files +const fs = require('fs') + +const packageJson = require('../package.json') +const srcFile = __dirname + '/../dist/ascon.js' +let contents = fs.readFileSync(srcFile).toString() +contents = '// js-ascon v' + packageJson.version + ' @ ' + packageJson.homepage + '\n' + contents +contents = contents.replace(/export default class JsAscon/, 'class JsAscon') +contents += ` +if (typeof module !== 'undefined' && module.exports) { + module.exports = JsAscon +} +if(typeof crypto === 'undefined' && typeof global !== 'undefined'){ + global.crypto = require('crypto') +} +` +fs.writeFileSync(srcFile, contents) \ No newline at end of file diff --git a/dist/ascon.js b/dist/ascon.js new file mode 100644 index 0000000..47adc74 --- /dev/null +++ b/dist/ascon.js @@ -0,0 +1,607 @@ +// js-ascon v1.0.0 @ https://github.com/brainfoolong/js-ascon +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Javascript / Typescript implementation of Ascon v1.2 + * Heavily inspired by the python implementation of https://github.com/meichlseder/pyascon + * @link https://github.com/brainfoolong/js-ascon + * @author BrainFooLong (Roland Eigelsreiter) + * @version 1.0.0 + */ +class JsAscon { + /** + * Encrypt any message to a hex string + * @param {string|Uint8Array} secretKey Your "password", so to say + * @param {any} messageToEncrypt Any type of message + * @param {any} associatedData Any type of associated data + * @param {string} cipherVariant See JsAscon.encrypt() + * @return {string} + */ + static encryptToHex(secretKey, messageToEncrypt, associatedData = null, cipherVariant = 'Ascon-128') { + const key = JsAscon.hash(secretKey, 'Ascon-Xof', cipherVariant === 'Ascon-80pq' ? 20 : 16); + const nonce = crypto.getRandomValues(new Uint8Array(16)); + const ciphertext = JsAscon.encrypt(key, nonce, associatedData !== null ? JSON.stringify(associatedData) : '', JSON.stringify(messageToEncrypt), cipherVariant); + return JsAscon.byteArrayToHex(ciphertext).substring(2) + JsAscon.byteArrayToHex(nonce).substring(2); + } + /** + * Decrypt any message from a hex string previously generated with encryptToHex + * @param {string|Uint8Array} secretKey Your "password", so to say + * @param {string} hexStr Any type of message + * @param {any} associatedData Any type of associated data + * @param {string} cipherVariant See JsAscon.encrypt() + * @return {any} Null indicate unsuccessfull decrypt + */ + static decryptFromHex(secretKey, hexStr, associatedData = null, cipherVariant = 'Ascon-128') { + const key = JsAscon.hash(secretKey, 'Ascon-Xof', cipherVariant === 'Ascon-80pq' ? 20 : 16); + const hexData = Uint8Array.from(hexStr.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); + const plaintextMessage = JsAscon.decrypt(key, hexData.slice(-16), associatedData !== null ? JSON.stringify(associatedData) : '', hexData.slice(0, -16), cipherVariant); + return plaintextMessage !== null ? JSON.parse(JsAscon.byteArrayToStr(plaintextMessage)) : null; + } + /** + * Ascon encryption + * @param {string|Uint8Array|number[]} key A string or byte array of a length 16 (for Ascon-128, Ascon-128a; 128-bit security) or + * 20 (for Ascon-80pq; 128-bit security) + * @param {string|Uint8Array|number[]} nonce A string or byte array of a length of 16 bytes (must not repeat for the same key!) + * @param {string|Uint8Array|number[]} associatedData A string or byte array of any length + * @param {string|Uint8Array|number[]} plaintext A string or byte array of any length + * @param {string} variant "Ascon-128", "Ascon-128a", or "Ascon-80pq" (specifies key size, rate and number of + * rounds) + * @return {Uint8Array} + */ + static encrypt(key, nonce, associatedData, plaintext, variant = 'Ascon-128') { + key = JsAscon.anyToByteArray(key); + const keyLength = key.length; + nonce = JsAscon.anyToByteArray(nonce); + const nonceLength = nonce.length; + JsAscon.assertInArray(variant, ['Ascon-128', 'Ascon-128a', 'Ascon-80pq'], 'Encrypt variant'); + if (['Ascon-128', 'Ascon-128a'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length'); + } + else { + JsAscon.assert(keyLength === 20 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length'); + } + const data = []; + const keySizeBits = keyLength * 8; + const permutationRoundsA = 12; + const permutationRoundsB = variant === 'Ascon-128a' ? 8 : 6; + const rate = variant === 'Ascon-128a' ? 16 : 8; + JsAscon.initialize(data, keySizeBits, rate, permutationRoundsA, permutationRoundsB, key, nonce); + associatedData = JsAscon.anyToByteArray(associatedData); + JsAscon.processAssociatedData(data, permutationRoundsB, rate, associatedData); + plaintext = JsAscon.anyToByteArray(plaintext); + const ciphertext = JsAscon.processPlaintext(data, permutationRoundsB, rate, plaintext); + const tag = JsAscon.finalize(data, permutationRoundsA, rate, key); + return JsAscon.concatByteArrays(ciphertext, tag); + } + /** + * Ascon decryption + * @param {string|Uint8Array|number[]} key A string or byte array of a length 16 (for Ascon-128, Ascon-128a; 128-bit security) or + * 20 (for Ascon-80pq; 128-bit security) + * @param {string|Uint8Array|number[]} nonce A string or byte array of a length of 16 bytes (must not repeat for the same key!) + * @param {string|Uint8Array|number[]} associatedData A string or byte array of any length + * @param {string|Uint8Array|number[]} ciphertextAndTag A string or byte array of any length + * @param {string} variant "Ascon-128", "Ascon-128a", or "Ascon-80pq" (specifies key size, rate and number of + * rounds) + * @return {Uint8Array|null} Returns plaintext as byte array or NULL when cannot decrypt + */ + static decrypt(key, nonce, associatedData, ciphertextAndTag, variant = 'Ascon-128') { + key = JsAscon.anyToByteArray(key); + const keyLength = key.length; + nonce = JsAscon.anyToByteArray(nonce); + const nonceLength = nonce.length; + JsAscon.assertInArray(variant, ['Ascon-128', 'Ascon-128a', 'Ascon-80pq'], 'Encrypt variant'); + if (['Ascon-128', 'Ascon-128a'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length'); + } + else { + JsAscon.assert(keyLength === 20 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length'); + } + const data = []; + const keySizeBits = keyLength * 8; + const permutationRoundsA = 12; + const permutationRoundsB = variant === 'Ascon-128a' ? 8 : 6; + const rate = variant === 'Ascon-128a' ? 16 : 8; + JsAscon.initialize(data, keySizeBits, rate, permutationRoundsA, permutationRoundsB, key, nonce); + associatedData = JsAscon.anyToByteArray(associatedData); + JsAscon.processAssociatedData(data, permutationRoundsB, rate, associatedData); + ciphertextAndTag = JsAscon.anyToByteArray(ciphertextAndTag); + const ciphertext = ciphertextAndTag.slice(0, -16); + const ciphertextTag = ciphertextAndTag.slice(-16); + const plaintext = JsAscon.processCiphertext(data, permutationRoundsB, rate, ciphertext); + const tag = JsAscon.finalize(data, permutationRoundsA, rate, key); + if (JsAscon.byteArrayToHex(tag) === JsAscon.byteArrayToHex(ciphertextTag)) { + return plaintext; + } + return null; + } + /** + * Ascon hash function and extendable-output function + * @param {string|Uint8Array} message A string or byte array + * @param {string} variant "Ascon-Hash", "Ascon-Hasha" (both with 256-bit output for 128-bit security), "Ascon-Xof", + * or "Ascon-Xofa" (both with arbitrary output length, security=min(128, bitlen/2)) + * @param {number} hashLength The requested output bytelength (must be 32 for variant "Ascon-Hash"; can be arbitrary + * for Ascon-Xof, but should be >= 32 for 128-bit security) + * @return {Uint8Array} The byte array representing the hash tag + */ + static hash(message, variant = 'Ascon-Hash', hashLength = 32) { + JsAscon.assertInArray(variant, ['Ascon-Hash', 'Ascon-Hasha', 'Ascon-Xof', 'Ascon-Xofa'], 'Hash variant'); + if (['Ascon-Hash', 'Ascon-Hasha'].indexOf(variant) > -1) { + JsAscon.assert(hashLength === 32, 'Incorrect hash length'); + } + message = JsAscon.anyToByteArray(message); + const messageLength = message.length; + const permutationRoundsA = 12; + const permutationRoundsB = ['Ascon-Hasha', 'Ascon-Xofa'].indexOf(variant) > -1 ? 8 : 12; + const rate = 8; + const data = JsAscon.byteArrayToState(JsAscon.concatByteArrays([0, rate * 8, permutationRoundsA, permutationRoundsA - permutationRoundsB], [0, 0, ['Ascon-Hash', 'Ascon-Hasha'].indexOf(variant) > -1 ? 1 : 0, 0], // tagspec, + new Uint8Array(32))); + JsAscon.debug('initial value', data, true); + JsAscon.permutation(data, permutationRoundsA); + JsAscon.debug('initialization', data, true); + // message processing (absorbing) + const messagePadded = JsAscon.concatByteArrays(message, [0x80], new Uint8Array(rate - (messageLength % rate) - 1)); + const messagePaddedLength = messagePadded.length; + // first s-1 blocks + for (let block = 0; block < messagePaddedLength - rate; block += rate) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block); + JsAscon.permutation(data, permutationRoundsB); + } + // last block + const block = messagePaddedLength - rate; + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block); + JsAscon.debug('process message', data); + // finalization (squeezing) + let hash = []; + JsAscon.permutation(data, permutationRoundsA); + while (hash.length < hashLength) { + hash = hash.concat(...JsAscon.bigIntToByteArray(data[0])); + JsAscon.permutation(data, permutationRoundsB); + } + JsAscon.debug('finalization', data); + return new Uint8Array(hash); + } + /** + * Ascon message authentication code (MAC) and pseudorandom function (PRF) + * @param {string|number[]|Uint8Array} key A string or byte array of a length of 16 bytes + * @param {string|number[]|Uint8Array} message A string or byte array (<= 16 for "Ascon-PrfShort") + * @param {string} variant "Ascon-Mac", "Ascon-Maca" (both 128-bit output, arbitrarily long input), "Ascon-Prf", + * "Ascon-Prfa" (both arbitrarily long input and output), or "Ascon-PrfShort" (t-bit output for t<=128, m-bit + * input for m<=128) + * @param {number} tagLength The requested output bytelength l/8 (must be <=16 for variants "Ascon-Mac", "Ascon-Maca", + * and "Ascon-PrfShort", arbitrary for "Ascon-Prf", "Ascon-Prfa"; should be >= 16 for 128-bit security) + * @return {Uint8Array} The byte array representing the authentication tag + */ + static mac(key, message, variant = 'Ascon-Mac', tagLength = 16) { + JsAscon.assertInArray(variant, ['Ascon-Mac', 'Ascon-Prf', 'Ascon-Maca', 'Ascon-Prfa', 'Ascon-PrfShort'], 'Mac variant'); + key = JsAscon.anyToByteArray(key); + const keyLength = key.length; + message = JsAscon.anyToByteArray(message); + const messageLength = message.length; + if (['Ascon-Mac', 'Ascon-Maca'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16 && tagLength <= 16, 'Incorrect key length'); + } + else if (['Ascon-Prf', 'Ascon-Prfa'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16, 'Incorrect key length'); + } + else if (variant === 'Ascon-PrfShort') { + JsAscon.assert(messageLength <= 16, 'Message to long for variant ' + variant); + JsAscon.assert(keyLength === 16 && tagLength <= 16 && messageLength <= 16, 'Incorrect key length'); + } + const permutationRoundsA = 12; + const permutationRoundsB = ['Ascon-Prfa', 'Ascon-Maca'].indexOf(variant) > -1 ? 8 : 12; + const messageBlockSize = ['Ascon-Prfa', 'Ascon-Maca'].indexOf(variant) > -1 ? 40 : 32; + const rate = 16; + if (variant === 'Ascon-PrfShort') { + const data = JsAscon.byteArrayToState(JsAscon.concatByteArrays([keyLength * 8, messageLength * 8, permutationRoundsA + 64, tagLength * 8, 0, 0, 0, 0], key, message, new Uint8Array(16 - messageLength))); + JsAscon.debug('initial value', data); + JsAscon.permutation(data, permutationRoundsA); + JsAscon.debug('process message', data); + data[3] ^= JsAscon.byteArrayToBigInt(key, 0); + data[4] ^= JsAscon.byteArrayToBigInt(key, 8); + return new Uint8Array([...JsAscon.bigIntToByteArray(data[3]), ...JsAscon.bigIntToByteArray(data[4])]); + } + const data = JsAscon.byteArrayToState(JsAscon.concatByteArrays([keyLength * 8, rate * 8, permutationRoundsA + 128, permutationRoundsA - permutationRoundsB], [0, 0, 0, ['Ascon-Mac', 'Ascon-Maca'].indexOf(variant) > -1 ? 128 : 0], // tagspec + key, new Uint8Array(16))); + JsAscon.debug('initial value', data); + JsAscon.permutation(data, permutationRoundsA); + JsAscon.debug('initialization', data); + // message processing (absorbing) + const messagePadded = JsAscon.concatByteArrays(message, [0x80], new Uint8Array(messageBlockSize - (messageLength % messageBlockSize) - 1)); + const messagePaddedLength = messagePadded.length; + const iterations = ['Ascon-Prfa', 'Ascon-Maca'].indexOf(variant) > -1 ? 4 : 3; + // first s-1 blocks + for (let block = 0; block < messagePaddedLength - messageBlockSize; block += messageBlockSize) { + for (let i = 0; i <= iterations; i++) { + data[i] ^= JsAscon.byteArrayToBigInt(messagePadded, block + (i * 8)); + } + JsAscon.permutation(data, permutationRoundsB); + } + // last block + const block = messagePaddedLength - messageBlockSize; + for (let i = 0; i <= iterations; i++) { + data[i] ^= JsAscon.byteArrayToBigInt(messagePadded, block + (i * 8)); + } + data[4] ^= 1n; + JsAscon.debug('process message', data); + // finalization (squeezing) + let tag = []; + JsAscon.permutation(data, permutationRoundsA); + while (tag.length < tagLength) { + tag = tag.concat(...JsAscon.bigIntToByteArray(data[0]), ...JsAscon.bigIntToByteArray(data[1])); + JsAscon.permutation(data, permutationRoundsB); + } + JsAscon.debug('finalization', data); + return new Uint8Array(tag); + } + /** + * Ascon initialization phase - internal helper function + * @param {BigInt[]} data Ascon state, a list of 5 64-bit integers + * @param {number} keySize Key size in bits + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {number} permutationRoundsA Number of initialization/finalization rounds for permutation + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {Uint8Array} key A bytes object of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; + * 128-bit security) + * @param {Uint8Array} nonce A bytes object of size 16 + */ + static initialize(data, keySize, rate, permutationRoundsA, permutationRoundsB, key, nonce) { + JsAscon.byteArrayToState(JsAscon.concatByteArrays([keySize, rate * 8, permutationRoundsA, permutationRoundsB], new Uint8Array(20 - key.length), key, nonce), data); + JsAscon.debug('initial value', data); + JsAscon.permutation(data, permutationRoundsA); + const zeroKey = JsAscon.byteArrayToState(JsAscon.concatByteArrays(new Uint8Array(40 - key.length), key)); + for (let i = 0; i <= 4; i++) { + data[i] ^= zeroKey[i]; + } + JsAscon.debug('initialization', data); + } + /** + * Ascon associated data processing phase - internal helper function + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} associatedData A byte array of any length + */ + static processAssociatedData(data, permutationRoundsB, rate, associatedData) { + if (associatedData.length) { + // message processing (absorbing) + const messagePadded = JsAscon.concatByteArrays(associatedData, [0x80], new Uint8Array(rate - (associatedData.length % rate) - 1)); + const messagePaddedLength = messagePadded.length; + for (let block = 0; block < messagePaddedLength; block += rate) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block); + if (rate === 16) { + data[1] ^= JsAscon.byteArrayToBigInt(messagePadded, block + 8); + } + JsAscon.permutation(data, permutationRoundsB); + } + } + data[4] ^= 1n; + JsAscon.debug('process associated data', data); + } + /** + * Ascon plaintext processing phase (during encryption) - internal helper function + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} plaintext A byte array of any length + * @return {Uint8Array} Returns the ciphertext as byte array + */ + static processPlaintext(data, permutationRoundsB, rate, plaintext) { + const lastLen = plaintext.length % rate; + const messagePadded = JsAscon.concatByteArrays(plaintext, [0x80], new Uint8Array(rate - lastLen - 1)); + const messagePaddedLength = messagePadded.length; + let ciphertext = new Uint8Array(0); + // first t-1 blocks + for (let block = 0; block < messagePaddedLength - rate; block += rate) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block); + ciphertext = JsAscon.concatByteArrays(ciphertext, JsAscon.bigIntToByteArray(data[0])); + if (rate === 16) { + data[1] ^= JsAscon.byteArrayToBigInt(messagePadded, block + 8); + ciphertext = JsAscon.concatByteArrays(ciphertext, JsAscon.bigIntToByteArray(data[1])); + } + JsAscon.permutation(data, permutationRoundsB); + } + // last block + const block = messagePaddedLength - rate; + if (rate === 8) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block); + ciphertext = JsAscon.concatByteArrays(ciphertext, JsAscon.bigIntToByteArray(data[0]).slice(0, lastLen)); + } + else if (rate === 16) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block); + data[1] ^= JsAscon.byteArrayToBigInt(messagePadded, block + 8); + ciphertext = JsAscon.concatByteArrays(ciphertext, JsAscon.bigIntToByteArray(data[0]).slice(0, lastLen > 8 ? 8 : lastLen), JsAscon.bigIntToByteArray(data[1]).slice(0, lastLen - 8 < 0 ? 0 : lastLen - 8)); + } + JsAscon.debug('process plaintext', data); + return ciphertext; + } + /** + * Ascon plaintext processing phase (during encryption) - internal helper function + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} ciphertext A byte array of any length + * @return {Uint8Array} Returns the ciphertext as byte array + */ + static processCiphertext(data, permutationRoundsB, rate, ciphertext) { + const lastLen = ciphertext.length % rate; + const messagePadded = JsAscon.concatByteArrays(ciphertext, new Uint8Array(rate - lastLen)); + const messagePaddedLength = messagePadded.length; + let plaintext = new Uint8Array(0); + // first t-1 blocks + for (let block = 0; block < messagePaddedLength - rate; block += rate) { + let ci = JsAscon.byteArrayToBigInt(messagePadded, block); + plaintext = JsAscon.concatByteArrays(plaintext, JsAscon.bigIntToByteArray(data[0] ^ ci)); + data[0] = ci; + if (rate === 16) { + ci = JsAscon.byteArrayToBigInt(messagePadded, block + 8); + plaintext = JsAscon.concatByteArrays(plaintext, JsAscon.bigIntToByteArray(data[1] ^ ci)); + data[1] = ci; + } + JsAscon.permutation(data, permutationRoundsB); + } + // last block + const block = messagePaddedLength - rate; + if (rate === 8) { + let ci = JsAscon.byteArrayToBigInt(messagePadded, block); + plaintext = JsAscon.concatByteArrays(plaintext, JsAscon.bigIntToByteArray(ci ^ data[0]).slice(0, lastLen)); + const padding = 0x80n << BigInt((rate - lastLen - 1) * 8); + const mask = BigInt('0xFFFFFFFFFFFFFFFF') >> BigInt(lastLen * 8); + data[0] = ci ^ (data[0] & mask) ^ padding; + } + else if (rate === 16) { + const lastLenWord = lastLen % 8; + const padding = 0x80n << BigInt((8 - lastLenWord - 1) * 8); + const mask = BigInt('0xFFFFFFFFFFFFFFFF') >> BigInt(lastLenWord * 8); + let ciA = JsAscon.byteArrayToBigInt(messagePadded, block); + let ciB = JsAscon.byteArrayToBigInt(messagePadded, block + 8); + plaintext = JsAscon.concatByteArrays(plaintext, JsAscon.concatByteArrays(JsAscon.bigIntToByteArray(data[0] ^ ciA), JsAscon.bigIntToByteArray(data[1] ^ ciB)).slice(0, lastLen)); + if (lastLen < 8) { + data[0] = ciA ^ (data[0] & mask) ^ padding; + } + else { + data[0] = ciA; + data[1] = ciB ^ (data[1] & mask) ^ padding; + } + } + JsAscon.debug('process ciphertext', data); + return plaintext; + } + /** + * Ascon finalization phase - internal helper function + * + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsA Number of initialization/finalization rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} key A bytes array of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; + * 128-bit security) + * @return {Uint8Array} The tag as a byte array + */ + static finalize(data, permutationRoundsA, rate, key) { + let zeroFilledKey = key; + if (key.length > 16) { + const newLen = key.length + 24 - key.length; + zeroFilledKey = new Uint8Array(newLen); + zeroFilledKey.set(key); + } + let index = (rate / 8) | 0; + data[index++] ^= JsAscon.byteArrayToBigInt(key, 0); + data[index++] ^= JsAscon.byteArrayToBigInt(key, 8); + data[index++] ^= JsAscon.byteArrayToBigInt(zeroFilledKey, 16); + JsAscon.permutation(data, permutationRoundsA); + data[3] ^= JsAscon.byteArrayToBigInt(key, -16); + data[4] ^= JsAscon.byteArrayToBigInt(key, -8); + JsAscon.debug('finalization', data); + return JsAscon.concatByteArrays(JsAscon.bigIntToByteArray(data[3]), JsAscon.bigIntToByteArray(data[4])); + } + /** + * Ascon core permutation for the sponge construction - internal helper function + * @param {BigInt[]} data Ascon state, a list of 5 64-bit integers + * @param {number} rounds + */ + static permutation(data, rounds = 1) { + JsAscon.assert(rounds <= 12, 'Permutation rounds must be <= 12'); + JsAscon.debug('permutation input', data, true); + for (let round = 12 - rounds; round < 12; round++) { + // add round constants + data[2] ^= BigInt(0xf0 - round * 0x10 + round); + JsAscon.debug('round constant addition', data, true); + // substitution layer + data[0] ^= data[4]; + data[4] ^= data[3]; + data[2] ^= data[1]; + let t = []; + for (let i = 0; i <= 4; i++) { + t[i] = (data[i] ^ BigInt('0xffffffffffffffff')) & data[(i + 1) % 5]; + } + for (let i = 0; i <= 4; i++) { + data[i] ^= t[(i + 1) % 5]; + } + data[1] ^= data[0]; + data[0] ^= data[4]; + data[3] ^= data[2]; + data[2] ^= BigInt('0xffffffffffffffff'); + JsAscon.debug('substitution layer', data, true); + // linear diffusion layer + data[0] ^= JsAscon.bitRotateRight(data[0], 19) ^ JsAscon.bitRotateRight(data[0], 28); + data[1] ^= JsAscon.bitRotateRight(data[1], 61) ^ JsAscon.bitRotateRight(data[1], 39); + data[2] ^= JsAscon.bitRotateRight(data[2], 1) ^ JsAscon.bitRotateRight(data[2], 6); + data[3] ^= JsAscon.bitRotateRight(data[3], 10) ^ JsAscon.bitRotateRight(data[3], 17); + data[4] ^= JsAscon.bitRotateRight(data[4], 7) ^ JsAscon.bitRotateRight(data[4], 41); + JsAscon.debug('linear diffusion layer', data, true); + } + } + /** + * Concat any amount of byte array to single byte array + * @param {ArrayLike[]} arrays + * @return {Uint8Array} + */ + static concatByteArrays(...arrays) { + let len = 0; + for (let i = 0; i < arrays.length; i++) { + len += arrays[i].length; + } + const arr = new Uint8Array(len); + let offset = 0; + for (let i = 0; i < arrays.length; i++) { + arr.set(arrays[i], offset); + offset += arrays[i].length; + } + return arr; + } + /** + * Convert a byte array to a binary string + * @param {Uint8Array} byteArray + * @return {string} + */ + static byteArrayToStr(byteArray) { + return new TextDecoder().decode(byteArray); + } + /** + * Convert a any value to a byte array + * @param {string|number[]|Uint8Array} val + * @return {Uint8Array} + */ + static anyToByteArray(val) { + if (val instanceof Uint8Array) + return val; + if (Array.isArray(val)) + return new Uint8Array(val); + return new TextEncoder().encode(val); + } + /** + * Convert given bigint into byte array + * @param {BigInt} nr + * @return {Uint8Array} + */ + static bigIntToByteArray(nr) { + let bytes = 8; + let arr = new Uint8Array(bytes); + while (nr > 0) { + arr[--bytes] = Number(nr & 255n); + nr >>= 8n; + } + return arr; + } + /** + * Convert given byte array into internal state array of 5 bigints + * @param {Uint8Array} byteArray + * @param {BigInt[]|null} fillInto If set, fill this given reference as well + * @return {BigInt[]} + */ + static byteArrayToState(byteArray, fillInto = null) { + const arr = [ + JsAscon.byteArrayToBigInt(byteArray, 0), + JsAscon.byteArrayToBigInt(byteArray, 8), + JsAscon.byteArrayToBigInt(byteArray, 16), + JsAscon.byteArrayToBigInt(byteArray, 24), + JsAscon.byteArrayToBigInt(byteArray, 32) + ]; + if (fillInto !== null) { + for (let i = 0; i < arr.length; i++) { + fillInto[i] = arr[i]; + } + } + return arr; + } + /** + * Convert given byte array to bigint + * @param {Uint8Array} byteArray + * @param {number} offset + * @return {BigInt} + */ + static byteArrayToBigInt(byteArray, offset) { + if (offset < 0) { + offset = byteArray.length + offset; + } + if (byteArray.length - 1 < offset) + return 0n; + return new DataView(byteArray.buffer).getBigUint64(offset); + } + /** + * Convert given byte array to visual hex representation with leading 0x + * @param {Uint8Array} byteArray + * @return {string} + */ + static byteArrayToHex(byteArray) { + return '0x' + Array.from(byteArray).map(x => x.toString(16).padStart(2, '0')).join(''); + } + /** + * Bit shift rotate right integer or given number of places + * @param {BigInt} nr + * @param {number} places + */ + static bitRotateRight(nr, places) { + const placesBig = BigInt(places); + const shift1 = BigInt(1); + const shiftRev = BigInt(64 - places); + return (nr >> placesBig) | ((nr & (shift1 << placesBig) - shift1) << (shiftRev)); + } + /** + * Assert that this is true + * If false, it throw and exception + * @param {string} value + * @param {string[]} values + * @param {string} errorMessage + */ + static assertInArray(value, values, errorMessage) { + JsAscon.assert(values.indexOf(value) > -1, errorMessage + ': Value \'' + value + '\' is not in available choices of\n' + JSON.stringify(values)); + } + /** + * Assert that this is true + * If false, it throw and exception + * @param {any} expected + * @param {any} actual + * @param {string} errorMessage + */ + static assertSame(expected, actual, errorMessage) { + JsAscon.assert(expected === actual, errorMessage + ': Value is expected to be\n' + JSON.stringify(expected) + '\nbut actual value is\n' + JSON.stringify(actual)); + } + /** + * Assert that this is true + * If false, it throw and exception + * @param {boolean} result + * @param {string} errorMessage + */ + static assert(result, errorMessage) { + if (!result) { + throw new Error(errorMessage); + } + } + /** + * Debug output + * @param {any} msg + * @param {BigInt[]|null} stateData + * @param {boolean} permutation Is a permutation debug + */ + static debug(msg, stateData = null, permutation = false) { + if (!permutation && !JsAscon.debugEnabled) { + return; + } + if (permutation && !JsAscon.debugPermutationEnabled) { + return; + } + if (stateData) { + let outMsg = '[Ascon Debug] ' + msg + ': ['; + for (let i = 0; i < stateData.length; i++) { + outMsg += '"0x' + stateData[i].toString(16).padStart(16, '0') + '", '; + } + console.log(outMsg.substring(0, outMsg.length - 2) + ']'); + } + else { + console.log('[Ascon Debug] ' + msg); + } + } +} +JsAscon.debugEnabled = false; +JsAscon.debugPermutationEnabled = false; +exports.default = JsAscon; +if (typeof BigInt === 'undefined') { + throw new Error('Cannot use JsAscon library, BigInt datatype is missing'); +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = JsAscon +} +if(typeof crypto === 'undefined' && typeof global !== 'undefined'){ + global.crypto = require('crypto') +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..29611e6 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "js-ascon", + "version": "1.0.0", + "description": "Persistent key/value data storage for your Browser and/or PWA, promisified, including file support and service worker support, all with IndexedDB.", + "scripts": { + "dist": "node ./node_modules/typescript/bin/tsc --project tsconfig.json && node build/dist.js", + "run-tests": "node ./node_modules/typescript/bin/tsc --project tsconfig.json && node tests/test.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/brainfoolong/js-ascon.git" + }, + "author": "BrainFooLong (Roland Eigelsreiter)", + "license": "MIT", + "bugs": { + "url": "https://github.com/brainfoolong/js-ascon/issues" + }, + "homepage": "https://github.com/brainfoolong/js-ascon", + "devDependencies": { + "typescript": "^5.3.2" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..afffbfd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,108 @@ +import type { PlaywrightTestConfig } from '@playwright/test' +import { devices } from '@playwright/test' + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + ignoreHTTPSErrors: true, + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { + ...devices['Pixel 5'], + }, + }, + { + name: 'Mobile Safari', + use: { + ...devices['iPhone 12'], + }, + }, + + /* Test against branded browsers. */ + { + name: 'Microsoft Edge', + use: { + channel: 'msedge', + }, + }, + { + name: 'Google Chrome', + use: { + channel: 'chrome', + }, + }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +} + +export default config diff --git a/src/ascon.ts b/src/ascon.ts new file mode 100644 index 0000000..fef64f6 --- /dev/null +++ b/src/ascon.ts @@ -0,0 +1,751 @@ +/** + * Javascript / Typescript implementation of Ascon v1.2 + * Heavily inspired by the python implementation of https://github.com/meichlseder/pyascon + * @link https://github.com/brainfoolong/js-ascon + * @author BrainFooLong (Roland Eigelsreiter) + * @version 1.0.0 + */ +export default class JsAscon { + public static debugEnabled: boolean = false + public static debugPermutationEnabled: boolean = false + + /** + * Encrypt any message to a hex string + * @param {string|Uint8Array} secretKey Your "password", so to say + * @param {any} messageToEncrypt Any type of message + * @param {any} associatedData Any type of associated data + * @param {string} cipherVariant See JsAscon.encrypt() + * @return {string} + */ + public static encryptToHex ( + secretKey: string | Uint8Array, + messageToEncrypt: any, + associatedData: any = null, + cipherVariant: string = 'Ascon-128' + ): string { + const key = JsAscon.hash(secretKey, 'Ascon-Xof', cipherVariant === 'Ascon-80pq' ? 20 : 16) + const nonce = crypto.getRandomValues(new Uint8Array(16)) + const ciphertext = JsAscon.encrypt( + key, + nonce, + associatedData !== null ? JSON.stringify(associatedData) : '', + JSON.stringify(messageToEncrypt), + cipherVariant + ) + return JsAscon.byteArrayToHex(ciphertext).substring(2) + JsAscon.byteArrayToHex(nonce).substring(2) + } + + /** + * Decrypt any message from a hex string previously generated with encryptToHex + * @param {string|Uint8Array} secretKey Your "password", so to say + * @param {string} hexStr Any type of message + * @param {any} associatedData Any type of associated data + * @param {string} cipherVariant See JsAscon.encrypt() + * @return {any} Null indicate unsuccessfull decrypt + */ + public static decryptFromHex ( + secretKey: string | Uint8Array, + hexStr: string, + associatedData: any = null, + cipherVariant: string = 'Ascon-128' + ): any { + const key = JsAscon.hash(secretKey, 'Ascon-Xof', cipherVariant === 'Ascon-80pq' ? 20 : 16) + const hexData = Uint8Array.from(hexStr.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))) + const plaintextMessage = JsAscon.decrypt( + key, + hexData.slice(-16), + associatedData !== null ? JSON.stringify(associatedData) : '', + hexData.slice(0, -16), + cipherVariant + ) + return plaintextMessage !== null ? JSON.parse(JsAscon.byteArrayToStr(plaintextMessage)) : null + } + + /** + * Ascon encryption + * @param {string|Uint8Array|number[]} key A string or byte array of a length 16 (for Ascon-128, Ascon-128a; 128-bit security) or + * 20 (for Ascon-80pq; 128-bit security) + * @param {string|Uint8Array|number[]} nonce A string or byte array of a length of 16 bytes (must not repeat for the same key!) + * @param {string|Uint8Array|number[]} associatedData A string or byte array of any length + * @param {string|Uint8Array|number[]} plaintext A string or byte array of any length + * @param {string} variant "Ascon-128", "Ascon-128a", or "Ascon-80pq" (specifies key size, rate and number of + * rounds) + * @return {Uint8Array} + */ + public static encrypt ( + key: string | Uint8Array, + nonce: string | Uint8Array, + associatedData: string | Uint8Array, + plaintext: string | Uint8Array, + variant: string = 'Ascon-128' + ): Uint8Array { + key = JsAscon.anyToByteArray(key) + const keyLength = key.length + nonce = JsAscon.anyToByteArray(nonce) + const nonceLength = nonce.length + JsAscon.assertInArray(variant, ['Ascon-128', 'Ascon-128a', 'Ascon-80pq'], 'Encrypt variant') + if (['Ascon-128', 'Ascon-128a'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length') + } else { + JsAscon.assert(keyLength === 20 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length') + } + const data = [] + const keySizeBits = keyLength * 8 + const permutationRoundsA = 12 + const permutationRoundsB = variant === 'Ascon-128a' ? 8 : 6 + const rate = variant === 'Ascon-128a' ? 16 : 8 + JsAscon.initialize(data, keySizeBits, rate, permutationRoundsA, permutationRoundsB, key, nonce) + associatedData = JsAscon.anyToByteArray(associatedData) + JsAscon.processAssociatedData(data, permutationRoundsB, rate, associatedData) + plaintext = JsAscon.anyToByteArray(plaintext) + const ciphertext = JsAscon.processPlaintext(data, permutationRoundsB, rate, plaintext) + const tag = JsAscon.finalize(data, permutationRoundsA, rate, key) + return JsAscon.concatByteArrays(ciphertext, tag) + } + + /** + * Ascon decryption + * @param {string|Uint8Array|number[]} key A string or byte array of a length 16 (for Ascon-128, Ascon-128a; 128-bit security) or + * 20 (for Ascon-80pq; 128-bit security) + * @param {string|Uint8Array|number[]} nonce A string or byte array of a length of 16 bytes (must not repeat for the same key!) + * @param {string|Uint8Array|number[]} associatedData A string or byte array of any length + * @param {string|Uint8Array|number[]} ciphertextAndTag A string or byte array of any length + * @param {string} variant "Ascon-128", "Ascon-128a", or "Ascon-80pq" (specifies key size, rate and number of + * rounds) + * @return {Uint8Array|null} Returns plaintext as byte array or NULL when cannot decrypt + */ + public static decrypt ( + key: string | Uint8Array, + nonce: string | Uint8Array, + associatedData: string | Uint8Array, + ciphertextAndTag: string | Uint8Array, + variant: string = 'Ascon-128' + ): Uint8Array | null { + key = JsAscon.anyToByteArray(key) + const keyLength = key.length + nonce = JsAscon.anyToByteArray(nonce) + const nonceLength = nonce.length + JsAscon.assertInArray(variant, ['Ascon-128', 'Ascon-128a', 'Ascon-80pq'], 'Encrypt variant') + if (['Ascon-128', 'Ascon-128a'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length') + } else { + JsAscon.assert(keyLength === 20 && nonceLength === 16, 'Incorrect key (' + keyLength + ') or nonce(' + nonceLength + ') length') + } + const data = [] + const keySizeBits = keyLength * 8 + const permutationRoundsA = 12 + const permutationRoundsB = variant === 'Ascon-128a' ? 8 : 6 + const rate = variant === 'Ascon-128a' ? 16 : 8 + JsAscon.initialize(data, keySizeBits, rate, permutationRoundsA, permutationRoundsB, key, nonce) + associatedData = JsAscon.anyToByteArray(associatedData) + JsAscon.processAssociatedData(data, permutationRoundsB, rate, associatedData) + ciphertextAndTag = JsAscon.anyToByteArray(ciphertextAndTag) + const ciphertext = ciphertextAndTag.slice(0, -16) + const ciphertextTag = ciphertextAndTag.slice(-16) + const plaintext = JsAscon.processCiphertext(data, permutationRoundsB, rate, ciphertext) + const tag = JsAscon.finalize(data, permutationRoundsA, rate, key) + if (JsAscon.byteArrayToHex(tag) === JsAscon.byteArrayToHex(ciphertextTag)) { + return plaintext + } + return null + } + + /** + * Ascon hash function and extendable-output function + * @param {string|Uint8Array} message A string or byte array + * @param {string} variant "Ascon-Hash", "Ascon-Hasha" (both with 256-bit output for 128-bit security), "Ascon-Xof", + * or "Ascon-Xofa" (both with arbitrary output length, security=min(128, bitlen/2)) + * @param {number} hashLength The requested output bytelength (must be 32 for variant "Ascon-Hash"; can be arbitrary + * for Ascon-Xof, but should be >= 32 for 128-bit security) + * @return {Uint8Array} The byte array representing the hash tag + */ + public static hash ( + message: string | number[] | Uint8Array, + variant: string = 'Ascon-Hash', + hashLength: number = 32 + ): Uint8Array { + JsAscon.assertInArray(variant, ['Ascon-Hash', 'Ascon-Hasha', 'Ascon-Xof', 'Ascon-Xofa'], 'Hash variant') + if (['Ascon-Hash', 'Ascon-Hasha'].indexOf(variant) > -1) { + JsAscon.assert(hashLength === 32, 'Incorrect hash length') + } + message = JsAscon.anyToByteArray(message) + const messageLength = message.length + const permutationRoundsA = 12 + const permutationRoundsB = ['Ascon-Hasha', 'Ascon-Xofa'].indexOf(variant) > -1 ? 8 : 12 + const rate = 8 + const data = JsAscon.byteArrayToState(JsAscon.concatByteArrays( + [0, rate * 8, permutationRoundsA, permutationRoundsA - permutationRoundsB], + [0, 0, ['Ascon-Hash', 'Ascon-Hasha'].indexOf(variant) > -1 ? 1 : 0, 0], // tagspec, + new Uint8Array(32) + )) + JsAscon.debug('initial value', data, true) + JsAscon.permutation(data, permutationRoundsA) + JsAscon.debug('initialization', data, true) + // message processing (absorbing) + const messagePadded = JsAscon.concatByteArrays( + message, + [0x80], + new Uint8Array(rate - (messageLength % rate) - 1) + ) + const messagePaddedLength = messagePadded.length + // first s-1 blocks + for (let block = 0; block < messagePaddedLength - rate; block += rate) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block) + JsAscon.permutation(data, permutationRoundsB) + } + // last block + const block = messagePaddedLength - rate + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block) + JsAscon.debug('process message', data) + // finalization (squeezing) + let hash = [] + JsAscon.permutation(data, permutationRoundsA) + while (hash.length < hashLength) { + hash = hash.concat(...JsAscon.bigIntToByteArray(data[0])) + JsAscon.permutation(data, permutationRoundsB) + } + JsAscon.debug('finalization', data) + return new Uint8Array(hash) + } + + /** + * Ascon message authentication code (MAC) and pseudorandom function (PRF) + * @param {string|number[]|Uint8Array} key A string or byte array of a length of 16 bytes + * @param {string|number[]|Uint8Array} message A string or byte array (<= 16 for "Ascon-PrfShort") + * @param {string} variant "Ascon-Mac", "Ascon-Maca" (both 128-bit output, arbitrarily long input), "Ascon-Prf", + * "Ascon-Prfa" (both arbitrarily long input and output), or "Ascon-PrfShort" (t-bit output for t<=128, m-bit + * input for m<=128) + * @param {number} tagLength The requested output bytelength l/8 (must be <=16 for variants "Ascon-Mac", "Ascon-Maca", + * and "Ascon-PrfShort", arbitrary for "Ascon-Prf", "Ascon-Prfa"; should be >= 16 for 128-bit security) + * @return {Uint8Array} The byte array representing the authentication tag + */ + public static mac ( + key: string | number[] | Uint8Array, + message: string | number[] | Uint8Array, + variant: string = 'Ascon-Mac', + tagLength: number = 16 + ): Uint8Array { + JsAscon.assertInArray( + variant, + ['Ascon-Mac', 'Ascon-Prf', 'Ascon-Maca', 'Ascon-Prfa', 'Ascon-PrfShort'], + 'Mac variant' + ) + key = JsAscon.anyToByteArray(key) + const keyLength = key.length + message = JsAscon.anyToByteArray(message) + const messageLength = message.length + if (['Ascon-Mac', 'Ascon-Maca'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16 && tagLength <= 16, 'Incorrect key length') + } else if (['Ascon-Prf', 'Ascon-Prfa'].indexOf(variant) > -1) { + JsAscon.assert(keyLength === 16, 'Incorrect key length') + } else if (variant === 'Ascon-PrfShort') { + JsAscon.assert(messageLength <= 16, 'Message to long for variant ' + variant) + JsAscon.assert(keyLength === 16 && tagLength <= 16 && messageLength <= 16, 'Incorrect key length') + } + const permutationRoundsA = 12 + const permutationRoundsB = ['Ascon-Prfa', 'Ascon-Maca'].indexOf(variant) > -1 ? 8 : 12 + const messageBlockSize = ['Ascon-Prfa', 'Ascon-Maca'].indexOf(variant) > -1 ? 40 : 32 + const rate = 16 + if (variant === 'Ascon-PrfShort') { + const data = JsAscon.byteArrayToState(JsAscon.concatByteArrays( + [keyLength * 8, messageLength * 8, permutationRoundsA + 64, tagLength * 8, 0, 0, 0, 0], + key, + message, + new Uint8Array(16 - messageLength) + )) + JsAscon.debug('initial value', data) + JsAscon.permutation(data, permutationRoundsA) + JsAscon.debug('process message', data) + data[3] ^= JsAscon.byteArrayToBigInt(key, 0) + data[4] ^= JsAscon.byteArrayToBigInt(key, 8) + return new Uint8Array([...JsAscon.bigIntToByteArray(data[3]), ...JsAscon.bigIntToByteArray(data[4])]) + } + const data = JsAscon.byteArrayToState(JsAscon.concatByteArrays( + [keyLength * 8, rate * 8, permutationRoundsA + 128, permutationRoundsA - permutationRoundsB], + [0, 0, 0, ['Ascon-Mac', 'Ascon-Maca'].indexOf(variant) > -1 ? 128 : 0], // tagspec + key, + new Uint8Array(16) + )) + JsAscon.debug('initial value', data) + JsAscon.permutation(data, permutationRoundsA) + JsAscon.debug('initialization', data) + // message processing (absorbing) + const messagePadded = JsAscon.concatByteArrays( + message, + [0x80], + new Uint8Array(messageBlockSize - (messageLength % messageBlockSize) - 1) + ) + const messagePaddedLength = messagePadded.length + const iterations = ['Ascon-Prfa', 'Ascon-Maca'].indexOf(variant) > -1 ? 4 : 3 + // first s-1 blocks + for (let block = 0; block < messagePaddedLength - messageBlockSize; block += messageBlockSize) { + for (let i = 0; i <= iterations; i++) { + data[i] ^= JsAscon.byteArrayToBigInt(messagePadded, block + (i * 8)) + } + JsAscon.permutation(data, permutationRoundsB) + } + // last block + const block = messagePaddedLength - messageBlockSize + for (let i = 0; i <= iterations; i++) { + data[i] ^= JsAscon.byteArrayToBigInt(messagePadded, block + (i * 8)) + } + data[4] ^= 1n + JsAscon.debug('process message', data) + // finalization (squeezing) + let tag = [] + JsAscon.permutation(data, permutationRoundsA) + while (tag.length < tagLength) { + tag = tag.concat(...JsAscon.bigIntToByteArray(data[0]), ...JsAscon.bigIntToByteArray(data[1])) + JsAscon.permutation(data, permutationRoundsB) + } + JsAscon.debug('finalization', data) + return new Uint8Array(tag) + } + + /** + * Ascon initialization phase - internal helper function + * @param {BigInt[]} data Ascon state, a list of 5 64-bit integers + * @param {number} keySize Key size in bits + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {number} permutationRoundsA Number of initialization/finalization rounds for permutation + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {Uint8Array} key A bytes object of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; + * 128-bit security) + * @param {Uint8Array} nonce A bytes object of size 16 + */ + public static initialize ( + data: bigint[], + keySize: number, + rate: number, + permutationRoundsA: number, + permutationRoundsB: number, + key: Uint8Array, + nonce: Uint8Array + ): void { + JsAscon.byteArrayToState(JsAscon.concatByteArrays( + [keySize, rate * 8, permutationRoundsA, permutationRoundsB], + new Uint8Array(20 - key.length), + key, + nonce + ), data) + JsAscon.debug('initial value', data) + JsAscon.permutation(data, permutationRoundsA) + const zeroKey = JsAscon.byteArrayToState(JsAscon.concatByteArrays( + new Uint8Array(40 - key.length), + key + )) + for (let i = 0; i <= 4; i++) { + data[i] ^= zeroKey[i] + } + JsAscon.debug('initialization', data) + } + + /** + * Ascon associated data processing phase - internal helper function + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} associatedData A byte array of any length + */ + public static processAssociatedData ( + data: bigint[], + permutationRoundsB: number, + rate: number, + associatedData: Uint8Array + ): void { + if (associatedData.length) { + // message processing (absorbing) + const messagePadded = JsAscon.concatByteArrays( + associatedData, + [0x80], + new Uint8Array(rate - (associatedData.length % rate) - 1) + ) + const messagePaddedLength = messagePadded.length + for (let block = 0; block < messagePaddedLength; block += rate) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block) + if (rate === 16) { + data[1] ^= JsAscon.byteArrayToBigInt(messagePadded, block + 8) + } + JsAscon.permutation(data, permutationRoundsB) + } + } + data[4] ^= 1n + JsAscon.debug('process associated data', data) + } + + /** + * Ascon plaintext processing phase (during encryption) - internal helper function + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} plaintext A byte array of any length + * @return {Uint8Array} Returns the ciphertext as byte array + */ + public static processPlaintext ( + data: bigint[], + permutationRoundsB: number, + rate: number, + plaintext: Uint8Array + ): Uint8Array { + const lastLen = plaintext.length % rate + const messagePadded = JsAscon.concatByteArrays( + plaintext, + [0x80], + new Uint8Array(rate - lastLen - 1) + ) + const messagePaddedLength = messagePadded.length + let ciphertext = new Uint8Array(0) + // first t-1 blocks + for (let block = 0; block < messagePaddedLength - rate; block += rate) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block) + ciphertext = JsAscon.concatByteArrays(ciphertext, JsAscon.bigIntToByteArray(data[0])) + if (rate === 16) { + data[1] ^= JsAscon.byteArrayToBigInt(messagePadded, block + 8) + ciphertext = JsAscon.concatByteArrays(ciphertext, JsAscon.bigIntToByteArray(data[1])) + + } + JsAscon.permutation(data, permutationRoundsB) + } + // last block + const block = messagePaddedLength - rate + if (rate === 8) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block) + ciphertext = JsAscon.concatByteArrays(ciphertext, JsAscon.bigIntToByteArray(data[0]).slice(0, lastLen)) + + } else if (rate === 16) { + data[0] ^= JsAscon.byteArrayToBigInt(messagePadded, block) + data[1] ^= JsAscon.byteArrayToBigInt(messagePadded, block + 8) + ciphertext = JsAscon.concatByteArrays( + ciphertext, + JsAscon.bigIntToByteArray(data[0]).slice(0, lastLen > 8 ? 8 : lastLen), + JsAscon.bigIntToByteArray(data[1]).slice(0, lastLen - 8 < 0 ? 0 : lastLen - 8), + ) + } + JsAscon.debug('process plaintext', data) + return ciphertext + } + + /** + * Ascon plaintext processing phase (during encryption) - internal helper function + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsB Number of intermediate rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} ciphertext A byte array of any length + * @return {Uint8Array} Returns the ciphertext as byte array + */ + public static processCiphertext ( + data: bigint[], + permutationRoundsB: number, + rate: number, + ciphertext: Uint8Array + ): Uint8Array { + const lastLen = ciphertext.length % rate + const messagePadded = JsAscon.concatByteArrays( + ciphertext, + new Uint8Array(rate - lastLen) + ) + const messagePaddedLength = messagePadded.length + let plaintext = new Uint8Array(0) + // first t-1 blocks + for (let block = 0; block < messagePaddedLength - rate; block += rate) { + let ci = JsAscon.byteArrayToBigInt(messagePadded, block) + plaintext = JsAscon.concatByteArrays(plaintext, JsAscon.bigIntToByteArray(data[0] ^ ci)) + data[0] = ci + if (rate === 16) { + ci = JsAscon.byteArrayToBigInt(messagePadded, block + 8) + plaintext = JsAscon.concatByteArrays(plaintext, JsAscon.bigIntToByteArray(data[1] ^ ci)) + data[1] = ci + } + JsAscon.permutation(data, permutationRoundsB) + } + // last block + const block = messagePaddedLength - rate + if (rate === 8) { + let ci = JsAscon.byteArrayToBigInt(messagePadded, block) + plaintext = JsAscon.concatByteArrays(plaintext, JsAscon.bigIntToByteArray(ci ^ data[0]).slice(0, lastLen)) + const padding = 0x80n << BigInt((rate - lastLen - 1) * 8) + const mask = BigInt('0xFFFFFFFFFFFFFFFF') >> BigInt(lastLen * 8) + data[0] = ci ^ (data[0] & mask) ^ padding + } else if (rate === 16) { + const lastLenWord = lastLen % 8 + const padding = 0x80n << BigInt((8 - lastLenWord - 1) * 8) + const mask = BigInt('0xFFFFFFFFFFFFFFFF') >> BigInt(lastLenWord * 8) + let ciA = JsAscon.byteArrayToBigInt(messagePadded, block) + let ciB = JsAscon.byteArrayToBigInt(messagePadded, block + 8) + plaintext = JsAscon.concatByteArrays( + plaintext, + JsAscon.concatByteArrays( + JsAscon.bigIntToByteArray(data[0] ^ ciA), + JsAscon.bigIntToByteArray(data[1] ^ ciB) + ).slice(0, lastLen) + ) + if (lastLen < 8) { + data[0] = ciA ^ (data[0] & mask) ^ padding + } else { + data[0] = ciA + data[1] = ciB ^ (data[1] & mask) ^ padding + } + } + JsAscon.debug('process ciphertext', data) + return plaintext + } + + /** + * Ascon finalization phase - internal helper function + * + * @param {BigInt[]} data data Ascon state, a list of 5 64-bit integers + * @param {number} permutationRoundsA Number of initialization/finalization rounds for permutation + * @param {number} rate Block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a) + * @param {Uint8Array} key A bytes array of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; + * 128-bit security) + * @return {Uint8Array} The tag as a byte array + */ + public static finalize ( + data: bigint[], + permutationRoundsA: number, + rate: number, + key: Uint8Array + ): Uint8Array { + let zeroFilledKey = key + if (key.length > 16) { + const newLen = key.length + 24 - key.length + zeroFilledKey = new Uint8Array(newLen) + zeroFilledKey.set(key) + } + let index = (rate / 8) | 0 + data[index++] ^= JsAscon.byteArrayToBigInt(key, 0) + data[index++] ^= JsAscon.byteArrayToBigInt(key, 8) + data[index++] ^= JsAscon.byteArrayToBigInt(zeroFilledKey, 16) + JsAscon.permutation(data, permutationRoundsA) + data[3] ^= JsAscon.byteArrayToBigInt(key, -16) + data[4] ^= JsAscon.byteArrayToBigInt(key, -8) + JsAscon.debug('finalization', data) + return JsAscon.concatByteArrays( + JsAscon.bigIntToByteArray(data[3]), + JsAscon.bigIntToByteArray(data[4]) + ) + } + + /** + * Ascon core permutation for the sponge construction - internal helper function + * @param {BigInt[]} data Ascon state, a list of 5 64-bit integers + * @param {number} rounds + */ + public static permutation (data: bigint[], rounds: number = 1): void { + JsAscon.assert(rounds <= 12, 'Permutation rounds must be <= 12') + JsAscon.debug('permutation input', data, true) + for (let round = 12 - rounds; round < 12; round++) { + // add round constants + data[2] ^= BigInt(0xf0 - round * 0x10 + round) + JsAscon.debug('round constant addition', data, true) + // substitution layer + data[0] ^= data[4] + data[4] ^= data[3] + data[2] ^= data[1] + let t = [] + for (let i = 0; i <= 4; i++) { + t[i] = (data[i] ^ BigInt('0xffffffffffffffff')) & data[(i + 1) % 5] + } + for (let i = 0; i <= 4; i++) { + data[i] ^= t[(i + 1) % 5] + } + data[1] ^= data[0] + data[0] ^= data[4] + data[3] ^= data[2] + data[2] ^= BigInt('0xffffffffffffffff') + JsAscon.debug('substitution layer', data, true) + // linear diffusion layer + data[0] ^= JsAscon.bitRotateRight(data[0], 19) ^ JsAscon.bitRotateRight(data[0], 28) + data[1] ^= JsAscon.bitRotateRight(data[1], 61) ^ JsAscon.bitRotateRight(data[1], 39) + data[2] ^= JsAscon.bitRotateRight(data[2], 1) ^ JsAscon.bitRotateRight(data[2], 6) + data[3] ^= JsAscon.bitRotateRight(data[3], 10) ^ JsAscon.bitRotateRight(data[3], 17) + data[4] ^= JsAscon.bitRotateRight(data[4], 7) ^ JsAscon.bitRotateRight(data[4], 41) + + JsAscon.debug('linear diffusion layer', data, true) + } + } + + /** + * Concat any amount of byte array to single byte array + * @param {ArrayLike[]} arrays + * @return {Uint8Array} + */ + public static concatByteArrays (...arrays: ArrayLike[]): Uint8Array { + let len = 0 + for (let i = 0; i < arrays.length; i++) { + len += arrays[i].length + } + const arr = new Uint8Array(len) + let offset = 0 + for (let i = 0; i < arrays.length; i++) { + arr.set(arrays[i], offset) + offset += arrays[i].length + } + return arr + } + + /** + * Convert a byte array to a binary string + * @param {Uint8Array} byteArray + * @return {string} + */ + public static byteArrayToStr (byteArray: Uint8Array): string { + return new TextDecoder().decode(byteArray) + } + + /** + * Convert a any value to a byte array + * @param {string|number[]|Uint8Array} val + * @return {Uint8Array} + */ + public static anyToByteArray (val: any): Uint8Array { + if (val instanceof Uint8Array) return val + if (Array.isArray(val)) return new Uint8Array(val) + return new TextEncoder().encode(val) + } + + /** + * Convert given bigint into byte array + * @param {BigInt} nr + * @return {Uint8Array} + */ + public static bigIntToByteArray (nr: bigint): Uint8Array { + let bytes = 8 + let arr = new Uint8Array(bytes) + while (nr > 0) { + arr[--bytes] = Number(nr & 255n) + nr >>= 8n + } + return arr + } + + /** + * Convert given byte array into internal state array of 5 bigints + * @param {Uint8Array} byteArray + * @param {BigInt[]|null} fillInto If set, fill this given reference as well + * @return {BigInt[]} + */ + public static byteArrayToState (byteArray: Uint8Array, fillInto: bigint[] | null = null): bigint[] { + const arr = [ + JsAscon.byteArrayToBigInt(byteArray, 0), + JsAscon.byteArrayToBigInt(byteArray, 8), + JsAscon.byteArrayToBigInt(byteArray, 16), + JsAscon.byteArrayToBigInt(byteArray, 24), + JsAscon.byteArrayToBigInt(byteArray, 32) + ] + if (fillInto !== null) { + for (let i = 0; i < arr.length; i++) { + fillInto[i] = arr[i] + } + } + return arr + } + + /** + * Convert given byte array to bigint + * @param {Uint8Array} byteArray + * @param {number} offset + * @return {BigInt} + */ + public static byteArrayToBigInt (byteArray: Uint8Array, offset: number): bigint { + if (offset < 0) { + offset = byteArray.length + offset + } + if (byteArray.length - 1 < offset) return 0n + return new DataView(byteArray.buffer).getBigUint64(offset) + } + + /** + * Convert given byte array to visual hex representation with leading 0x + * @param {Uint8Array} byteArray + * @return {string} + */ + public static byteArrayToHex (byteArray: Uint8Array): string { + return '0x' + Array.from(byteArray).map(x => x.toString(16).padStart(2, '0')).join('') + } + + /** + * Bit shift rotate right integer or given number of places + * @param {BigInt} nr + * @param {number} places + */ + public static bitRotateRight (nr: bigint, places: number): bigint { + const placesBig = BigInt(places) + const shift1 = BigInt(1) + const shiftRev = BigInt(64 - places) + return (nr >> placesBig) | ((nr & (shift1 << placesBig) - shift1) << (shiftRev)) + } + + /** + * Assert that this is true + * If false, it throw and exception + * @param {string} value + * @param {string[]} values + * @param {string} errorMessage + */ + public static assertInArray (value: string, values: string[], errorMessage: string): void { + JsAscon.assert( + values.indexOf(value) > -1, + errorMessage + ': Value \'' + value + '\' is not in available choices of\n' + JSON.stringify(values) + ) + } + + /** + * Assert that this is true + * If false, it throw and exception + * @param {any} expected + * @param {any} actual + * @param {string} errorMessage + */ + public static assertSame (expected: any, actual: any, errorMessage: string): void { + JsAscon.assert( + expected === actual, + errorMessage + ': Value is expected to be\n' + JSON.stringify(expected) + '\nbut actual value is\n' + JSON.stringify(actual) + ) + + } + + /** + * Assert that this is true + * If false, it throw and exception + * @param {boolean} result + * @param {string} errorMessage + */ + public static assert (result: boolean, errorMessage: string): void { + if (!result) { + throw new Error(errorMessage) + } + } + + /** + * Debug output + * @param {any} msg + * @param {BigInt[]|null} stateData + * @param {boolean} permutation Is a permutation debug + */ + public static debug ( + msg: any, + stateData: Array | null = null, + permutation: boolean = false + ): void { + if (!permutation && !JsAscon.debugEnabled) { + return + } + if (permutation && !JsAscon.debugPermutationEnabled) { + return + } + if (stateData) { + let outMsg = '[Ascon Debug] ' + msg + ': [' + for (let i = 0; i < stateData.length; i++) { + outMsg += '"0x' + stateData[i].toString(16).padStart(16, '0') + '", ' + } + console.log(outMsg.substring(0, outMsg.length - 2) + ']') + } else { + console.log('[Ascon Debug] ' + msg) + } + } +} + +if (typeof BigInt === 'undefined') { + throw new Error('Cannot use JsAscon library, BigInt datatype is missing') +} \ No newline at end of file diff --git a/tests/performance.js b/tests/performance.js new file mode 100644 index 0000000..0ea45f2 --- /dev/null +++ b/tests/performance.js @@ -0,0 +1,48 @@ +const JsAscon = require('../dist/ascon') + +JsAscon.debugEnabled = false +JsAscon.debugPermutationEnabled = false + +let cycles = [ + { + 'nr': 10, + 'messageSize': 32, + 'assocSize': 128, + }, + { + 'nr': 10, + 'messageSize': 128, + 'assocSize': 512, + }, + { + 'nr': 10, + 'messageSize': 128 * 8, + 'assocSize': 512 * 4, + }, + { + 'nr': 10, + 'messageSize': 512 * 8, + 'assocSize': 0, + } +] + +for (let i in cycles) { + const cycle = cycles[i] + let totalTime = 0 + let runs = cycle['nr'] + let message, associatedData + for (i = 1; i <= runs; i++) { + const key = crypto.getRandomValues(new Uint8Array(16)) + message = crypto.getRandomValues(new Uint8Array(cycle['messageSize'])) + associatedData = cycle['assocSize'] ? crypto.getRandomValues(new Uint8Array(cycle['assocSize'])) : null + + const start = performance.now() + const encrypted = JsAscon.encryptToHex(key, message, associatedData) + const decrypted = JsAscon.decryptFromHex(key, encrypted, associatedData) + totalTime += performance.now() - start + JsAscon.assertSame(JSON.stringify(message), JSON.stringify(decrypted), 'Encryption/Decryption to hex failed') + } + + console.log('### ' + runs + ' cycles with ' + (message ? message.length : 0) + ' byte message data and ' + (associatedData ? associatedData.length : 0) + ' byte associated data ###') + console.log('Total Time: ' + (totalTime / 1000).toFixed(3) + ' seconds') +} diff --git a/tests/test-all.js b/tests/test-all.js new file mode 100644 index 0000000..03f4768 --- /dev/null +++ b/tests/test-all.js @@ -0,0 +1,3 @@ +require('./test-hash.js') +require('./test-mac.js') +require('./test-encrypt-decrypt.js') \ No newline at end of file diff --git a/tests/test-encrypt-decrypt.js b/tests/test-encrypt-decrypt.js new file mode 100644 index 0000000..8dbbd06 --- /dev/null +++ b/tests/test-encrypt-decrypt.js @@ -0,0 +1,117 @@ +const JsAscon = require('../dist/ascon') + +JsAscon.debugEnabled = false +JsAscon.debugPermutationEnabled = false + +let expected, actual + +const key20 = [ + 0x90, + 0x80, + 0x70, + 0x60, + 0x50, + 0x40, + 0x30, + 0x20, + 0x10, + 0xAA, + 0x90, + 0x90, + 0x90, + 0x90, + 0xCC, + 0xEF, + 0xAA, + 0x90, + 0x90, + 0x90, +] +const key16 = [0x90, 0x80, 0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, 0xAA, 0x90, 0x90, 0x90, 0x90, 0xCC, 0xEF] +const nonce = [0x50, 0x10, 0x30, 0x70, 0x90, 0x60, 0x40, 0x30, 0xEF, 0x20, 0x10, 0xAA, 0x90, 0x90, 0x90, 0xCC] +const plaintextSimple = 'ascon' +const plaintextMore = 'ascon-asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+' +const accociatedMore = 'BLAB-asconASDFNASKIQAL-_;:;#+asconKIQAL-_;:;#+asconASDFNASKIQAL+' +const variants = [ + { + 'variant': 'Ascon-128', + 'key': key16, + 'plaintext': plaintextSimple, + 'associatedData': 'ASCON', + 'expectedCiphertextHex': '0x265e8b5755', + 'expectedTagHex': '0x0d95cfeeb4cfe5a4f9ff29380137f12d', + }, + { + 'variant': 'Ascon-128a', + 'key': key16, + 'plaintext': plaintextSimple, + 'associatedData': 'ASCON', + 'expectedCiphertextHex': '0x24443ec02c', + 'expectedTagHex': '0xd411bf0897d558a95d09cccccc06d273', + }, + { + 'variant': 'Ascon-80pq', + 'key': key20, + 'plaintext': plaintextSimple, + 'associatedData': 'ASCON', + 'expectedCiphertextHex': '0x112e6c44be', + 'expectedTagHex': '0x83bea05f00a5f8b9f08efd404144b87b', + }, + { + 'variant': 'Ascon-128', + 'key': key16, + 'plaintext': plaintextMore, + 'associatedData': accociatedMore, + 'expectedCiphertextHex': '0x2287b412d5c2658c38fb1616e2a3c6ff85952bbaefe021757e535ccfd4a0806cf9c5d61a368739fe661ac16d4c943a84c16196b343fdc8aaf76cc2e5ad067843dc28bae8fcf7972bfa36aaf6e734ba4ac89b3c559bdb5ba49bfb8df56d6beafd0104d9d4d495', + 'expectedTagHex': '0x7c1e88242bea67a90f369fb0889b74c9', + }, + { + 'variant': 'Ascon-128a', + 'key': key16, + 'plaintext': plaintextMore, + 'associatedData': accociatedMore, + 'expectedCiphertextHex': '0x72e5fde15539b1dbf9f7aea29e58598267971ae9b0446db26a0fdd7f5821cb492ca4c5ec9c40d5fd6536cc4a1d4b4cb616423d4c6d33c8a06364e7137726447d1bdee5d2071cacea601c1ab199b57748e35766248cbb26f0287abb70280b8510de508e22cc6f', + 'expectedTagHex': '0x45eafc378d5f1d2d3b4af25ba3ef70ac', + }, + { + 'variant': 'Ascon-80pq', + 'key': key20, + 'plaintext': plaintextMore, + 'associatedData': accociatedMore, + 'expectedCiphertextHex': '0xe483db31c108a269c2cacd33534544c0ba524a6f46016473260b9d4aa81dd0a61f994f46bb3966f2aea436990f024fbaf1477c0cbd6664b53ecdd4acf91f683054762c952dbfa42235763a8cb97ee94a25d8f0d53e200ba6a291715e3713c02ee63196aa5917', + 'expectedTagHex': '0x6d06c25335b03f3eef29537c3a133afd', + }, +] +for (let i in variants) { + const row = variants[i] + const variant = row['variant'] + const plaintext = row['plaintext'] + const plaintextHex = JsAscon.byteArrayToHex(JsAscon.anyToByteArray(plaintext)) + const associatedData = row['associatedData'] + + const ciphertextAndTag = JsAscon.encrypt(row['key'], nonce, associatedData, plaintext, variant) + const ciphertext = ciphertextAndTag.slice(0, -16) + const tag = ciphertextAndTag.slice(-16) + // check ciphertext + expected = row['expectedCiphertextHex'] + actual = JsAscon.byteArrayToHex(ciphertext) + JsAscon.assertSame(expected, actual, + 'Encrypted ciphertext of word "' + plaintext + '" in variant "' + variant + '"') + // check encrypted tag + expected = row['expectedTagHex'] + actual = JsAscon.byteArrayToHex(tag) + JsAscon.assertSame(expected, actual, 'Encrypted tag of word "' + plaintext + '" in variant "' + variant + '"') + // check decryption + const plaintextReceived = JsAscon.decrypt(row['key'], nonce, associatedData, ciphertextAndTag, variant) + actual = JsAscon.byteArrayToHex(plaintextReceived ?? []) + JsAscon.assertSame(plaintextHex, JsAscon.byteArrayToHex(plaintextReceived ?? []), + 'Decryption from ciphertext failed in variant "' + variant + '"') +} + +// test convenient methods +let key = 'mypassword' +let message = ['this can be any data type 😎 文', 123] +let associatedData = 'Some data 😋 文 This data is not contained in the encrypt output. You must pass the same data to encrypt and decrypt in order to be able to decrypt the message.' +let encrypted = JsAscon.encryptToHex(key, message, associatedData) +let decrypted = JsAscon.decryptFromHex(key, encrypted, associatedData) +JsAscon.assertSame(JSON.stringify(message), JSON.stringify(decrypted), 'Encryption/Decryption to hex failed') \ No newline at end of file diff --git a/tests/test-hash.js b/tests/test-hash.js new file mode 100644 index 0000000..e6b7d1c --- /dev/null +++ b/tests/test-hash.js @@ -0,0 +1,28 @@ +const JsAscon = require('../dist/ascon') + +JsAscon.debugEnabled = false +JsAscon.debugPermutationEnabled = false + +let word, expected, actual + +word = 'ascon' +expected = '0x02c895cb92d79f195ed9e3e2af89ae307059104aaa819b9a987a76cf7cf51e6e' +actual = JsAscon.byteArrayToHex(JsAscon.hash(word)) +JsAscon.assertSame(expected, actual, 'Hash of word "' + word + '" in variant "Ascon-Hash"') + +word = 'asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+' +expected = '0x9223f5c59a29c05a60121936c90968ecb3103c3c69a876f4d5de87cd4d3fec84' +actual = JsAscon.byteArrayToHex(JsAscon.hash(word)) +JsAscon.assertSame(expected, actual, 'Hash of word "' + word + '" in variant "Ascon-Hash"') + +expected = '0x54a4c99e9a43141b4ade74044c74e6fa9bc7ddcb2334c0fc2308c8c834c7feec' +actual = JsAscon.byteArrayToHex(JsAscon.hash(word, 'Ascon-Xof')) +JsAscon.assertSame(expected, actual, 'Hash of word "' + word + '" in variant "Ascon-Xof"') + +expected = '0x1349fdcb638579236bfc8f56ac260b6359706276d7bed25b9dd751645a523b2f' +actual = JsAscon.byteArrayToHex(JsAscon.hash(word, 'Ascon-Xofa')) +JsAscon.assertSame(expected, actual, 'Hash of word "' + word + '" in variant "Ascon-Xofa"') + +expected = '0xb03447d661a92286a403507c0bb647c6c10dad98a4366b60a0631cd5cb7ed930' +actual = JsAscon.byteArrayToHex(JsAscon.hash(word, 'Ascon-Hasha')) +JsAscon.assertSame(expected, actual, 'Hash of word "' + word + '" in variant "Ascon-Hasha"') \ No newline at end of file diff --git a/tests/test-mac.js b/tests/test-mac.js new file mode 100644 index 0000000..320184d --- /dev/null +++ b/tests/test-mac.js @@ -0,0 +1,37 @@ +const JsAscon = require('../dist/ascon') + +JsAscon.debugEnabled = false +JsAscon.debugPermutationEnabled = false + +let key = [0x90, 0x80, 0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, 0xAA, 0x90, 0x90, 0x90, 0x90, 0xCC, 0xEF] +let word, expected, actual + +word = 'ascon' +expected = '0x5432a3ff217b31c6a7105a175438b1f9' +actual = JsAscon.byteArrayToHex(JsAscon.mac(key, word)) +JsAscon.assertSame(expected, actual, 'Mac of word "' + word + '" in variant "Ascon-Mac"') + +word = 'asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+' +expected = '0x558cb5e4ae72cc04da650971dc7b2f43' +actual = JsAscon.byteArrayToHex(JsAscon.mac(key, word)) +JsAscon.assertSame(expected, actual, 'Mac of word "' + word + '" in variant "Ascon-Mac"') + +word = 'asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+' +expected = '0x68dfa25dbca5a16559f963b34351b95b' +actual = JsAscon.byteArrayToHex(JsAscon.mac(key, word, 'Ascon-Maca')) +JsAscon.assertSame(expected, actual, 'Mac of word "' + word + '" in variant "Ascon-Maca"') + +word = 'asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+' +expected = '0xc674ed5d593aeed416664d592c917050' +actual = JsAscon.byteArrayToHex(JsAscon.mac(key, word, 'Ascon-Prf')) +JsAscon.assertSame(expected, actual, 'Mac of word "' + word + '" in variant "Ascon-Prf"') + +word = 'asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+asconASDFNASKIQAL-_;:;#+' +expected = '0x4d7087c67b452a80b373df49f2c134c5' +actual = JsAscon.byteArrayToHex(JsAscon.mac(key, word, 'Ascon-Prfa')) +JsAscon.assertSame(expected, actual, 'Mac of word "' + word + '" in variant "Ascon-Prfa"') + +word = 'ascon' +expected = '0xbc38d3219d01a84e0afecd930c40ac9d' +actual = JsAscon.byteArrayToHex(JsAscon.mac(key, word, 'Ascon-PrfShort')) +JsAscon.assertSame(expected, actual, 'Mac of word "' + word + '" in variant "Ascon-PrfShort"') \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..53d4a88 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "sourceMap": false, + "noImplicitAny": false, + "target": "ES2020", + "moduleResolution" : "node", + "module": "CommonJS", + "lib": [ + "ES2020", + "DOM" + ] + }, + "include": [ + "src/ascon.ts" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +}