Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 976a89f

Browse files
Adrinlolagneum
authored andcommitted
fix(ui): entropy password validation
1 parent c8b1e82 commit 976a89f

File tree

5 files changed

+288
-10
lines changed

5 files changed

+288
-10
lines changed
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { CloneDto, formatCloneDto } from '@postgres.ai/shared/types/api/entities/clone'
1+
import {
2+
CloneDto,
3+
formatCloneDto,
4+
} from '@postgres.ai/shared/types/api/entities/clone'
25

36
import { request } from 'helpers/request'
47

@@ -8,16 +11,18 @@ type Request = {
811
}
912

1013
export const getClone = async (req: Request) => {
11-
const response = await request('/rpc/dblab_clone_status', {
14+
const response = (await request('/rpc/dblab_clone_status', {
1215
method: 'POST',
1316
body: JSON.stringify({
1417
instance_id: req.instanceId,
1518
clone_id: req.cloneId,
16-
})
17-
})
19+
}),
20+
}))
1821

1922
return {
20-
response: response.ok ? formatCloneDto(await response.json() as CloneDto) : null,
23+
response: response.ok
24+
? formatCloneDto((await response.json()) as CloneDto)
25+
: null,
2126
error: response.ok ? null : response,
2227
}
2328
}

‎ui/packages/shared/components/ErrorStub/index.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import clsx from 'clsx'
1111

1212
type Props = {
1313
title?: string
14-
message?: string
14+
message?:
15+
| string
16+
| {
17+
details: string
18+
}
1519
className?: string
1620
size?: 'big' | 'normal'
1721
}
@@ -71,7 +75,9 @@ export const ErrorStub = (props: Props) => {
7175
)}
7276
>
7377
<h2 className={classes.title}>{title}</h2>
74-
<p className={classes.message}>{message}</p>
78+
<p className={classes.message}>
79+
{typeof message === 'object' ? message.details : message}
80+
</p>
7581
</Paper>
7682
)
7783
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
const replaceChars = '!@$&*'
2+
const sepChars = '_-., '
3+
const otherSpecialChars = '"#%"()+/:;<=>?[\\]^{|}~'
4+
const lowerChars = 'abcdefghijklmnopqrstuvwxyz'
5+
const upperChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
6+
const digitsChars = '0123456789'
7+
export const MIN_ENTROPY = 60
8+
9+
function getBase(password: string): number {
10+
let uniqueChars: string[] = []
11+
for (const c of password) {
12+
if (!uniqueChars.includes(c)) {
13+
uniqueChars.push(c)
14+
}
15+
}
16+
let hasReplace = false
17+
let hasSep = false
18+
let hasOtherSpecial = false
19+
let hasLower = false
20+
let hasUpper = false
21+
let hasDigits = false
22+
let base = 0
23+
24+
for (let i = 0; i < uniqueChars.length; i++) {
25+
switch (true) {
26+
case replaceChars.includes(uniqueChars[i]):
27+
hasReplace = true
28+
break
29+
case sepChars.includes(uniqueChars[i]):
30+
hasSep = true
31+
break
32+
case otherSpecialChars.includes(uniqueChars[i]):
33+
hasOtherSpecial = true
34+
break
35+
case lowerChars.includes(uniqueChars[i]):
36+
hasLower = true
37+
break
38+
case upperChars.includes(uniqueChars[i]):
39+
hasUpper = true
40+
break
41+
case digitsChars.includes(uniqueChars[i]):
42+
hasDigits = true
43+
break
44+
default:
45+
base++
46+
break
47+
}
48+
}
49+
if (hasReplace) {
50+
base += replaceChars.length
51+
}
52+
if (hasSep) {
53+
base += sepChars.length
54+
}
55+
if (hasOtherSpecial) {
56+
base += otherSpecialChars.length
57+
}
58+
if (hasLower) {
59+
base += lowerChars.length
60+
}
61+
if (hasUpper) {
62+
base += upperChars.length
63+
}
64+
if (hasDigits) {
65+
base += digitsChars.length
66+
}
67+
return base
68+
}
69+
const seqNums = '0123456789'
70+
const seqKeyboard0 = 'qwertyuiop'
71+
const seqKeyboard1 = 'asdfghjkl'
72+
const seqKeyboard2 = 'zxcvbnm'
73+
const seqAlphabet = 'abcdefghijklmnopqrstuvwxyz'
74+
function removeMoreThanTwoFromSequence(s: string, seq: string): string {
75+
const seqRunes: string[] = Array.from(seq)
76+
let runes: string[] = Array.from(s)
77+
let matches = 0
78+
for (let i = 0; i < runes.length; i++) {
79+
for (let j = 0; j < seqRunes.length; j++) {
80+
if (i >= runes.length) {
81+
break
82+
}
83+
const r = runes[i]
84+
const r2 = seqRunes[j]
85+
if (r !== r2) {
86+
matches = 0
87+
continue
88+
}
89+
// found a match, advance the counter
90+
matches++
91+
if (matches > 2) {
92+
runes.splice(i, 1)
93+
} else {
94+
i++
95+
}
96+
}
97+
}
98+
return runes.join('')
99+
}
100+
function getReversedString(s: string): string {
101+
const rune: string[] = Array.from(s)
102+
const n = rune.length
103+
for (let i = 0; i < Math.floor(n / 2); i++) {
104+
;[rune[i], rune[n - 1 - i]] = [rune[n - 1 - i], rune[i]]
105+
}
106+
return rune.join('')
107+
}
108+
function removeMoreThanTwoRepeatingChars(s: string): string {
109+
let prevPrev: string = ''
110+
let prev: string = ''
111+
const runes: string[] = Array.from(s)
112+
for (let i = 0; i < runes.length; i++) {
113+
const r = runes[i]
114+
if (r === prev && r === prevPrev) {
115+
runes.splice(i, 1)
116+
i--
117+
}
118+
prevPrev = prev
119+
prev = r
120+
}
121+
return runes.join('')
122+
}
123+
function getLength(password: string): number {
124+
password = removeMoreThanTwoRepeatingChars(password)
125+
password = removeMoreThanTwoFromSequence(password, seqNums)
126+
password = removeMoreThanTwoFromSequence(password, seqKeyboard0)
127+
password = removeMoreThanTwoFromSequence(password, seqKeyboard1)
128+
password = removeMoreThanTwoFromSequence(password, seqKeyboard2)
129+
password = removeMoreThanTwoFromSequence(password, seqAlphabet)
130+
password = removeMoreThanTwoFromSequence(password, getReversedString(seqNums))
131+
password = removeMoreThanTwoFromSequence(
132+
password,
133+
getReversedString(seqKeyboard0),
134+
)
135+
password = removeMoreThanTwoFromSequence(
136+
password,
137+
getReversedString(seqKeyboard1),
138+
)
139+
password = removeMoreThanTwoFromSequence(
140+
password,
141+
getReversedString(seqKeyboard2),
142+
)
143+
password = removeMoreThanTwoFromSequence(
144+
password,
145+
getReversedString(seqAlphabet),
146+
)
147+
return password.length
148+
}
149+
export function getEntropy(password: string): number {
150+
return getEntropyInternal(password)
151+
}
152+
function getEntropyInternal(password: string): number {
153+
const base = getBase(password)
154+
const length = getLength(password)
155+
// calculate log2(base^length)
156+
return logPow(base, length, 2)
157+
}
158+
function logX(base: number, n: number): number {
159+
if (base == 0) {
160+
return 0
161+
} else {
162+
return Math.log2(n) / Math.log2(base)
163+
}
164+
}
165+
function logPow(expBase: number, pow: number, logBase: number): number {
166+
let total = 0
167+
for (let i = 0; i < pow; i++) {
168+
total += logX(logBase, expBase)
169+
}
170+
return total
171+
}
172+
173+
export function validatePassword(password: string, minEntropy: number): string {
174+
const entropy: number = getEntropy(password)
175+
if (entropy >= minEntropy) {
176+
return ''
177+
}
178+
179+
let hasReplace: boolean = false
180+
let hasSep: boolean = false
181+
let hasOtherSpecial: boolean = false
182+
let hasLower: boolean = false
183+
let hasUpper: boolean = false
184+
let hasDigits: boolean = false
185+
186+
for (const c of password) {
187+
switch (true) {
188+
case replaceChars.includes(c):
189+
hasReplace = true
190+
break
191+
case sepChars.includes(c):
192+
hasSep = true
193+
break
194+
case otherSpecialChars.includes(c):
195+
hasOtherSpecial = true
196+
break
197+
case lowerChars.includes(c):
198+
hasLower = true
199+
break
200+
case upperChars.includes(c):
201+
hasUpper = true
202+
break
203+
case digitsChars.includes(c):
204+
hasDigits = true
205+
break
206+
}
207+
}
208+
209+
const allMessages: string[] = []
210+
211+
if (!hasOtherSpecial || !hasSep || !hasReplace) {
212+
allMessages.push('including more special characters')
213+
}
214+
if (!hasLower) {
215+
allMessages.push('using lowercase letters')
216+
}
217+
if (!hasUpper) {
218+
allMessages.push('using uppercase letters')
219+
}
220+
if (!hasDigits) {
221+
allMessages.push('using numbers')
222+
}
223+
224+
if (allMessages.length > 0) {
225+
const errorMessage: string = `Weak password, try ${allMessages.join(
226+
', ',
227+
)} or using a longer password`
228+
return errorMessage
229+
}
230+
231+
return 'Weak password, try using a longer password'
232+
}

‎ui/packages/shared/pages/CreateClone/index.tsx‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import cn from 'classnames'
12
import { useEffect } from 'react'
23
import { useHistory } from 'react-router-dom'
34
import { observer } from 'mobx-react-lite'
@@ -15,6 +16,11 @@ import { compareSnapshotsDesc } from '@postgres.ai/shared/utils/snapshot'
1516
import { round } from '@postgres.ai/shared/utils/numbers'
1617
import { formatBytesIEC } from '@postgres.ai/shared/utils/units'
1718
import { SectionTitle } from '@postgres.ai/shared/components/SectionTitle'
19+
import {
20+
MIN_ENTROPY,
21+
getEntropy,
22+
validatePassword,
23+
} from '@postgres.ai/shared/helpers/getEntropy'
1824

1925
import { useCreatedStores, MainStoreApi } from './useCreatedStores'
2026
import { useForm, FormValues } from './useForm'
@@ -37,15 +43,26 @@ type Props = Host
3743
export const CreateClone = observer((props: Props) => {
3844
const history = useHistory()
3945
const stores = useCreatedStores(props.api)
46+
const cloneError = stores.main.cloneError
4047
const timer = useTimer()
4148

4249
// Form.
4350
const onSubmit = async (values: FormValues) => {
51+
if (!values.dbPassword || getEntropy(values.dbPassword) < MIN_ENTROPY) {
52+
formik.setFieldError(
53+
'dbPassword',
54+
validatePassword(values.dbPassword, MIN_ENTROPY),
55+
)
56+
return
57+
}
58+
4459
timer.start()
4560

4661
const isSuccess = await stores.main.createClone(values)
4762

48-
if (!isSuccess) {
63+
formik.setFieldError('dbPassword', '')
64+
65+
if (!isSuccess || cloneError) {
4966
timer.pause()
5067
timer.reset()
5168
}
@@ -196,10 +213,23 @@ export const CreateClone = observer((props: Props) => {
196213
label="Database password *"
197214
type="password"
198215
value={formik.values.dbPassword}
199-
onChange={(e) => formik.setFieldValue('dbPassword', e.target.value)}
216+
onChange={(e) => {
217+
formik.setFieldValue('dbPassword', e.target.value)
218+
if (formik.errors.dbPassword) {
219+
formik.setFieldError('dbPassword', '')
220+
}
221+
}}
200222
error={Boolean(formik.errors.dbPassword)}
201223
disabled={isCreatingClone}
202224
/>
225+
<p
226+
className={cn(
227+
formik.errors.dbPassword && styles.error,
228+
styles.remark,
229+
)}
230+
>
231+
{formik.errors.dbPassword}
232+
</p>
203233
</div>
204234

205235
<div className={styles.section}>
@@ -220,7 +250,8 @@ export const CreateClone = observer((props: Props) => {
220250
<span>Expected cloning time:</span>
221251
<strong>
222252
{round(
223-
stores.main.instance.state?.cloning.expectedCloningTime as number,
253+
stores.main.instance.state?.cloning
254+
.expectedCloningTime as number,
224255
2,
225256
)}{' '}
226257
s

‎ui/packages/shared/pages/CreateClone/styles.module.scss‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
font-size: 12px;
2727
}
2828

29+
.error {
30+
color: red;
31+
}
32+
2933
.snapshotTag {
3034
font-weight: 700;
3135
margin-left: 4px;

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /