/*
  Copyright (c) 2021 Corinne Diakhoumpa et Erwan Iev-Le Tac

  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.
*/

const assert = require('assert').strict;

// This program encrypts the message of "Cryptique" using the RSA algorithm
// with years (2021, 229) as the public key and generates the corresponding
// ASCII-art calendar.
//
// WARNING: This program is only used for generating the calendar of "Cryptique"
// and for illustration purpose. It has many security vulnerabilities (choice of
// parameters, use of BigInt, no padding, etc) which is actually one idea that
// "Cryptique" is supposed to highlight. DO NOT USE FOR REAL CYPHERING.

const message = "NUL_BESOIN_D'ORDINATEUR_QUANTIQUE_POUR_LIRE_CE_CALENDRIER...";
const n = 2021n; // = 43 * 47
const e = 229n; // 1 < e < lambda(n) = 966
const d = 367n; // d * e = 1 mod 966

// Helper functions to convert between index and allowed ASCII characters.
// At most 32 characters are allowed, so each can be encoded on 5 bits.
const asciiA = 65n;
const letterCount = 26n;
const specialChar = "!',.?_";
const specialCharLength = BigInt(specialChar.length);
assert(letterCount + specialCharLength <= 2n ** 5n);

function toASCII(index) {
    assert(0n <= index && index < letterCount + specialCharLength);
    if (index < letterCount)
        return String.fromCharCode(Number(BigInt.asUintN(16, asciiA + index)));
    return specialChar[index - letterCount];
}

function fromASCII(c) {
    let letterIndex = BigInt(c.charCodeAt(0)) - asciiA;
    if (0n <= letterIndex && letterIndex < letterCount)
        return letterIndex;
    let specialCharIndex = BigInt(specialChar.indexOf(c));
    assert(specialCharIndex >= 0n);
    return letterCount + specialCharIndex;
}

// We have 2^10 < n < 2^11 so the ciphered value are 11-bits words. Below
// are helper functions to convert between these and pairs of (high, low) words
// of 6 bits and 5 bits respectively.
function concat(a, b) {
    return (b << 5n) + a;
}

function high(c) {
    return c >> 5n;
}

function low(c) {
    return c & 0x1Fn;
}

// Perform encryption.
// Each pair of 5 bits characters is encoded on 10 bits and encrypted as
// an 11 bits words. It is then represented as a 3 digits hexa number (12 bits)
// for one day, on the calendar. Republican months contain 30 days, so one can
// encode message of 60 characters.

const weeks_per_month = 3;
const days_per_week = 10;
let calendar = "";

console.log("\nEncrypting message...");
console.log(`  Content: "${message}"`);
console.log(`  Length: ${message.length}\n`);
assert.strictEqual(message.length % 2, 0);
assert.strictEqual(message.length / 2, weeks_per_month * days_per_week);

function toHexa(c, length) {
    return c.toString(16).toUpperCase().padStart(length, "0");
}

for (let i = 0; i < message.length / 2; i++) {
    if ( i % days_per_week == 0)
        calendar += "\n\n    ";

    let line = "";

    // Print two consecutive letters.
    let l = fromASCII(message[2 * i]);
    let h = fromASCII(message[2 * i + 1]);
    line += `${message[2 * i]}, ${toHexa(l.toString(16), 2)} `
    line += `${message[2 * i + 1]}, ${toHexa(h.toString(16), 2)} `

    // Group and encrypt them.
    let c = concat(l, h) ** e % n;
    line += ` | ${toHexa(c, 3)}`;
    calendar += `${toHexa(c, 3)} `;

    // Decrypt.
    let u = c ** d % n;
    line += ` | ${toHexa(u, 3)}`;

    // Ungroup letters.
    let ul = toASCII(low(u));
    let uh = toASCII(high(u));
    line += ` | ${ul}, ${toHexa(low(u), 2)}, ${uh}, ${toHexa(high(u), 2)}`;

    // Print the line and verify that deciphered text matches the original one.
    console.log(line);
    assert.strictEqual(ul, message[2 * i]);
    assert.strictEqual(uh, message[2 * i + 1]);
}
console.log("\ndone.")

// Print the calendar.
calendar += "\n";
console.log(calendar);