Compare commits
21 Commits
fix/referr
...
feature/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b56813ece | ||
|
|
8d205d9d93 | ||
|
|
9a18e79862 | ||
|
|
ab2df05d50 | ||
|
|
163de16a84 | ||
|
|
544ac18b59 | ||
|
|
2551022d5e | ||
|
|
041b84eb0b | ||
|
|
69166ff501 | ||
|
|
043e159e53 | ||
|
|
d11cccec35 | ||
|
|
f45300583c | ||
|
|
977a4b5c93 | ||
|
|
6589a98d52 | ||
|
|
e7e3b871e4 | ||
|
|
063213cb89 | ||
|
|
0bf6fafb3e | ||
|
|
14a83ec721 | ||
|
|
dfb8889b9d | ||
|
|
b24e3d31b0 | ||
|
|
b27fb5ec07 |
@@ -58,7 +58,7 @@ function App() {
|
|||||||
// rerender
|
// rerender
|
||||||
// setRender((r) => r + 1)
|
// setRender((r) => r + 1)
|
||||||
|
|
||||||
if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
// if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useDebounce } from 'use-debounce'
|
|||||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
import { CheckmarkIcon } from '@/assets'
|
import { CheckmarkIcon } from '@/assets'
|
||||||
|
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
const FORM_DEFAULT_VALUES = {
|
const FORM_DEFAULT_VALUES = {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -42,35 +43,71 @@ export const ModalImportKeys = () => {
|
|||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
})
|
})
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isAvailable, setIsAvailable] = useState(false)
|
const [nameNpub, setNameNpub] = useState('')
|
||||||
|
const [isTakenByNsec, setIsTakenByNsec] = useState(false)
|
||||||
|
const [isBadNsec, setIsBadNsec] = useState(false)
|
||||||
const enteredUsername = watch('username')
|
const enteredUsername = watch('username')
|
||||||
|
const enteredNsec = watch('nsec')
|
||||||
const [debouncedUsername] = useDebounce(enteredUsername, 100)
|
const [debouncedUsername] = useDebounce(enteredUsername, 100)
|
||||||
|
const [debouncedNsec] = useDebounce(enteredNsec, 100)
|
||||||
|
|
||||||
const checkIsUsernameAvailable = useCallback(async () => {
|
const checkIsUsernameAvailable = useCallback(async () => {
|
||||||
if (!debouncedUsername.trim().length) return undefined
|
if (!debouncedUsername.trim().length) return undefined
|
||||||
const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`)
|
const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`)
|
||||||
|
setNameNpub(npubNip05 || '')
|
||||||
setIsAvailable(!npubNip05)
|
|
||||||
}, [debouncedUsername])
|
}, [debouncedUsername])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkIsUsernameAvailable()
|
checkIsUsernameAvailable()
|
||||||
}, [checkIsUsernameAvailable])
|
}, [checkIsUsernameAvailable])
|
||||||
|
|
||||||
|
const checkNsecUsername = useCallback(async () => {
|
||||||
|
if (!debouncedNsec.trim().length) {
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
setIsBadNsec(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { type, data } = nip19.decode(debouncedNsec)
|
||||||
|
const ok = type === 'nsec';
|
||||||
|
setIsBadNsec(!ok)
|
||||||
|
if (ok) {
|
||||||
|
const npub = nip19.npubEncode(
|
||||||
|
// @ts-ignore
|
||||||
|
getPublicKey(data))
|
||||||
|
setIsTakenByNsec(!!nameNpub && nameNpub === npub)
|
||||||
|
} else {
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setIsBadNsec(true)
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}, [debouncedNsec])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkNsecUsername()
|
||||||
|
}, [checkNsecUsername])
|
||||||
|
|
||||||
const cleanUpStates = useCallback(() => {
|
const cleanUpStates = useCallback(() => {
|
||||||
hidePassword()
|
hidePassword()
|
||||||
reset()
|
reset()
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setIsAvailable(false)
|
setNameNpub('')
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
setIsBadNsec(false)
|
||||||
}, [reset, hidePassword])
|
}, [reset, hidePassword])
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const submitHandler = async (values: FormInputType) => {
|
const submitHandler = async (values: FormInputType) => {
|
||||||
if (isLoading || !isAvailable) return undefined
|
if (isLoading) return undefined
|
||||||
try {
|
try {
|
||||||
const { nsec, username } = values
|
const { nsec, username } = values
|
||||||
|
if (!nsec || !username) throw new Error("Enter username and nsec")
|
||||||
|
if (nameNpub && !isTakenByNsec) throw new Error("Name taken")
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const k: any = await swicCall('importKey', username, nsec)
|
const k: any = await swicCall('importKey', username, nsec)
|
||||||
notify('Key imported!', 'success')
|
notify('Key imported!', 'success')
|
||||||
@@ -88,9 +125,11 @@ export const ModalImportKeys = () => {
|
|||||||
}
|
}
|
||||||
}, [isModalOpened, cleanUpStates])
|
}, [isModalOpened, cleanUpStates])
|
||||||
|
|
||||||
const getInputHelperText = () => {
|
const getNameHelperText = () => {
|
||||||
if (!enteredUsername) return "Don't worry, username can be changed later."
|
if (!enteredUsername) return "Don't worry, username can be changed later."
|
||||||
if (!isAvailable) return 'Already taken'
|
if (isTakenByNsec) return 'Name matches your key'
|
||||||
|
if (isBadNsec) return 'Invalid nsec'
|
||||||
|
if (nameNpub) return 'Already taken'
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CheckmarkIcon /> Available
|
<CheckmarkIcon /> Available
|
||||||
@@ -98,7 +137,13 @@ export const ModalImportKeys = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputHelperText = getInputHelperText()
|
const getNsecHelperText = () => {
|
||||||
|
if (isBadNsec) return 'Invalid nsec'
|
||||||
|
return 'Keys stay on your device.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameHelperText = getNameHelperText()
|
||||||
|
const nsecHelperText = getNsecHelperText()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
@@ -116,14 +161,14 @@ export const ModalImportKeys = () => {
|
|||||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||||
{...register('username')}
|
{...register('username')}
|
||||||
error={!!errors.username}
|
error={!!errors.username}
|
||||||
helperText={inputHelperText}
|
helperText={nameHelperText}
|
||||||
helperTextProps={{
|
helperTextProps={{
|
||||||
sx: {
|
sx: {
|
||||||
'&.helper_text': {
|
'&.helper_text': {
|
||||||
color:
|
color:
|
||||||
enteredUsername && isAvailable
|
enteredUsername && (isTakenByNsec || !nameNpub)
|
||||||
? theme.palette.success.main
|
? theme.palette.success.main
|
||||||
: enteredUsername && !isAvailable
|
: enteredUsername && nameNpub
|
||||||
? theme.palette.error.main
|
? theme.palette.error.main
|
||||||
: theme.palette.textSecondaryDecorate.main,
|
: theme.palette.textSecondaryDecorate.main,
|
||||||
},
|
},
|
||||||
@@ -137,11 +182,13 @@ export const ModalImportKeys = () => {
|
|||||||
{...register('nsec')}
|
{...register('nsec')}
|
||||||
error={!!errors.nsec}
|
error={!!errors.nsec}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
helperText="Keys stay on your device."
|
helperText={nsecHelperText}
|
||||||
helperTextProps={{
|
helperTextProps={{
|
||||||
sx: {
|
sx: {
|
||||||
'&.helper_text': {
|
'&.helper_text': {
|
||||||
color: theme.palette.textSecondaryDecorate.main,
|
color: isBadNsec
|
||||||
|
? theme.palette.error.main
|
||||||
|
: theme.palette.textSecondaryDecorate.main,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { dbi } from '@/modules/db'
|
|||||||
import { usePassword } from '@/hooks/usePassword'
|
import { usePassword } from '@/hooks/usePassword'
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectKeys } from '@/store'
|
import { selectKeys } from '@/store'
|
||||||
|
import { isValidPassphase, isWeakPassphase } from '@/modules/keys'
|
||||||
|
|
||||||
type ModalSettingsProps = {
|
type ModalSettingsProps = {
|
||||||
isSynced: boolean
|
isSynced: boolean
|
||||||
@@ -58,8 +59,9 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsPasswordInvalid(false)
|
const password = e.target.value
|
||||||
setEnteredPassword(e.target.value)
|
setIsPasswordInvalid(!!password && !isValidPassphase(password))
|
||||||
|
setEnteredPassword(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
@@ -76,7 +78,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
|
|
||||||
if (enteredPassword.trim().length < 6) {
|
if (!isValidPassphase(enteredPassword)) {
|
||||||
return setIsPasswordInvalid(true)
|
return setIsPasswordInvalid(true)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -114,18 +116,30 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
{...inputProps}
|
{...inputProps}
|
||||||
onChange={handlePasswordChange}
|
onChange={handlePasswordChange}
|
||||||
value={enteredPassword}
|
value={enteredPassword}
|
||||||
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
// helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
||||||
placeholder="Enter a password"
|
placeholder="Enter a password"
|
||||||
helperTextProps={{
|
// helperTextProps={{
|
||||||
sx: {
|
// sx: {
|
||||||
'&.helper_text': {
|
// '&.helper_text': {
|
||||||
color: 'red',
|
// color: 'red',
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
}}
|
// }}
|
||||||
disabled={!isChecked}
|
disabled={!isChecked}
|
||||||
/>
|
/>
|
||||||
{isSynced ? (
|
{isPasswordInvalid ? (
|
||||||
|
<Typography variant="body2" color={'red'}>
|
||||||
|
Password must include 6+ English letters, numbers or punctuation marks.
|
||||||
|
</Typography>
|
||||||
|
) : !!enteredPassword && isWeakPassphase(enteredPassword) ? (
|
||||||
|
<Typography variant="body2" color={'orange'}>
|
||||||
|
Weak password
|
||||||
|
</Typography>
|
||||||
|
) : !!enteredPassword && !isPasswordInvalid ? (
|
||||||
|
<Typography variant="body2" color={'green'}>
|
||||||
|
Good password
|
||||||
|
</Typography>
|
||||||
|
) : isSynced ? (
|
||||||
<Typography variant="body2" color={'GrayText'}>
|
<Typography variant="body2" color={'GrayText'}>
|
||||||
To change your password, type a new one and sync.
|
To change your password, type a new one and sync.
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -139,7 +153,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
Sync {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
Sync {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</StyledSettingContainer>
|
</StyledSettingContainer>
|
||||||
<Button onClick={onClose}>Done</Button>
|
{/* <Button onClick={onClose}>Done</Button> */}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,11 +21,31 @@ const ALGO = 'aes-256-cbc'
|
|||||||
const IV_SIZE = 16
|
const IV_SIZE = 16
|
||||||
|
|
||||||
// valid passwords are a limited ASCII only, see notes below
|
// valid passwords are a limited ASCII only, see notes below
|
||||||
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()]{4,}$/
|
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()\-_]{6,}$/
|
||||||
|
|
||||||
const ALGO_LOCAL = 'AES-CBC'
|
const ALGO_LOCAL = 'AES-CBC'
|
||||||
const KEY_SIZE_LOCAL = 256
|
const KEY_SIZE_LOCAL = 256
|
||||||
|
|
||||||
|
export function isValidPassphase(passphrase: string): boolean {
|
||||||
|
return ASCII_REGEX.test(passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWeakPassphase(passphrase: string): boolean {
|
||||||
|
const BIG_LETTER_REGEX = /[A-Z]+/
|
||||||
|
const SMALL_LETTER_REGEX = /[a-z]+/
|
||||||
|
const NUMBER_REGEX = /[0-9]+/
|
||||||
|
const PUNCT_REGEX = /[!@#$%^&*()\-_]+/
|
||||||
|
const big = BIG_LETTER_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const small = SMALL_LETTER_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const number = NUMBER_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const punct = PUNCT_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const base = big * 26 + small * 26 + number * 10 + punct * 12
|
||||||
|
const compl = Math.pow(base, passphrase.length)
|
||||||
|
const thresh = Math.pow(11, 14)
|
||||||
|
// console.log({ big, small, number, punct, base, compl, thresh });
|
||||||
|
return compl < thresh;
|
||||||
|
}
|
||||||
|
|
||||||
export class Keys {
|
export class Keys {
|
||||||
subtle: any
|
subtle: any
|
||||||
|
|
||||||
@@ -33,10 +53,6 @@ export class Keys {
|
|||||||
this.subtle = cryptoSubtle
|
this.subtle = cryptoSubtle
|
||||||
}
|
}
|
||||||
|
|
||||||
public isValidPassphase(passphrase: string): boolean {
|
|
||||||
return ASCII_REGEX.test(passphrase)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async generatePassKey(pubkey: string, passphrase: string): Promise<{ passkey: Buffer; pwh: string }> {
|
public async generatePassKey(pubkey: string, passphrase: string): Promise<{ passkey: Buffer; pwh: string }> {
|
||||||
const salt = Buffer.from(pubkey, 'hex')
|
const salt = Buffer.from(pubkey, 'hex')
|
||||||
|
|
||||||
@@ -45,7 +61,7 @@ export class Keys {
|
|||||||
// We could use string.normalize() to make sure all JS implementations
|
// We could use string.normalize() to make sure all JS implementations
|
||||||
// are compatible, but since we're looking to make this thing a standard
|
// are compatible, but since we're looking to make this thing a standard
|
||||||
// then the simplest way is to exclude unicode and only work with ASCII
|
// then the simplest way is to exclude unicode and only work with ASCII
|
||||||
if (!this.isValidPassphase(passphrase)) throw new Error('Password must be 4+ ASCII chars')
|
if (!isValidPassphase(passphrase)) throw new Error('Password must be 4+ ASCII chars')
|
||||||
|
|
||||||
return new Promise((ok, fail) => {
|
return new Promise((ok, fail) => {
|
||||||
// NOTE: we should use Argon2 or scrypt later, for now
|
// NOTE: we should use Argon2 or scrypt later, for now
|
||||||
|
|||||||
Reference in New Issue
Block a user