Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manual entropy #21

Merged
merged 2 commits into from
Nov 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions components/EntropyInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<template>
<div>
<b-field label="Entropy Generation">
<b-field>
<p class="control">
<b-button size="is-small" type="is-info" outlined @click="generateRandomEntropy">
Generate Random
</b-button>
<b-button size="is-small" :type="isGeneratingEntropy ? 'is-primary' : 'is-info'" outlined @click="generateEntropy">
{{ isGeneratingEntropy ? 'Stop Generating' : 'Generate Manually' }}
</b-button>
<b-button size="is-small" type="is-info" outlined @click="clearEntropy">
Reset
</b-button>
<b-button
v-if="entropy"
size="is-small"
icon-left="chevron-down"
icon-right="alert-octagon"
type="is-danger"
outlined
@click="toggleShowEntropyArray"
>
Show entropy
</b-button>
</p>
</b-field>
</b-field>
<b-field>
<b-progress
v-if="isGeneratingEntropy"
size="is-small"
type="is-primary"
:show-value="true"
:max="requiredPoints"
:value="pointsGenerated"
>
{{ pointsGenerated }} / {{ requiredPoints }}
</b-progress>
</b-field>
<b-field v-if="showEntropyArray">
<b-taglist attached>
<b-tag type="is-dark">
Entropy
</b-tag>
<b-tag type="is-danger">
{{ entropy }}
</b-tag>
</b-taglist>
</b-field>
</div>
</template>

<script>
import { uint8ArrayCoordinateRandomize, wordsToUint8Array, uint8ArrayToHash } from '~/helpers/entropyUtils'

export default {
name: 'EntropyInput',
components: {
},
props: {
words: String,
entropy: Uint8Array
},
data () {
return {
isGeneratingEntropy: false,
entropyGenerationProgress: 0,
requiredPoints: 200,
lastX: 0,
lastY: 0,
lastEntropyTick: null,
showEntropyArray: false,
pointsGenerated: 0
}
},
watch: {
words () {
this.generateRandomEntropy()
}
},
methods: {
toggleShowEntropyArray () {
this.showEntropyArray = !this.showEntropyArray
},
clearEntropy () {
this.$emit('clearEntropy')
this.lastEntropyTick = null
this.entropyGenerationProgress = 0
},
generateRandomEntropy () {
const initArray = wordsToUint8Array(Number(this.words))
const entropyArray = window.crypto.getRandomValues(initArray)
this.$emit('updateEntropy', entropyArray)
},
generateEntropy (event) {
this.isGeneratingEntropy = !this.isGeneratingEntropy
const initArray = wordsToUint8Array(Number(this.words))
const entropyArray = this.entropy || window.crypto.getRandomValues(initArray)
this.pointsGenerated = 0
this.$emit('updateEntropy', entropyArray)
if (this.isGeneratingEntropy) {
this.entropyGenerationProgress = 0
window.addEventListener('mousemove', this.addEntropy)
} else {
window.removeEventListener('mousemove', this.addEntropy)
}
},
addEntropy (event) {
if (this.pointsGenerated >= this.requiredPoints) {
this.isGeneratingEntropy = false
this.lastEntropyTick = null
window.removeEventListener('mousemove', this.addEntropy)
}

const ts = new Date().getTime()

if (!this.lastEntropyTick) {
this.lastEntropyTick = ts
}

if (ts - this.lastEntropyTick > 100) {
const x = event.clientX
const y = event.clientY
if (x !== this.lastX && y !== this.lastY) {
this.lastX = x
this.lastY = y
const entropyArray = uint8ArrayCoordinateRandomize(this.entropy, x, y)
this.$emit('updateEntropy', entropyArray)
this.lastEntropyTick = ts
this.pointsGenerated += 1
}
}
}
}
}

</script>

<style>
</style>
55 changes: 50 additions & 5 deletions components/Generator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,26 @@
<b-button type="is-primary" outlined @click="generateMnemonic">
Generate
</b-button>
<b-field>
<b-button
type="is-text"
icon-left="chevron-down"
aria-controls="advancedEntropyGeneration"
@click="showEntropyInput = !showEntropyInput"
>
Advanced
</b-button>
</b-field>
</b-field>
</b-field>
</b-field>
<b-field>
<b-collapse :open="showEntropyInput" aria-id="advancedEntropyGeneration">
<div>
<EntropyInput :words="words" :entropy="entropy" @updateEntropy="updateEntropy" @clearEntropy="clearEntropy" />
</div>
</b-collapse>
</b-field>

<b-tabs size="is-small" class="spacer-top-md" expanded>
<b-tab-item>
Expand Down Expand Up @@ -202,28 +219,32 @@
<script>

import NodeInfo from '~/components/NodeInfo'
import { shortenMnemonic, shortMnemonicToOriginal, generateMnemonic, validateMnemonic, mnemonicToSeed, mnemonicToEntropy } from '~/helpers/bip39utils'
import EntropyInput from '~/components/EntropyInput'
import { shortenMnemonic, shortMnemonicToOriginal, generateMnemonic, generateMnemonicFromEntropy, validateMnemonic, mnemonicToSeed, mnemonicToEntropy } from '~/helpers/bip39utils'
import { getFormattedShares, shareGroupName } from '~/helpers/slip39utils'
import { copyInputToClipboard } from '~/helpers/browserUtils'

export default {
name: 'Generator',
components: {
NodeInfo
NodeInfo,
EntropyInput
},
data () {
return {
shortenMnemonic: false,
language: 'english',
words: 12,
words: "12",
mnemonic: null,
shortMnemonic: null,
recoveredSecret: null,
allShares: null,
passphrase: '',
masterThreshold: 1,
thresholds: [3],
shareGroups: [5]
shareGroups: [5],
showEntropyInput: false,
entropy: null
}
},
computed: {
Expand Down Expand Up @@ -257,6 +278,25 @@ export default {
}
},
methods: {
updateEntropy (value) {
this.entropy = value
if (this.entropy) {
this.generateMnemonic()
}
},
clearEntropy () {
this.entropy = null
this.clearMnemonic()
},
clearMnemonic () {
if (this.mnemonic) {
this.mnemonic = null
}

if (this.shortMnemonic) {
this.shortMnemonic = null
}
},
addGroup () {
if (this.thresholds.length < 6) {
this.thresholds.push(3)
Expand All @@ -276,7 +316,12 @@ export default {
this.allShares = getFormattedShares(masterSecret, passphrase, this.masterThreshold, groups)
},
generateMnemonic () {
this.mnemonic = generateMnemonic(this.language, this.words)
if (this.entropy) {
// User generated entropy manually
this.mnemonic = generateMnemonicFromEntropy(this.language, this.entropy)
} else {
this.mnemonic = generateMnemonic(this.language, Number(this.words))
}
this.shortMnemonic = shortenMnemonic(this.mnemonic)
},
wordCount (str) {
Expand Down
4 changes: 3 additions & 1 deletion components/Restore.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export default {
},
methods: {
focusAddShareInput () {
this.$refs.addinput.focus()
if (this.$refs.addinput) {
this.$refs.addinput.focus()
}
},
addShare () {
const trimmedshare = this.newShare.trim()
Expand Down
5 changes: 5 additions & 0 deletions helpers/bip39utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export const generateMnemonic = (language, wordCount) => {
return bip39.generateMnemonic(strength)
}

export const generateMnemonicFromEntropy = (language, entropy) => {
bip39.setDefaultWordlist(language)
return bip39.entropyToMnemonic(entropy)
}

export const validateMnemonic = (mnemonic) => {
return bip39.validateMnemonic(mnemonic)
}
Expand Down
44 changes: 44 additions & 0 deletions helpers/entropyUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const wordsToUint8Array = (words) => {
switch (Number(words)) {
case 12:
return new Uint8Array(16)
case 18:
return new Uint8Array(24)
case 24:
return new Uint8Array(32)
}

throw new Error(`Invalid word count length specified. Got ${words} but expected 12, 18 or 24`)
}

export const coordinateToRandomUInt8 = (x, y) => {
const xMax = Math.floor(x) % 255
const yMax = Math.floor(y) % 255
const xRand = getRandomInt(xMax)
const yRand = getRandomInt(yMax)
const randUnit8 = Math.floor((xRand + yRand) / 2)
return randUnit8
}

export const uint8ArrayCoordinateRandomize = (array, x, y) => {
const copy = new Uint8Array(array)
const randomIndex = getRandomInt(array.length)
const randomUint8 = coordinateToRandomUInt8(x, y)
copy[randomIndex] = randomUint8
return copy
}

export const getRandomInt = (max) => {
return Math.floor(secureMathRandom() * Math.floor(max))
}

export const secureMathRandom = () => {
// Divide a random UInt32 by the maximum value (2^32 -1) to get a result between 0 and 1
return window.crypto.getRandomValues(new Uint32Array(1))[0] / 4294967295
}

// export const uint8ArrayToHash = async (message) => {
// const hashBuffer = await window.crypto.subtle.digest('SHA-256', message) // hash the message
// const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
// return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
// }
4 changes: 4 additions & 0 deletions pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
<div class="card-content">
<b-tabs position="is-left" expanded @input="restoreSelected">
<b-tab-item class="spacer-top-md" icon="pencil" label="Generate">
<keep-alive>
<Generator />
</keep-alive>
</b-tab-item>
<b-tab-item icon="backup-restore" label="Restore">
<keep-alive>
<Restore ref="restore" />
</keep-alive>
</b-tab-item>
</b-tabs>
</div>
Expand Down