mirror of
				https://github.com/Alamantus/otp-generator.git
				synced 2025-11-04 10:17:04 +01:00 
			
		
		
		
	Use random.org to choose the random pad values
This commit is contained in:
		
							parent
							
								
									b931f18f03
								
							
						
					
					
						commit
						6fb8494c8f
					
				
					 14 changed files with 757 additions and 38 deletions
				
			
		| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
# One-Time Pad Generator
 | 
			
		||||
 | 
			
		||||
Encrypt messages with a randomly-generated [one-time pad](https://en.wikipedia.org/wiki/One-time_pad)! Send your encrypted message along with your pad,
 | 
			
		||||
and your friend can decrypt the message. Just be sure you destroy the pad when you're done!
 | 
			
		||||
Encrypt messages with a randomly-generated [one-time pad](https://en.wikipedia.org/wiki/One-time_pad)!
 | 
			
		||||
Send your encrypted message along with your pad, and your friend can decrypt the message.
 | 
			
		||||
Just be sure you destroy the pad when you're done!
 | 
			
		||||
 | 
			
		||||
Uses [Random.org](https://www.random.org) to generate a truly random one-time pad.
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
require=function(r,e,n){function t(n,o){function i(r){return t(i.resolve(r))}function f(e){return r[n][1][e]||e}if(!e[n]){if(!r[n]){var c="function"==typeof require&&require;if(!o&&c)return c(n,!0);if(u)return u(n,!0);var l=new Error("Cannot find module '"+n+"'");throw l.code="MODULE_NOT_FOUND",l}i.resolve=f;var s=e[n]=new t.Module(n);r[n][0].call(s.exports,i,s,s.exports)}return e[n].exports}function o(r){this.id=r,this.bundle=t,this.exports={}}var u="function"==typeof require&&require;t.isParcelRequire=!0,t.Module=o,t.modules=r,t.cache=e,t.parent=u;for(var i=0;i<n.length;i++)t(n[i]);return t}({4:[function(require,module,exports) {
 | 
			
		||||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","&","$"],t=exports.generatePad=function(t){for(var n=[],u=0;u<t;u++){var a=Math.floor(Math.random()*e.length);n.push(e[a])}return n},n=function(e){return e.replace(/[\s]+/g,"&").replace(/[^a-zA-Z0-9\&]/g,"$")},u=exports.encrypt=function(u){var a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,d=n(u).toUpperCase();return{oneTimePad:a=a||t(d.length),encryptedMessage:a.map(function(t,n){var u=d.charAt(n),a=""!==u?e.indexOf(u):e.length-1,r=e.indexOf(t);return e[(a+r)%e.length]}).join("")}},a=exports.decrypt=function(t,n){return t=t.toUpperCase(),n.map(function(n,u){for(var a=e.indexOf(t.charAt(u))-e.indexOf(n);a<0;)a+=e.length;return e[a%e.length]}).join("").replace(/\&/g," ").replace(/\$/g,"-")};window.onload=function(){document.getElementById("encryptInput").onclick=function(){var e=document.getElementById("inputError"),t=document.getElementById("input").value,a=n(document.getElementById("inputPad").value).toUpperCase(),d=""!==a?a.split(""):null;if(null!==d&&d.length<t.length)document.getElementById("inputPad").value=d.join(""),e.innerHTML="The pad must be at least as long as the input";else{e.innerHTML="";var r=u(t,d);document.getElementById("inputPad").value=r.oneTimePad.join(""),document.getElementById("encrypted").innerHTML=r.encryptedMessage}},document.getElementById("decryptInput").onclick=function(){var e=document.getElementById("encryptedInput").value,t=document.getElementById("encryptedInputPad").value.split(""),n=a(e,t);document.getElementById("decrypted").innerHTML=n},document.getElementById("padLength").oninput=function(e){parseInt(e.target.value)<1&&(e.target.value=1)},document.getElementById("generatePad").onclick=function(){var e=document.getElementById("padLength");""===e.value&&(e.value="10");var n=parseInt(e.value,10),u=t(n);document.getElementById("inputPad").value=u.join("")},document.getElementById("clearPad").onclick=function(){document.getElementById("padLength").value="",document.getElementById("inputPad").value=""}};
 | 
			
		||||
},{}]},{},[4])
 | 
			
		||||
							
								
								
									
										684
									
								
								docs/c78a0231a6d0c69eb078bc96cafd649c.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										684
									
								
								docs/c78a0231a6d0c69eb078bc96cafd649c.js
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 434 KiB  | 
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>One-Time Pad Generator</title> <meta name="description" content="One-Time Pad Generator"> <link rel="stylesheet" href="c612cd5e7d7f16f744f6cbfb390e8362.css"> <link rel="stylesheet" href="8e744682420400baa41f79ace446de2c.css"> <style>pre{word-break:break-all;white-space:pre-wrap}</style> <!--[if lt IE 9]>
 | 
			
		||||
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>One-Time Pad Generator</title> <meta name="description" content="One-Time Pad Generator"> <link rel="stylesheet" href="2d72691b72b80268e116b606ca55ce89.css"> <link rel="stylesheet" href="f8167f18c4f78a03b0feca8921b96daa.css"> <style>pre{word-break:break-all;white-space:pre-wrap}</style> <!--[if lt IE 9]>
 | 
			
		||||
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
 | 
			
		||||
  <![endif]--> </head> <body> <div class="section"> <div class="container"> <div class="columns"> <div class="column"> <h2 class="title"> Encrypt a Message </h2> <div class="field"> <label class="label"> Your Message <div class="control"> <textarea id="input" class="textarea"></textarea> </div> </label> </div> <div class="control"> <label class="label"> One-Time Pad </label> </div> <div class="field has-addons is-small"> <div class="control"> <input class="input is-small" type="number" id="padLength" placeholder="Length"> </div> <div class="control"> <a class="button is-small" id="generatePad"> <span> Generate Pad </span> <span class="icon is-small"> <i class="fa fa-lock"></i> </span> </a> </div> <div class="control"> <a class="button is-small is-danger" id="clearPad"> <span> Clear Pad </span> <span class="icon is-small"> <i class="fa fa-asterisk"></i> </span> </a> </div> </div> <div class="control"> <textarea id="inputPad" class="textarea"></textarea> <div class="help is-danger" id="inputError"></div> </div> <div class="field"> <div class="control"> <a class="button" id="encryptInput"> <span> Encrypt </span> <span class="icon"> <i class="fa fa-lock"></i> </span> </a> </div> </div> <div class="columns"> <div class="column"> <label class="label">Encrypted Message</label> <div class="box"> <pre id="encrypted"></pre> </div> </div> </div> </div> <div class="column"> <h2 class="title"> Decrypt a Message </h2> <div class="field"> <label class="label"> Their Message <div class="control"> <textarea id="encryptedInput" class="textarea"></textarea> </div> </label> </div> <div class="field"> <label class="label"> One-Time Pad <div class="control"> <textarea id="encryptedInputPad" class="textarea"></textarea> </div> <div class="help is-error" id="inputError"></div> </label> </div> <div class="field"> <div class="control"> <a class="button" id="decryptInput"> <span> Decrypt </span> <span class="icon"> <i class="fa fa-unlock"></i> </span> </a> </div> </div> <div class="columns"> <div class="column"> <label class="label">Decrypted Message</label> <div class="box"> <pre id="decrypted"></pre> </div> </div> </div> </div> </div> </div> </div> <script src="af63cea32e54b78eeda13c0231ff431e.js"></script> </body> </html>
 | 
			
		||||
  <![endif]--> </head> <body> <div class="section"> <div class="container"> <div class="columns"> <div class="column"> <h2 class="title"> Encrypt a Message </h2> <div class="field"> <label class="label"> Your Message <div class="control"> <textarea id="input" class="textarea"></textarea> </div> </label> </div> <div class="control"> <label class="label"> One-Time Pad </label> </div> <div class="field has-addons is-small"> <div class="control"> <input class="input is-small" type="number" id="padLength" placeholder="Length"> </div> <div class="control"> <a class="button is-small" id="generatePad"> <span> Generate Pad </span> <span class="icon is-small"> <i class="fa fa-lock"></i> </span> </a> </div> <div class="control"> <a class="button is-small is-danger" id="clearPad"> <span> Clear Pad </span> <span class="icon is-small"> <i class="fa fa-asterisk"></i> </span> </a> </div> </div> <div class="control"> <textarea id="inputPad" class="textarea"></textarea> <div class="help is-danger" id="inputError"></div> </div> <div class="field"> <div class="control"> <a class="button" id="encryptInput"> <span> Encrypt </span> <span class="icon"> <i class="fa fa-lock"></i> </span> </a> </div> </div> <div class="columns"> <div class="column"> <label class="label">Encrypted Message</label> <div class="box"> <pre id="encrypted"></pre> </div> </div> </div> </div> <div class="column"> <h2 class="title"> Decrypt a Message </h2> <div class="field"> <label class="label"> Their Message <div class="control"> <textarea id="encryptedInput" class="textarea"></textarea> </div> </label> </div> <div class="field"> <label class="label"> One-Time Pad <div class="control"> <textarea id="encryptedInputPad" class="textarea"></textarea> </div> <div class="help is-error" id="inputError"></div> </label> </div> <div class="field"> <div class="control"> <a class="button" id="decryptInput"> <span> Decrypt </span> <span class="icon"> <i class="fa fa-unlock"></i> </span> </a> </div> </div> <div class="columns"> <div class="column"> <label class="label">Decrypted Message</label> <div class="box"> <pre id="decrypted"></pre> </div> </div> </div> </div> </div> </div> </div> <script src="c78a0231a6d0c69eb078bc96cafd649c.js"></script> </body> </html>
 | 
			
		||||
							
								
								
									
										68
									
								
								index.js
									
										
									
									
									
								
							
							
						
						
									
										68
									
								
								index.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,3 +1,6 @@
 | 
			
		|||
import 'babel-polyfill';
 | 
			
		||||
import 'whatwg-fetch';
 | 
			
		||||
 | 
			
		||||
const CHARS = [
 | 
			
		||||
  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B',
 | 
			
		||||
  'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
 | 
			
		||||
| 
						 | 
				
			
			@ -6,12 +9,8 @@ const CHARS = [
 | 
			
		|||
];
 | 
			
		||||
 | 
			
		||||
export const generatePad = (length) => {
 | 
			
		||||
  const pad = [];
 | 
			
		||||
  for (let i = 0; i < length; i++) {
 | 
			
		||||
    const letter = Math.floor(Math.random() * CHARS.length);
 | 
			
		||||
    pad.push(CHARS[letter]);
 | 
			
		||||
  }
 | 
			
		||||
  return pad;
 | 
			
		||||
  return fetch(`https://www.random.org/integers/?num=${length}&min=0&max=${CHARS.length - 1}&col=1&base=10&format=plain&rnd=new`)
 | 
			
		||||
    .then(response => response.text()).then(text => text.split('\n').map(number => CHARS[parseInt(number)]));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const stripString = (string) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,31 +19,35 @@ const stripString = (string) => {
 | 
			
		|||
 | 
			
		||||
export const encrypt = (string, pad = null) => {
 | 
			
		||||
  const strippedString = stripString(string).toUpperCase();
 | 
			
		||||
  pad = pad ? pad : generatePad(strippedString.length);
 | 
			
		||||
  return {
 | 
			
		||||
    oneTimePad: pad,
 | 
			
		||||
    encryptedMessage: pad.map((letter, index) => {
 | 
			
		||||
      const messageLetter = strippedString.charAt(index);
 | 
			
		||||
      const letterValue = messageLetter !== '' ? CHARS.indexOf(messageLetter) : CHARS.length - 1;
 | 
			
		||||
      const padValue = CHARS.indexOf(letter);
 | 
			
		||||
      return CHARS[(letterValue + padValue) % CHARS.length];
 | 
			
		||||
    }).join(''),
 | 
			
		||||
  };
 | 
			
		||||
  const padPromise = pad ? Promise.resolve(pad) : generatePad(strippedString.length);
 | 
			
		||||
  return padPromise.then(pad => {
 | 
			
		||||
    return {
 | 
			
		||||
      oneTimePad: pad,
 | 
			
		||||
      encryptedMessage: pad.map((letter, index) => {
 | 
			
		||||
        const messageLetter = strippedString.charAt(index);
 | 
			
		||||
        const letterValue = messageLetter !== '' ? CHARS.indexOf(messageLetter) : CHARS.length - 1;
 | 
			
		||||
        const padValue = CHARS.indexOf(letter);
 | 
			
		||||
        return CHARS[(letterValue + padValue) % CHARS.length];
 | 
			
		||||
      }).join(''),
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const decrypt = (string, pad) => {
 | 
			
		||||
  string = string.toUpperCase();
 | 
			
		||||
  return pad.map((letter, index) => {
 | 
			
		||||
  const message = pad.map((letter, index) => {
 | 
			
		||||
    const letterValue = CHARS.indexOf(string.charAt(index));
 | 
			
		||||
    const padValue = CHARS.indexOf(letter);
 | 
			
		||||
    let charIndex = (letterValue - padValue);
 | 
			
		||||
    while (charIndex < 0) {charIndex += CHARS.length}
 | 
			
		||||
    return CHARS[charIndex % CHARS.length];
 | 
			
		||||
  }).join('').replace(/\&/g, ' ').replace(/\$/g, '-');
 | 
			
		||||
  return Promise.resolve(message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.onload = () => {
 | 
			
		||||
  document.getElementById('encryptInput').onclick = () => {
 | 
			
		||||
  const encryptButton = document.getElementById('encryptInput');
 | 
			
		||||
  encryptButton.onclick = () => {
 | 
			
		||||
    const error = document.getElementById('inputError');
 | 
			
		||||
    const input = document.getElementById('input').value;
 | 
			
		||||
    const inputPad = stripString(document.getElementById('inputPad').value).toUpperCase();
 | 
			
		||||
| 
						 | 
				
			
			@ -54,17 +57,24 @@ window.onload = () => {
 | 
			
		|||
      error.innerHTML = 'The pad must be at least as long as the input';
 | 
			
		||||
    } else {
 | 
			
		||||
      error.innerHTML = '';
 | 
			
		||||
      const encryption = encrypt(input, pad);
 | 
			
		||||
      document.getElementById('inputPad').value = encryption.oneTimePad.join('');
 | 
			
		||||
      document.getElementById('encrypted').innerHTML = encryption.encryptedMessage;
 | 
			
		||||
      encryptButton.classList.add('is-loading');
 | 
			
		||||
      encryptButton.disabled = true;
 | 
			
		||||
      encrypt(input, pad).then(encryption => {
 | 
			
		||||
        document.getElementById('padLength').value = encryption.oneTimePad.length;
 | 
			
		||||
        document.getElementById('inputPad').value = encryption.oneTimePad.join('');
 | 
			
		||||
        document.getElementById('encrypted').innerHTML = encryption.encryptedMessage;
 | 
			
		||||
        encryptButton.classList.remove('is-loading');
 | 
			
		||||
        encryptButton.disabled = false;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  document.getElementById('decryptInput').onclick = () => {
 | 
			
		||||
    const input = document.getElementById('encryptedInput').value;
 | 
			
		||||
    const pad = document.getElementById('encryptedInputPad').value.split('');
 | 
			
		||||
    const output = decrypt(input, pad);
 | 
			
		||||
    document.getElementById('decrypted').innerHTML = output;
 | 
			
		||||
    decrypt(input, pad).then(output => {
 | 
			
		||||
      document.getElementById('decrypted').innerHTML = output;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  document.getElementById('padLength').oninput = (event) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,14 +82,20 @@ window.onload = () => {
 | 
			
		|||
    if (value < 1) event.target.value = 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  document.getElementById('generatePad').onclick = () => {
 | 
			
		||||
  const generatePadButton = document.getElementById('generatePad');
 | 
			
		||||
  generatePadButton.onclick = () => {
 | 
			
		||||
    const field = document.getElementById('padLength');
 | 
			
		||||
    if (field.value === '') {
 | 
			
		||||
      field.value = '10';
 | 
			
		||||
    }
 | 
			
		||||
    const length = parseInt(field.value, 10);
 | 
			
		||||
    const output = generatePad(length);
 | 
			
		||||
    document.getElementById('inputPad').value = output.join('');
 | 
			
		||||
    generatePadButton.classList.add('is-loading');
 | 
			
		||||
    generatePadButton.disabled = true;
 | 
			
		||||
    generatePad(length).then(output => {
 | 
			
		||||
      document.getElementById('inputPad').value = output.join('');
 | 
			
		||||
      generatePadButton.classList.remove('is-loading');
 | 
			
		||||
      generatePadButton.disabled = false;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  document.getElementById('clearPad').onclick = () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,14 +6,17 @@
 | 
			
		|||
  "license": "MIT",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "parcel ./index.html",
 | 
			
		||||
    "build": "parcel build ./index.html --out-dir docs --public-url ./"
 | 
			
		||||
    "build": "rimraf docs && parcel build ./index.html --out-dir docs --public-url ./"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "babel-preset-env": "^1.6.1",
 | 
			
		||||
    "parcel-bundler": "^1.6.2"
 | 
			
		||||
    "parcel-bundler": "^1.6.2",
 | 
			
		||||
    "rimraf": "^2.6.2"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "babel-polyfill": "^6.26.0",
 | 
			
		||||
    "bulma": "^0.6.2",
 | 
			
		||||
    "font-awesome": "^4.7.0"
 | 
			
		||||
    "font-awesome": "^4.7.0",
 | 
			
		||||
    "whatwg-fetch": "^2.0.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								yarn.lock
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -539,6 +539,14 @@ babel-plugin-transform-strict-mode@^6.24.1:
 | 
			
		|||
    babel-runtime "^6.22.0"
 | 
			
		||||
    babel-types "^6.24.1"
 | 
			
		||||
 | 
			
		||||
babel-polyfill@^6.26.0:
 | 
			
		||||
  version "6.26.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    babel-runtime "^6.26.0"
 | 
			
		||||
    core-js "^2.5.0"
 | 
			
		||||
    regenerator-runtime "^0.10.5"
 | 
			
		||||
 | 
			
		||||
babel-preset-env@^1.6.1:
 | 
			
		||||
  version "1.6.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48"
 | 
			
		||||
| 
						 | 
				
			
			@ -2990,6 +2998,10 @@ regenerate@^1.2.1:
 | 
			
		|||
  version "1.3.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
 | 
			
		||||
 | 
			
		||||
regenerator-runtime@^0.10.5:
 | 
			
		||||
  version "0.10.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
 | 
			
		||||
 | 
			
		||||
regenerator-runtime@^0.11.0:
 | 
			
		||||
  version "0.11.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
 | 
			
		||||
| 
						 | 
				
			
			@ -3096,7 +3108,7 @@ right-align@^0.1.1:
 | 
			
		|||
  dependencies:
 | 
			
		||||
    align-text "^0.1.1"
 | 
			
		||||
 | 
			
		||||
rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1:
 | 
			
		||||
rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1, rimraf@^2.6.2:
 | 
			
		||||
  version "2.6.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
 | 
			
		||||
  dependencies:
 | 
			
		||||
| 
						 | 
				
			
			@ -3701,6 +3713,10 @@ vm-browserify@0.0.4:
 | 
			
		|||
  dependencies:
 | 
			
		||||
    indexof "0.0.1"
 | 
			
		||||
 | 
			
		||||
whatwg-fetch@^2.0.3:
 | 
			
		||||
  version "2.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
 | 
			
		||||
 | 
			
		||||
whet.extend@~0.9.9:
 | 
			
		||||
  version "0.9.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue