playbook_ui 11.12.1.pre.alpha.passphrase1 → 11.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_passphrase/_passphrase.jsx +97 -56
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.html.erb +1 -145
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.jsx +3 -127
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.md +2 -11
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_common.jsx +8 -90
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.html.erb +2 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.jsx +20 -6
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.md +1 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_input_props.html.erb +2 -2
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_input_props.jsx +1 -1
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.html.erb +5 -318
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.jsx +48 -134
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.md +5 -11
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_strength_change.jsx +20 -96
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_strength_change.md +2 -6
- data/app/pb_kits/playbook/pb_passphrase/docs/example.yml +0 -4
- data/app/pb_kits/playbook/pb_passphrase/docs/index.js +0 -1
- data/app/pb_kits/playbook/pb_passphrase/passphrase.html.erb +1 -1
- data/app/pb_kits/playbook/pb_passphrase/passphrase.rb +9 -5
- data/app/pb_kits/playbook/pb_passphrase/passphrase.test.jsx +47 -0
- data/app/pb_kits/playbook/pb_passphrase/passwordStrength.js +55 -0
- data/app/pb_kits/playbook/pb_passphrase/useHaveIBeenPwned.js +52 -0
- data/app/pb_kits/playbook/pb_passphrase/useZxcvbn.js +58 -0
- data/lib/playbook/version.rb +2 -2
- metadata +8 -9
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_common.html.erb +0 -136
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_common.md +0 -5
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_confirmation.html.erb +0 -51
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_confirmation.jsx +0 -39
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_strength_change.html.erb +0 -123
@@ -1,157 +1,71 @@
|
|
1
1
|
import React, { useState } from 'react'
|
2
2
|
|
3
|
-
import
|
4
|
-
import
|
5
|
-
|
3
|
+
import Body from '../../pb_body/_body'
|
4
|
+
import Passphrase from '../../pb_passphrase/_passphrase'
|
5
|
+
import TextInput from '../../pb_text_input/_text_input'
|
6
6
|
|
7
7
|
const PassphraseMeterSettings = (props) => {
|
8
8
|
const [input, setInput] = useState('')
|
9
|
-
const [result, setResult] = useState({})
|
10
|
-
const [calculatedStrength, setCalculatedStrength] = useState(0)
|
11
|
-
|
12
|
-
const meterSettings = [
|
13
|
-
{
|
14
|
-
label: "Default settings"
|
15
|
-
},
|
16
|
-
{
|
17
|
-
minLength: 5,
|
18
|
-
label: "Min length = 5",
|
19
|
-
},
|
20
|
-
{
|
21
|
-
minLength: 30,
|
22
|
-
label: "Min length = 30",
|
23
|
-
},
|
24
|
-
{
|
25
|
-
label: "Average threshold = 1",
|
26
|
-
averageThreshold: 1,
|
27
|
-
},
|
28
|
-
{
|
29
|
-
label: "Strong Threshold = 4",
|
30
|
-
strongThreshold: 4,
|
31
|
-
},
|
32
|
-
]
|
33
|
-
|
34
|
-
const handleStrengthCalculation = (settings) => {
|
35
|
-
const {
|
36
|
-
passphrase = "",
|
37
|
-
common = false,
|
38
|
-
isPwned = false,
|
39
|
-
averageThreshold = 2,
|
40
|
-
minLength = 12,
|
41
|
-
strongThreshold = 3,
|
42
|
-
} = settings
|
43
|
-
|
44
|
-
const resultByScore = {
|
45
|
-
0: {
|
46
|
-
variant: 'negative',
|
47
|
-
label: '',
|
48
|
-
percent: 0,
|
49
|
-
},
|
50
|
-
1: {
|
51
|
-
variant: 'negative',
|
52
|
-
label: 'This passphrase is too common',
|
53
|
-
percent: 25,
|
54
|
-
},
|
55
|
-
2: {
|
56
|
-
variant: 'negative',
|
57
|
-
label: 'Too weak',
|
58
|
-
percent: 25,
|
59
|
-
},
|
60
|
-
3: {
|
61
|
-
variant: 'warning',
|
62
|
-
label: 'Almost there, keep going!',
|
63
|
-
percent: 50,
|
64
|
-
},
|
65
|
-
4: {
|
66
|
-
variant: 'positive',
|
67
|
-
label: 'Success! Strong passphrase',
|
68
|
-
percent: 100,
|
69
|
-
}
|
70
|
-
}
|
71
|
-
|
72
|
-
const { score } = zxcvbn(passphrase);
|
73
|
-
|
74
|
-
const noPassphrase = passphrase.length <= 0
|
75
|
-
const commonPassphrase = common || isPwned
|
76
|
-
const weakPassphrase = passphrase.length < minLength || score < averageThreshold
|
77
|
-
const averagePassphrase = score < strongThreshold
|
78
|
-
const strongPassphrase = score >= strongThreshold
|
79
|
-
|
80
|
-
if (noPassphrase) {
|
81
|
-
return {...resultByScore[0], score}
|
82
|
-
} else if (commonPassphrase) {
|
83
|
-
return {...resultByScore[1], score}
|
84
|
-
} else if (weakPassphrase) {
|
85
|
-
return {...resultByScore[2], score}
|
86
|
-
} else if (averagePassphrase){
|
87
|
-
return {...resultByScore[3], score}
|
88
|
-
} else if (strongPassphrase) {
|
89
|
-
return {...resultByScore[4], score}
|
90
|
-
}
|
91
|
-
}
|
92
9
|
|
93
|
-
|
94
|
-
const handleChange = (e) => {
|
95
|
-
const passphrase = e.target.value;
|
96
|
-
|
97
|
-
setInput(passphrase)
|
98
|
-
|
99
|
-
const calculated = []
|
100
|
-
|
101
|
-
meterSettings.forEach((setting, index) => {
|
102
|
-
const results = handleStrengthCalculation({passphrase, ...setting})
|
103
|
-
if (index == 0) setCalculatedStrength(results.score)
|
104
|
-
calculated.push(results)
|
105
|
-
})
|
106
|
-
|
107
|
-
setResult(calculated)
|
108
|
-
}
|
10
|
+
const handleChange = (e) => setInput(e.target.value)
|
109
11
|
|
12
|
+
const [strength, setStrength] = useState(0)
|
13
|
+
const handleStrengthChange = (str) => setStrength(str)
|
110
14
|
return (
|
111
15
|
<>
|
112
16
|
<div>
|
113
17
|
<Body>
|
114
|
-
{
|
115
|
-
"These examples will all share the same input value. Type in any of the inputs to see how the strength meter changes in response to different settings."
|
116
|
-
}
|
18
|
+
{'These examples will all share the same input value. Type in any of the inputs to see how the strength meter changes in response to different settings.'}
|
117
19
|
</Body>
|
20
|
+
<br />
|
21
|
+
<TextInput
|
22
|
+
disabled
|
23
|
+
label="Calculated Strength"
|
24
|
+
readOnly
|
25
|
+
value={strength}
|
26
|
+
/>
|
27
|
+
|
118
28
|
<Passphrase
|
119
|
-
label=
|
29
|
+
label="Default settings"
|
120
30
|
onChange={handleChange}
|
31
|
+
onStrengthChange={handleStrengthChange}
|
121
32
|
value={input}
|
122
33
|
{...props}
|
123
34
|
/>
|
124
|
-
|
125
|
-
|
126
|
-
label="
|
127
|
-
|
128
|
-
|
35
|
+
|
36
|
+
<Passphrase
|
37
|
+
label="Min length = 5"
|
38
|
+
minLength={5}
|
39
|
+
onChange={handleChange}
|
40
|
+
value={input}
|
41
|
+
{...props}
|
42
|
+
/>
|
43
|
+
<Passphrase
|
44
|
+
label="Min length = 30"
|
45
|
+
minLength={30}
|
46
|
+
onChange={handleChange}
|
47
|
+
value={input}
|
48
|
+
{...props}
|
49
|
+
/>
|
50
|
+
|
51
|
+
<Passphrase
|
52
|
+
averageThreshold={1}
|
53
|
+
label="Average threshold = 1"
|
54
|
+
onChange={handleChange}
|
55
|
+
value={input}
|
56
|
+
{...props}
|
57
|
+
/>
|
58
|
+
|
59
|
+
<Passphrase
|
60
|
+
label="Strong Threshold = 4"
|
61
|
+
onChange={handleChange}
|
62
|
+
strongThreshold={4}
|
63
|
+
value={input}
|
64
|
+
{...props}
|
129
65
|
/>
|
130
|
-
{meterSettings.map((settings, index) => (
|
131
|
-
<div key={index}>
|
132
|
-
<Passphrase
|
133
|
-
label={settings.label}
|
134
|
-
onChange={handleChange}
|
135
|
-
value={input}
|
136
|
-
{...props}
|
137
|
-
/>
|
138
|
-
{input.length > 0 && (
|
139
|
-
<>
|
140
|
-
<ProgressSimple
|
141
|
-
percent={result[index].percent}
|
142
|
-
variant={result[index].variant}
|
143
|
-
/>
|
144
|
-
<Caption size='xs'
|
145
|
-
text={result[index].label}
|
146
|
-
/>
|
147
|
-
</>
|
148
|
-
)}
|
149
|
-
</div>
|
150
|
-
))}
|
151
|
-
|
152
66
|
</div>
|
153
67
|
</>
|
154
|
-
)
|
68
|
+
)
|
155
69
|
}
|
156
70
|
|
157
71
|
export default PassphraseMeterSettings
|
@@ -1,15 +1,9 @@
|
|
1
|
-
|
1
|
+
By default, the minimum length is 12 and the strength meter will show a strength of 1 if not met. Notice the bar won't change from red until the minimum is met
|
2
|
+
Use the `minLength` prop to adjust.
|
2
3
|
|
3
|
-
The `meterSettings` array contains different settings for each rendered input. The `handleStrengthCalculation` handles the strength calculation using those settings, showing different results for the same `passphrase` input.
|
4
4
|
|
5
|
-
|
5
|
+
The meter also response to `averageThreshold` and `strongTreshold` props. `averageThresold` defaults to 2, and `strongThreshold` defaults to 3.
|
6
|
+
This means that the bar will turn yellow when the strength of the passphrase is calculated to be 2 on a 0-4 scale, and green when 3.
|
6
7
|
|
7
8
|
Adjust these props to tune the sensitivity of the bar.
|
8
|
-
|
9
|
-
Note: minimum length trumps strength and will set the bar to a red color, despite whatever strength is calculated.
|
10
|
-
|
11
|
-
<div class="pb_pill_kit_warning"><div class="pb_title_kit_size_4 pb_pill_text">Disclaimer</div></div>
|
12
|
-
|
13
|
-
This example depends on the `zxcvbn` library.
|
14
|
-
|
15
|
-
You can use any library to achieve the same result, this example only intends to show how to add more features to the `Passphrase` kit.
|
9
|
+
Note: minimum length trumps strength and will set the bar to a red color, despite whatever strength is calculated.
|
@@ -1,110 +1,34 @@
|
|
1
1
|
import React, { useState } from 'react'
|
2
|
-
import { useEffect } from 'react'
|
3
2
|
|
4
|
-
import
|
5
|
-
|
3
|
+
import Passphrase from '../_passphrase'
|
4
|
+
|
5
|
+
import TextInput from '../../pb_text_input/_text_input'
|
6
6
|
|
7
7
|
const PassphraseStrengthChange = (props) => {
|
8
8
|
const [input, setInput] = useState('')
|
9
|
-
const [checkStrength, setCheckStrength] = useState({
|
10
|
-
label: '',
|
11
|
-
percent: 0,
|
12
|
-
score: 0,
|
13
|
-
variant: ''
|
14
|
-
})
|
15
9
|
|
16
10
|
const handleChange = (e) => setInput(e.target.value)
|
17
11
|
|
18
|
-
const
|
19
|
-
|
20
|
-
passphrase = "",
|
21
|
-
common = false,
|
22
|
-
isPwned = false,
|
23
|
-
averageThreshold = 2,
|
24
|
-
minLength = 12,
|
25
|
-
strongThreshold = 3,
|
26
|
-
} = settings
|
27
|
-
|
28
|
-
const resultByScore = {
|
29
|
-
0: {
|
30
|
-
variant: 'negative',
|
31
|
-
label: '',
|
32
|
-
percent: 0,
|
33
|
-
},
|
34
|
-
1: {
|
35
|
-
variant: 'negative',
|
36
|
-
label: 'This passphrase is too common',
|
37
|
-
percent: 25,
|
38
|
-
},
|
39
|
-
2: {
|
40
|
-
variant: 'negative',
|
41
|
-
label: 'Too weak',
|
42
|
-
percent: 25,
|
43
|
-
},
|
44
|
-
3: {
|
45
|
-
variant: 'warning',
|
46
|
-
label: 'Almost there, keep going!',
|
47
|
-
percent: 50,
|
48
|
-
},
|
49
|
-
4: {
|
50
|
-
variant: 'positive',
|
51
|
-
label: 'Success! Strong passphrase',
|
52
|
-
percent: 100,
|
53
|
-
}
|
54
|
-
}
|
55
|
-
|
56
|
-
const { score } = zxcvbn(passphrase);
|
57
|
-
|
58
|
-
const noPassphrase = passphrase.length <= 0
|
59
|
-
const commonPassphrase = common || isPwned
|
60
|
-
const weakPassphrase = passphrase.length < minLength || score < averageThreshold
|
61
|
-
const averagePassphrase = score < strongThreshold
|
62
|
-
const strongPassphrase = score >= strongThreshold
|
63
|
-
|
64
|
-
if (noPassphrase) {
|
65
|
-
return {...resultByScore[0], score}
|
66
|
-
} else if (commonPassphrase) {
|
67
|
-
return {...resultByScore[1], score}
|
68
|
-
} else if (weakPassphrase) {
|
69
|
-
return {...resultByScore[2], score}
|
70
|
-
} else if (averagePassphrase){
|
71
|
-
return {...resultByScore[3], score}
|
72
|
-
} else if (strongPassphrase) {
|
73
|
-
return {...resultByScore[4], score}
|
74
|
-
}
|
75
|
-
}
|
76
|
-
|
77
|
-
useEffect(() => {
|
78
|
-
const result = handleStrengthCalculation({ passphrase: input })
|
79
|
-
setCheckStrength({...result})
|
80
|
-
},[input])
|
12
|
+
const [strength, setStrength] = useState(0)
|
13
|
+
const handleStrengthChange = (str) => setStrength(str)
|
81
14
|
|
82
15
|
return (
|
83
16
|
<>
|
84
|
-
<
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
</>
|
100
|
-
)}
|
101
|
-
<TextInput
|
102
|
-
disabled
|
103
|
-
label="Passphrase Strength"
|
104
|
-
marginTop="xl"
|
105
|
-
readOnly
|
106
|
-
value={checkStrength.score}
|
107
|
-
/>
|
17
|
+
<div>
|
18
|
+
<Passphrase
|
19
|
+
label="Passphrase"
|
20
|
+
onChange={handleChange}
|
21
|
+
onStrengthChange={handleStrengthChange}
|
22
|
+
value={input}
|
23
|
+
{...props}
|
24
|
+
/>
|
25
|
+
<TextInput
|
26
|
+
disabled
|
27
|
+
label="Passphrase Strength"
|
28
|
+
readOnly
|
29
|
+
value={strength}
|
30
|
+
/>
|
31
|
+
</div>
|
108
32
|
</>
|
109
33
|
)
|
110
34
|
}
|
@@ -1,7 +1,3 @@
|
|
1
|
-
|
1
|
+
As the strength of the entered passphrase changes, the optional `onStrengthChange` callback is called with the new strength value. This exposes the calculated strength.
|
2
2
|
|
3
|
-
<
|
4
|
-
|
5
|
-
This example depends on the `zxcvbn` library.
|
6
|
-
|
7
|
-
You can use any library to achieve the same result, this example only intends to show how to add more features to the `Passphrase` kit.
|
3
|
+
Strength is calculated on a 0-4 scale by the <a href="https://github.com/dropbox/zxcvbn" target="_blank"> Zxcvbn package</a>
|
@@ -2,17 +2,13 @@ examples:
|
|
2
2
|
|
3
3
|
rails:
|
4
4
|
- passphrase_default: Default
|
5
|
-
- passphrase_confirmation: Confirmation
|
6
5
|
- passphrase_meter_settings: Meter Settings
|
7
6
|
- passphrase_input_props: Input Props
|
8
7
|
- passphrase_tips: Tips
|
9
|
-
- passphrase_strength_change: Strength Change
|
10
|
-
- passphrase_common: Common Passphrases
|
11
8
|
- passphrase_breached: Breached Passphrases
|
12
9
|
|
13
10
|
react:
|
14
11
|
- passphrase_default: Default
|
15
|
-
- passphrase_confirmation: Confirmation
|
16
12
|
- passphrase_meter_settings: Meter Settings
|
17
13
|
- passphrase_input_props: Input Props
|
18
14
|
- passphrase_tips: Tips
|
@@ -1,5 +1,4 @@
|
|
1
1
|
export { default as PassphraseDefault } from './_passphrase_default.jsx'
|
2
|
-
export { default as PassphraseConfirmation } from './_passphrase_confirmation.jsx'
|
3
2
|
export { default as PassphraseMeterSettings } from './_passphrase_meter_settings'
|
4
3
|
export { default as PassphraseInputProps } from './_passphrase_input_props'
|
5
4
|
export { default as PassphraseTips } from './_passphrase_tips'
|
@@ -1 +1 @@
|
|
1
|
-
<%= react_component('Passphrase', object.passphrase_options
|
1
|
+
<%= react_component('Passphrase', object.passphrase_options) %>
|
@@ -3,14 +3,15 @@
|
|
3
3
|
module Playbook
|
4
4
|
module PbPassphrase
|
5
5
|
class Passphrase < Playbook::KitBase
|
6
|
+
prop :average_threshold
|
7
|
+
prop :check_pwned
|
6
8
|
prop :confirmation, type: Playbook::Props::Boolean, default: false
|
7
9
|
prop :input_props, type: Playbook::Props::Hash, default: {}
|
8
10
|
prop :label
|
9
|
-
prop :
|
10
|
-
|
11
|
-
|
11
|
+
prop :min_length
|
12
|
+
prop :show_tips_below
|
13
|
+
prop :strong_threshold
|
12
14
|
prop :tips, type: Playbook::Props::Array, default: []
|
13
|
-
prop :value, type: Playbook::Props::String
|
14
15
|
|
15
16
|
def classname
|
16
17
|
generate_classname("pb_passphrase")
|
@@ -18,15 +19,18 @@ module Playbook
|
|
18
19
|
|
19
20
|
def passphrase_options
|
20
21
|
{
|
22
|
+
checkPwned: check_pwned,
|
21
23
|
dark: dark,
|
22
24
|
id: id,
|
25
|
+
averageThreshold: average_threshold,
|
23
26
|
confirmation: confirmation,
|
24
27
|
inputProps: input_props,
|
25
28
|
label: label,
|
29
|
+
minLength: min_length,
|
26
30
|
showTipsBelow: show_tips_below,
|
31
|
+
strongThreshold: strong_threshold,
|
27
32
|
tips: tips,
|
28
33
|
uncontrolled: true,
|
29
|
-
value: value,
|
30
34
|
}.compact
|
31
35
|
end
|
32
36
|
end
|
@@ -64,6 +64,41 @@ test('passes input props to input element', () => {
|
|
64
64
|
expect(input).toBeDisabled()
|
65
65
|
})
|
66
66
|
|
67
|
+
test('progress bar is invisible when value is empty', () => {
|
68
|
+
render(
|
69
|
+
<Passphrase
|
70
|
+
data={{ testid: testId }}
|
71
|
+
/>
|
72
|
+
)
|
73
|
+
|
74
|
+
const kit = screen.getByTestId(testId)
|
75
|
+
expect(kit.querySelector('[class^=pb_progress_simple_wrapper]')).toHaveClass('progress-empty-input')
|
76
|
+
})
|
77
|
+
|
78
|
+
test('progress bar is visible when value is not empty', () => {
|
79
|
+
render(
|
80
|
+
<Passphrase
|
81
|
+
data={{ testid: testId }}
|
82
|
+
value="test_password_input"
|
83
|
+
/>
|
84
|
+
)
|
85
|
+
|
86
|
+
const kit = screen.getByTestId(testId)
|
87
|
+
expect(kit.querySelector('[class^=pb_progress_simple_wrapper]')).not.toHaveClass('progress-empty-input')
|
88
|
+
})
|
89
|
+
|
90
|
+
test('no progress bar is show when confirmation is true', () => {
|
91
|
+
render(
|
92
|
+
<Passphrase
|
93
|
+
confirmation
|
94
|
+
data={{ testid: testId }}
|
95
|
+
/>
|
96
|
+
)
|
97
|
+
|
98
|
+
const kit = screen.getByTestId(testId)
|
99
|
+
expect(kit.querySelector('[class^=pb_progress_simple_wrapper]')).toBeNull()
|
100
|
+
})
|
101
|
+
|
67
102
|
test('popover target shows when tips are given', () => {
|
68
103
|
render(
|
69
104
|
<Passphrase
|
@@ -86,3 +121,15 @@ test('popover target does not show when tips are not given', () => {
|
|
86
121
|
const kit = screen.getByTestId(testId)
|
87
122
|
expect(kit.querySelector('[class^=pb_popover_reference_wrapper]')).toBeNull()
|
88
123
|
})
|
124
|
+
|
125
|
+
test('data-strength attribute exposes strength of password', () => {
|
126
|
+
render(
|
127
|
+
<Passphrase
|
128
|
+
data={{ testid: testId }}
|
129
|
+
value="correct horse battery staple"
|
130
|
+
/>
|
131
|
+
)
|
132
|
+
|
133
|
+
const kit = screen.getByTestId(testId)
|
134
|
+
expect(parseInt(kit.getAttribute('data-strength'))).toBeGreaterThan(0)
|
135
|
+
})
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import zxcvbn from 'zxcvbn'
|
2
|
+
|
3
|
+
export const zxcvbnPasswordScore = (options) => {
|
4
|
+
const {
|
5
|
+
calculate = zxcvbn,
|
6
|
+
averageThreshold = 2,
|
7
|
+
strongThreshold = 3,
|
8
|
+
minLength = 12,
|
9
|
+
} = options
|
10
|
+
|
11
|
+
return {
|
12
|
+
minLength,
|
13
|
+
averageThreshold,
|
14
|
+
strongThreshold,
|
15
|
+
test: function (password = '', common = false) {
|
16
|
+
const feedbackValues = (str) => {
|
17
|
+
let percent, variant, text
|
18
|
+
|
19
|
+
if (password.length <= 0) {
|
20
|
+
percent = '0'
|
21
|
+
variant = 'negative'
|
22
|
+
text = '\u00A0' //nbsp to keep form from jumping when typing beings
|
23
|
+
} else if (common) {
|
24
|
+
percent = '25'
|
25
|
+
variant = 'negative'
|
26
|
+
text = 'This passphrase is too common'
|
27
|
+
} else if (password.length < this.minLength || str < this.averageThreshold) {
|
28
|
+
percent = '25'
|
29
|
+
variant = 'negative'
|
30
|
+
text = 'Too weak'
|
31
|
+
} else if (str < this.strongThreshold){
|
32
|
+
percent = '50'
|
33
|
+
variant = 'warning'
|
34
|
+
text = 'Almost there, keep going!'
|
35
|
+
} else if (str >= this.strongThreshold) {
|
36
|
+
percent = '100'
|
37
|
+
variant = 'positive'
|
38
|
+
text = 'Success! Strong passphrase'
|
39
|
+
}
|
40
|
+
return { percent, variant, text }
|
41
|
+
}
|
42
|
+
|
43
|
+
const result = calculate(password)
|
44
|
+
|
45
|
+
return (
|
46
|
+
{
|
47
|
+
suggestions: result.feedback.suggestions,
|
48
|
+
warning: result.feedback.warning,
|
49
|
+
strength: result.score,
|
50
|
+
...feedbackValues(result.score),
|
51
|
+
}
|
52
|
+
)
|
53
|
+
},
|
54
|
+
}
|
55
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
import { useEffect, useState } from 'react'
|
3
|
+
|
4
|
+
const checkHaveIBeenPwned = async function (passphrase) {
|
5
|
+
const buffer = new TextEncoder('utf-8').encode(passphrase)
|
6
|
+
const digest = await crypto.subtle.digest('SHA-1', buffer)
|
7
|
+
const hashArray = Array.from(new Uint8Array(digest))
|
8
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
|
9
|
+
|
10
|
+
const firstFive = hashHex.slice(0, 5)
|
11
|
+
const endOfHash = hashHex.slice(5)
|
12
|
+
|
13
|
+
const resp = await fetch(`https://api.pwnedpasswords.com/range/${firstFive}`)
|
14
|
+
const text = await resp.text()
|
15
|
+
|
16
|
+
const match = text.split('\n').some((line) => {
|
17
|
+
//Each line is <sha-1-hash-suffix>:<count of incidents>
|
18
|
+
return line.split(':')[0] === endOfHash.toUpperCase()
|
19
|
+
})
|
20
|
+
return match
|
21
|
+
}
|
22
|
+
|
23
|
+
/**
|
24
|
+
* If the input hasn't changed in <delay> ms,
|
25
|
+
* hit the haveibeenpwned api and check if the given passphrase is compromised
|
26
|
+
*/
|
27
|
+
export default function useHaveIBeenPwned(passphrase, minLength, delay = 400) {
|
28
|
+
const [isPwned, setIsPwned] = useState(false)
|
29
|
+
|
30
|
+
useEffect(
|
31
|
+
() => {
|
32
|
+
// only check the API for passphrases above the minimum size
|
33
|
+
if (passphrase.length < minLength) {
|
34
|
+
setIsPwned(false)
|
35
|
+
return
|
36
|
+
}
|
37
|
+
|
38
|
+
const handler = setTimeout(() => {
|
39
|
+
checkHaveIBeenPwned(passphrase)
|
40
|
+
.then((pwned) => setIsPwned(pwned))
|
41
|
+
.catch(() => setIsPwned(false))
|
42
|
+
}, delay)
|
43
|
+
|
44
|
+
return () => {
|
45
|
+
clearTimeout(handler)
|
46
|
+
}
|
47
|
+
},
|
48
|
+
[passphrase, minLength, delay]
|
49
|
+
)
|
50
|
+
|
51
|
+
return isPwned
|
52
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
2
|
+
import zxcvbn from 'zxcvbn'
|
3
|
+
|
4
|
+
export default function useZxcvbn(options) {
|
5
|
+
const { passphrase = '', common, isPwned, confirmation, averageThreshold, minLength, strongThreshold } = options
|
6
|
+
const calculator = useMemo(
|
7
|
+
() => confirmation ? () => ({ score: 0 }) : zxcvbn,
|
8
|
+
[confirmation]
|
9
|
+
)
|
10
|
+
|
11
|
+
const [percent, setPercent] = useState('0')
|
12
|
+
const [variant, setVariant] = useState('negative')
|
13
|
+
const [text, setText] = useState('\u00A0') //nbsp to keep height constant
|
14
|
+
const [result, setResult] = useState({})
|
15
|
+
|
16
|
+
useEffect(() => {
|
17
|
+
if (confirmation) return
|
18
|
+
const newResult = calculator(passphrase)
|
19
|
+
setResult(newResult)
|
20
|
+
const str = newResult.score
|
21
|
+
|
22
|
+
const noPassphrase = passphrase.length <= 0
|
23
|
+
const commonPassphrase = common || isPwned
|
24
|
+
const weakPassphrase = passphrase.length < minLength || str < averageThreshold
|
25
|
+
const averagePassphrase = str < strongThreshold
|
26
|
+
const strongPassphrase = str >= strongThreshold
|
27
|
+
|
28
|
+
if (noPassphrase) {
|
29
|
+
setPercent('0')
|
30
|
+
setVariant('negative')
|
31
|
+
setText('\u00A0') //nbsp to keep height constant
|
32
|
+
} else if (commonPassphrase) {
|
33
|
+
setPercent('25')
|
34
|
+
setVariant('negative')
|
35
|
+
setText('This passphrase is too common')
|
36
|
+
} else if (weakPassphrase) {
|
37
|
+
setPercent('25')
|
38
|
+
setVariant('negative')
|
39
|
+
setText('Too weak')
|
40
|
+
} else if (averagePassphrase){
|
41
|
+
setPercent('50')
|
42
|
+
setVariant('warning')
|
43
|
+
setText('Almost there, keep going!')
|
44
|
+
} else if (strongPassphrase) {
|
45
|
+
setPercent('100')
|
46
|
+
setVariant('positive')
|
47
|
+
setText('Success! Strong passphrase')
|
48
|
+
}
|
49
|
+
}, [passphrase, common, isPwned, averageThreshold, minLength, strongThreshold]
|
50
|
+
)
|
51
|
+
|
52
|
+
return {
|
53
|
+
strength: common || isPwned ? 0 : result.score,
|
54
|
+
percent,
|
55
|
+
variant,
|
56
|
+
text,
|
57
|
+
}
|
58
|
+
}
|
data/lib/playbook/version.rb
CHANGED