fix #35 migrate to Typescript

This commit is contained in:
Pavel Rybalko 2021-09-23 11:18:01 +07:00
parent 77ba6c7939
commit c79f43b613
No known key found for this signature in database
GPG Key ID: 84C73B280D672411
23 changed files with 2882 additions and 1460 deletions

View File

@ -13,7 +13,8 @@ module.exports = {
globalReturn: false,
impliedStrict: false,
jsx: false
}
},
parser: "@typescript-eslint/parser"
},
env: {
browser: true,

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v14.17.6

View File

@ -1,10 +1,10 @@
module.exports = {
moduleFileExtensions: ["js", "jsx", "json", "vue"],
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "vue"],
transform: {
"^.+\\.vue$": "vue-jest",
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
"jest-transform-stub",
"^.+\\.jsx?$": "babel-jest"
"^.+\\.(js|jsx|ts|tsx)$": "ts-jest"
},
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"

View File

@ -19,17 +19,20 @@
"scryptsy": "^2.0.0",
"secrets.js-grempe": "^1.1.0",
"tweetnacl": "^1.0.0",
"typescript": "^4.4.3",
"vue": "^2.5.17",
"vue-qrcode-reader": "^1.3.1",
"vue-qriously": "^1.1.1",
"vue-router": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-eslint": "^3.1.5",
"@vue/cli-plugin-unit-jest": "^3.1.1",
"@types/jest": "^26.0.24",
"@typescript-eslint/parser": "^4.31.2",
"@vue/cli-plugin-eslint": "^4.5.13",
"@vue/cli-plugin-typescript": "^4.5.13",
"@vue/cli-plugin-unit-jest": "^4.5.13",
"@vue/cli-service": "^3.1.4",
"@vue/test-utils": "^1.0.0-beta.20",
"babel-eslint": "^10.0.1",
"eslint": "^5.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
@ -40,7 +43,9 @@
"html-webpack-inline-source-plugin": "1.0.0-beta.2",
"html-webpack-plugin": "4.0.0-beta.4",
"jest-junit": "^6.0.1",
"vue-template-compiler": "^2.5.17"
"ts-jest": "^26.4.4",
"vue-template-compiler": "^2.5.17",
"vuetype": "^0.3.2"
},
"postcss": {
"plugins": {
@ -50,7 +55,7 @@
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
"not ie <= 11"
],
"jest-junit": {
"outputDirectory": ".test-results/jest",

View File

@ -35,22 +35,23 @@
</div>
</template>
<script>
import GeneralInfo from "./components/GeneralInfo";
import GoOfflineInfo from "./components/GoOfflineInfo";
import SavePageInfo from "./components/SavePageInfo";
import ForkMe from "./components/ForkMe";
<script lang="ts">
import GeneralInfo from "./components/GeneralInfo.vue";
import GoOfflineInfo from "./components/GoOfflineInfo.vue";
import SavePageInfo from "./components/SavePageInfo.vue";
import ForkMe from "./components/ForkMe.vue";
import { version } from "../package.json";
import Vue from "vue";
export default {
export default Vue.extend({
name: "App",
components: { GeneralInfo, GoOfflineInfo, SavePageInfo, ForkMe },
computed: {
localFile: function() {
localFile(): boolean {
return window.location.protocol === "file:";
},
secure: function() {
secure(): boolean {
if (process.env.NODE_ENV === "production") {
return this.localFile && !this.isOnline;
} else {
@ -64,7 +65,7 @@ export default {
return process.env.GIT_REVISION;
}
}
};
});
</script>
<style>

View File

@ -2,16 +2,24 @@
<canvas v-canvas-message="text" class="canvasText" width="0" height="28px" />
</template>
<script>
export default {
<script lang="ts">
import Vue, { VNodeDirective } from "vue";
export default Vue.extend({
name: "CanvasText",
directives: {
canvasMessage: function(canvasElement, binding) {
var context = canvasElement.getContext("2d");
canvasMessage: function(el: HTMLElement, binding: VNodeDirective): void {
const canvasElement = el as HTMLCanvasElement,
context = canvasElement.getContext("2d");
if (!context) {
// eslint-disable-next-line no-console
console.warn("Failed to find canvasElement context");
return;
}
context.clearRect(0, 0, canvasElement.width, canvasElement.height);
context.fillStyle = "black";
context.font = "20px Arial";
var textSize = context.measureText(binding.value);
const textSize = context.measureText(binding.value);
canvasElement.setAttribute("width", textSize.width + 20 + "px");
context.font = "20px Arial";
context.fillText(binding.value, 15, 20);
@ -23,5 +31,5 @@ export default {
required: true
}
}
};
});
</script>

View File

@ -4,7 +4,7 @@
</span>
</template>
<script>
<script lang="ts">
export default {
name: "ForkMe",
props: {

View File

@ -39,19 +39,21 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "GeneralInfo",
data: function() {
return { unfolded: true };
},
created: function() {
var self = this;
const self = this;
this.$eventHub.$on("foldGeneralInfo", function() {
self.unfolded = false;
});
}
};
});
</script>
<style>

View File

@ -19,7 +19,7 @@
</div>
</template>
<script>
<script lang="ts">
export default {
name: "GoOfflineInfo"
};

View File

@ -8,7 +8,7 @@
</div>
</template>
<script>
<script lang="ts">
export default {
name: "SavePageInfo"
};

View File

@ -1,9 +1,8 @@
<script>
import Vue from "vue";
<script lang="ts">
import Vue, { VNode } from "vue";
import ShardQrCode from "./ShardQrCode.vue";
import ShardQrCode from "./ShardQrCode";
export default {
export default Vue.extend({
name: "ShardInfo",
components: { ShardQrCode },
props: {
@ -24,18 +23,23 @@ export default {
this.vm.$el.remove();
this.vm.$destroy();
},
render: function() {
var element = document.createElement("div");
var print = document.getElementById("print");
print.appendChild(element);
var passedProps = this.$props;
render: function(): VNode {
const element = document.createElement("div");
const print = document.getElementById("print");
if (print) {
print.appendChild(element);
} else {
// eslint-disable-next-line no-console
console.warn("Failed to find `print` element");
}
const passedProps = this.$props;
this.vm = new Vue({
el: element,
render: function(h) {
return h(ShardQrCode, { props: passedProps });
}
});
return this.vm;
return this.vm.$vnode;
}
};
});
</script>

View File

@ -26,9 +26,10 @@
</div>
</template>
<script>
<script lang="ts">
import Vue from "vue";
import { version } from "../../package.json";
export default {
export default Vue.extend({
name: "ShardQrCode",
props: {
title: {
@ -59,7 +60,7 @@ export default {
return process.env.GIT_REVISION;
}
}
};
});
</script>
<style>

View File

@ -1,8 +1,18 @@
import Vue from "vue";
declare module "vue/types/vue" {
interface Vue {
$eventHub: Vue;
vm: Vue;
isOnline: boolean;
}
}
// @ts-ignore
import VueQriously from "vue-qriously";
Vue.use(VueQriously);
// @ts-ignore
import QrcodeStream from "vue-qrcode-reader";
Vue.use(QrcodeStream);

View File

@ -1,5 +1,7 @@
import { VueConstructor } from "vue";
const plugin = {
install(Vue) {
install(Vue: VueConstructor) {
const vm = new Vue({
data: {
online: window.navigator.onLine

View File

@ -1,9 +1,9 @@
import Vue from "vue";
import Router from "vue-router";
import Info from "./views/Info";
import Share from "./views/Share";
import Combine from "./views/Combine";
import Info from "./views/Info.vue";
import Share from "./views/Share.vue";
import Combine from "./views/Combine.vue";
Vue.use(Router);

View File

@ -4,6 +4,20 @@ const SCRYPT = require("scryptsy");
const SECRETS = require("secrets.js-grempe");
export type Shard = {
data: string;
version: number;
title: string;
nonce: string;
requiredShards: number;
};
type EncryptedData = {
value: Uint8Array;
nonce: Uint8Array;
salt: Uint8Array;
};
const HexEncodeArray = [
"0",
"1",
@ -23,34 +37,34 @@ const HexEncodeArray = [
"f"
];
function strToUint8Array(str) {
return new TextEncoder("utf-8").encode(str);
function strToUint8Array(str: string): Uint8Array {
return new TextEncoder().encode(str);
}
function uint8ArrayToStr(arr) {
function uint8ArrayToStr(arr: Uint8Array): string {
return new TextDecoder("utf-8").decode(arr);
}
function hashString(str) {
function hashString(str: string): Uint8Array {
return CRYPTO.hash(strToUint8Array(str));
}
function hexify(arr) {
var s = "";
for (var i = 0; i < arr.length; i++) {
function hexify(arr: Uint8Array): string {
let s = "";
for (let i = 0; i < arr.length; i++) {
// `i` is a numerical counter for the loop and is never changed outside of there
// therefore `i` is numerical, and is safe to use as an array index
// eslint-disable-next-line security/detect-object-injection
var code = arr[i];
const code = arr[i];
s += HexEncodeArray[code >>> 4];
s += HexEncodeArray[code & 0x0f];
}
return s;
}
function dehexify(str) {
var arr = new Uint8Array(str.length / 2);
for (var i = 0; i < arr.length; i++) {
function dehexify(str: string): Uint8Array {
const arr = new Uint8Array(str.length / 2);
for (let i = 0; i < arr.length; i++) {
// `i` is a numerical counter for the loop and is never changed outside of there
// therefore `i` is numerical, and is safe to use as an array index
// eslint-disable-next-line security/detect-object-injection
@ -59,9 +73,13 @@ function dehexify(str) {
return arr;
}
function encrypt(data, salt, passphrase) {
var key = SCRYPT(passphrase, Buffer.from(salt), 1 << 15, 8, 1, 32);
var nonce = CRYPTO.randomBytes(24);
function encrypt(
data: string,
salt: Uint8Array,
passphrase: string
): EncryptedData {
const key = SCRYPT(passphrase, Buffer.from(salt), 1 << 15, 8, 1, 32);
const nonce = CRYPTO.randomBytes(24);
return {
nonce,
salt,
@ -69,23 +87,34 @@ function encrypt(data, salt, passphrase) {
};
}
function decrypt(data, salt, passphrase, nonce) {
var key = SCRYPT(passphrase, Buffer.from(salt.buffer), 1 << 15, 8, 1, 32);
function decrypt(
data: Uint8Array,
salt: Uint8Array,
passphrase: string,
nonce: Uint8Array
): Uint8Array {
const key = SCRYPT(passphrase, Buffer.from(salt.buffer), 1 << 15, 8, 1, 32);
// This is a false positive, `secretbox.open` is unrelated to `fs.open`
// eslint-disable-next-line security/detect-non-literal-fs-filename
return CRYPTO.secretbox.open(data, nonce, key);
}
function share(data, title, passphrase, totalShards, requiredShards) {
var salt = hashString(title);
var encrypted = encrypt(data, salt, passphrase);
var nonce = BASE64.fromByteArray(encrypted.nonce);
var hexEncrypted = hexify(encrypted.value);
function share(
data: string,
title: string,
passphrase: string,
totalShards: number,
requiredShards: number
): string[] {
const salt = hashString(title),
encrypted = encrypt(data, salt, passphrase),
nonce = BASE64.fromByteArray(encrypted.nonce),
hexEncrypted = hexify(encrypted.value);
return SECRETS.share(hexEncrypted, totalShards, requiredShards).map(function(
shard
shard: string
) {
// First char is non-hex (base36) and signifies the bitfield size of our share
var encodedShard =
const encodedShard =
shard[0] + BASE64.fromByteArray(dehexify(shard.slice(1)));
return JSON.stringify({
@ -100,7 +129,7 @@ function share(data, title, passphrase, totalShards, requiredShards) {
});
}
function parse(payload) {
function parse(payload: string): Shard {
let parsed = JSON.parse(payload);
return {
version: parsed.v || 0, // 'undefined' version is treated as 0
@ -111,8 +140,8 @@ function parse(payload) {
};
}
function reconstruct(shardObjects, passphrase) {
var shardsRequirements = shardObjects.map(shard => shard.requiredShards);
function reconstruct(shardObjects: Shard[], passphrase: string): string {
const shardsRequirements = shardObjects.map(shard => shard.requiredShards);
if (!shardsRequirements.every(r => r === shardsRequirements[0])) {
throw "Mismatching min shards requirement among shards!";
}
@ -122,38 +151,39 @@ function reconstruct(shardObjects, passphrase) {
} provided`;
}
var nonces = shardObjects.map(shard => shard.nonce);
const nonces = shardObjects.map(shard => shard.nonce);
if (!nonces.every(n => n === nonces[0])) {
throw "Nonces mismatch among shards!";
}
var titles = shardObjects.map(shard => shard.title);
const titles = shardObjects.map(shard => shard.title);
if (!titles.every(t => t === titles[0])) {
throw "Titles mismatch among shards!";
}
var versions = shardObjects.map(shard => shard.version);
const versions = shardObjects.map(shard => shard.version);
if (!versions.every(v => v === versions[0])) {
throw "Versions mismatch along shards!";
}
switch (versions[0]) {
case 0:
var shardData = shardObjects.map(shard => shard.data);
var encryptedSecret = SECRETS.combine(shardData);
var secret = dehexify(encryptedSecret);
var nonce = dehexify(nonces[0]);
var salt = hashString(titles[0]);
var shardData = shardObjects.map(shard => shard.data),
encryptedSecret = SECRETS.combine(shardData),
secret = dehexify(encryptedSecret),
nonce = dehexify(nonces[0]),
salt = hashString(titles[0]);
return uint8ArrayToStr(decrypt(secret, salt, passphrase, nonce));
case 1:
var shardDataV1 = shardObjects.map(
shard => shard.data[0] + hexify(BASE64.toByteArray(shard.data.slice(1)))
);
var encryptedSecretV1 = SECRETS.combine(shardDataV1);
var secretV1 = dehexify(encryptedSecretV1);
var nonceV1 = BASE64.toByteArray(nonces[0]);
var saltV1 = hashString(titles[0]);
shard =>
shard.data[0] + hexify(BASE64.toByteArray(shard.data.slice(1)))
),
encryptedSecretV1 = SECRETS.combine(shardDataV1),
secretV1 = dehexify(encryptedSecretV1),
nonceV1 = BASE64.toByteArray(nonces[0]),
saltV1 = hashString(titles[0]);
return uint8ArrayToStr(decrypt(secretV1, saltV1, passphrase, nonceV1));
default:

View File

@ -7778,12 +7778,13 @@ export default {
"zoom"
],
generate: function(amount) {
const Crypto = window.crypto || window.msCrypto;
var keys = new Uint16Array(amount);
generate: function(amount: number) {
// @ts-ignore
const Crypto = window.crypto || window.msCrypto; // for IE 11;
const keys = new Uint16Array(amount);
Crypto.getRandomValues(keys);
var wordlist = this.wordlist;
const wordlist = this.wordlist;
return (
Array.from(keys.map(key => key % 2048))
// `key` is an element in `keys`, which can only hold Uints;

View File

@ -53,33 +53,46 @@
</div>
</template>
<script>
<script lang="ts">
/*eslint no-console: ["error", { allow: ["warn", "error"] }] */
import crypto from "../util/crypto";
import crypto, { Shard } from "../util/crypto";
import Vue from "vue";
export default {
type CombineData = {
title: string;
nonce: string;
shards: Shard[];
qrCodes: Set<string>;
requiredShards?: number;
passphrase: string;
recoveredSecret?: string;
PLACEHOLDER_QR_DATA: string;
};
export default Vue.extend({
name: "Combine",
data: function() {
data(): CombineData {
return {
title: "",
nonce: "",
shards: new Set(),
qrCodes: [],
shards: [],
qrCodes: new Set(),
requiredShards: undefined,
passphrase: "",
recoveredSecret: undefined
recoveredSecret: undefined,
PLACEHOLDER_QR_DATA: ""
};
},
computed: {
needMoreShards: function() {
return !this.requiredShards || this.qrCodes.length < this.requiredShards;
needMoreShards(): boolean {
return !this.requiredShards || this.shards.length < this.requiredShards;
},
remainingCodes: function() {
remainingCodes(): number {
if (!this.requiredShards) {
return 0;
} else {
return this.requiredShards - this.qrCodes.length;
return this.requiredShards - this.shards.length;
}
}
},
@ -87,35 +100,35 @@ export default {
this.$eventHub.$emit("foldGeneralInfo");
},
methods: {
onDecode: function(result) {
var parsed = crypto.parse(result);
if (!this.shards.has(parsed.data)) {
if (this.title && this.title != parsed.title) {
console.error("title mismatch!");
return;
} else {
this.title = parsed.title;
}
if (this.nonce && this.nonce != parsed.nonce) {
console.error("nonce mismatch!");
return;
} else {
this.nonce = parsed.nonce;
}
if (
this.requiredShards &&
this.requiredShards != parsed.requiredShards
) {
console.error("requiredShards mismatch");
return;
} else {
this.requiredShards = parsed.requiredShards;
}
this.qrCodes.push(result);
this.shards.add(parsed);
} else {
onDecode: function(result: string) {
if (this.qrCodes.has(result)) {
console.warn("Shard already seen");
return;
}
const parsed = crypto.parse(result);
if (this.title && this.title !== parsed.title) {
console.error("title mismatch!");
return;
} else {
this.title = parsed.title;
}
if (this.nonce && this.nonce !== parsed.nonce) {
console.error("nonce mismatch!");
return;
} else {
this.nonce = parsed.nonce;
}
if (
this.requiredShards &&
this.requiredShards !== parsed.requiredShards
) {
console.error("requiredShards mismatch");
return;
} else {
this.requiredShards = parsed.requiredShards;
}
this.qrCodes.add(result);
this.shards.push(parsed);
},
reconstruct: function() {
if (!this.passphrase) {
@ -125,11 +138,11 @@ export default {
.split(" ")
.filter(el => el)
.join("-");
var shards = Array.from(this.shards);
const shards = Array.from(this.shards);
this.recoveredSecret = crypto.reconstruct(shards, this.passphrase);
}
}
};
});
</script>
<style>

View File

@ -2,7 +2,7 @@
<div />
</template>
<script>
<script lang="ts">
export default {};
</script>

View File

@ -69,17 +69,26 @@
</div>
</template>
<script>
<script lang="ts">
import passPhrase from "../util/passPhrase";
import crypto from "../util/crypto";
import ShardInfo from "../components/ShardInfo";
import CanvasText from "../components/CanvasText";
import ShardInfo from "../components/ShardInfo.vue";
import CanvasText from "../components/CanvasText.vue";
import Vue from "vue";
export default {
type ShareData = {
title: string;
secret: string;
totalShards: number;
recoveryPassphrase: string;
encryptionMode: boolean;
};
export default Vue.extend({
name: "Share",
components: { ShardInfo, CanvasText },
data: function() {
data(): ShareData {
return {
title: "",
secret: "",
@ -89,13 +98,13 @@ export default {
};
},
computed: {
secretTooLong: function() {
secretTooLong(): boolean {
return this.secret.length > 1024;
},
requiredShards: function() {
requiredShards(): number {
return Math.floor(this.totalShards / 2) + 1;
},
shards: function() {
shards(): string[] {
if (!this.encryptionMode) {
return [];
}
@ -122,7 +131,7 @@ export default {
this.encryptionMode = !this.encryptionMode;
}
}
};
});
</script>
<style>

64
tsconfig.json Normal file
View File

@ -0,0 +1,64 @@
{
"compilerOptions": {
/* Basic Options */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "build", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitOverride": true,
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"resolveJsonModule": true,
/* Advanced Options */
// "declarationDir": "lib" /* Output directory for generated declaration files. */
}
}

3904
yarn.lock

File diff suppressed because it is too large Load Diff