feat(tool): bip39-generator

This commit is contained in:
Corentin Thomasset 2022-04-11 22:47:05 +02:00
parent 3ae872847b
commit d55329f3ab
No known key found for this signature in database
GPG key ID: DBD997E935996158
5 changed files with 193 additions and 28 deletions

32
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "it-tools", "name": "it-tools",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@it-tools/bip39": "^0.0.4",
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0", "@vicons/tabler": "^0.12.0",
"@vueuse/core": "^8.2.1", "@vueuse/core": "^8.2.1",
@ -686,6 +687,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@it-tools/bip39": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@it-tools/bip39/-/bip39-0.0.4.tgz",
"integrity": "sha512-0PWO7VKi6VALiFcm8z2WgxzSZ5wAko0OctBZ0I5+jjtSIXm3t1d54yrrHfgFOZDTyMpCXi638oLpzqexcfRtbA==",
"dependencies": {
"js-sha256": "^0.9.0",
"nanoid": "^3.3.2"
},
"funding": {
"url": "https://github.com/sponsors/CorentinTh"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz",
@ -4961,6 +4974,11 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"node_modules/js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"node_modules/js-stringify": { "node_modules/js-stringify": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
@ -8416,6 +8434,15 @@
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true "dev": true
}, },
"@it-tools/bip39": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@it-tools/bip39/-/bip39-0.0.4.tgz",
"integrity": "sha512-0PWO7VKi6VALiFcm8z2WgxzSZ5wAko0OctBZ0I5+jjtSIXm3t1d54yrrHfgFOZDTyMpCXi638oLpzqexcfRtbA==",
"requires": {
"js-sha256": "^0.9.0",
"nanoid": "^3.3.2"
}
},
"@jridgewell/resolve-uri": { "@jridgewell/resolve-uri": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz",
@ -11516,6 +11543,11 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"js-stringify": { "js-stringify": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",

View file

@ -14,6 +14,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@it-tools/bip39": "^0.0.4",
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0", "@vicons/tabler": "^0.12.0",
"@vueuse/core": "^8.2.1", "@vueuse/core": "^8.2.1",

View file

@ -0,0 +1,27 @@
import { reactive, watch, type Ref } from 'vue';
type UseValidationRule<T> = {
validator: (value: T) => boolean
message: string
}
export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: UseValidationRule<T>[] }) {
const state = reactive<{
message: string,
status: undefined | 'error'
}>({
message: '',
status: undefined
})
watch([source], () => {
for(const rule of rules) {
if(!rule.validator(source.value)){
state.message = rule.message
state.status = 'error'
}
}
})
return state;
}

View file

@ -42,9 +42,13 @@ import {
NH3, NH3,
NEllipsis, NEllipsis,
NTag, NTag,
NInputGroup,
NInputGroupLabel,
} from 'naive-ui'; } from 'naive-ui';
const components = [ const components = [
NInputGroup,
NInputGroupLabel,
NTag, NTag,
NResult, NResult,
NEllipsis, NEllipsis,

View file

@ -1,27 +1,45 @@
<template> <template>
<div> <div>
<n-card> <n-card>
<n-space item-style="flex: 1 1 0"> <n-grid cols="3" x-gap="12">
<n-form-item label="Language:"> <n-gi span="1">
<n-select v-model:value="language" :options="languages" /> <n-form-item label="Language:">
</n-form-item> <n-select v-model:value="language"
<n-form-item label="Entropy (seed):"> :options="Object.keys(languages).map(label => ({ label, value: label }))" />
<n-input v-model:value="entropy" placeholder="Your string..." /> </n-form-item>
</n-form-item> </n-gi>
</n-space> <n-gi span="2">
<n-form-item label="Passphrase (mnemonic):"> <n-form-item label="Entropy (seed):" :feedback="entropyValidation.message"
<n-input :validation-status="entropyValidation.status">
style="text-align: center;" <n-input-group>
:value="passphrase" <n-input v-model:value="entropy" placeholder="Your string..." />
type="textarea" <n-button @click="refreshEntropy">
placeholder="Your string hash" <n-icon size="22">
:autosize="{ minRows: 1 }" <Refresh />
readonly </n-icon>
autocomplete="off" </n-button>
autocorrect="off" <n-button @click="copyEntropy">
autocapitalize="off" <n-icon size="22">
spellcheck="false" <Copy />
/> </n-icon>
</n-button>
</n-input-group>
</n-form-item>
</n-gi>
</n-grid>
<br>
<n-form-item label="Passphrase (mnemonic):" :feedback="mnemonicValidation.message"
:validation-status="mnemonicValidation.status">
<n-input-group>
<n-input style="text-align: center; flex: 1;" v-model:value="passphrase"
placeholder="Your mnemonic..." autocomplete="off" autocorrect="off" autocapitalize="off"
spellcheck="false" />
<n-button @click="copyPassphrase">
<n-icon size="22" :component="Copy" />
</n-button>
</n-input-group>
</n-form-item> </n-form-item>
</n-card> </n-card>
</div> </div>
@ -30,16 +48,99 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCopy } from '@/composable/copy'; import { useCopy } from '@/composable/copy';
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { entropyToMnemonic } from 'bip39' import {
entropyToMnemonic,
englishWordList,
chineseSimplifiedWordList,
chineseTraditionalWordList,
czechWordList,
frenchWordList,
italianWordList,
japaneseWordList,
koreanWordList,
portugueseWordList,
spanishWordList,
generateEntropy,
mnemonicToEntropy
} from '@it-tools/bip39'
import { Copy, Refresh } from '@vicons/tabler'
import { useValidation } from '@/composable/validation';
const entropy = ref('1d60683972011cb97322ed6ae96225f3')
const language = ref('English') const languages = {
const languages = ref(['English']) 'English': englishWordList,
const passphrase = computed(() => { 'Chinese simplified': chineseSimplifiedWordList,
// setDefaultWordlist(language.value) 'Chinese traditional': chineseTraditionalWordList,
return entropyToMnemonic(Buffer.from(entropy.value, "utf-8")) 'Czech': czechWordList,
'French': frenchWordList,
'Italian': italianWordList,
'Japanese': japaneseWordList,
'Korean': koreanWordList,
'Portuguese': portugueseWordList,
'Spanish': spanishWordList
}
const entropy = ref(generateEntropy())
const passphraseInput = ref('')
const language = ref<keyof typeof languages>('English')
const passphrase = computed({
get() {
try {
return entropyToMnemonic(entropy.value, languages[language.value])
} catch (_) {
return passphraseInput.value
}
},
set(value: string) {
passphraseInput.value = value
try {
entropy.value = mnemonicToEntropy(value, languages[language.value])
} catch (_) {
entropy.value = ''
}
}
}) })
const entropyValidation = useValidation({
source: entropy,
rules: [
{
validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4'
},
{
validator: (value) => /^[a-fA-f0-9]?$/.test(value),
message: 'Entropy should an hexadecimal number'
}
]
})
const mnemonicValidation = useValidation({
source: passphrase,
rules: [
{
validator: (value) => {
try {
mnemonicToEntropy(value)
return true
} catch (_) {
return false
}
},
message: 'Invalid mnemonic'
}
]
})
function refreshEntropy() {
entropy.value = generateEntropy()
}
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' })
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' })
</script> </script>