feat: replace private workspaces with private usernames (#1783)

This commit is contained in:
Mo
2022-10-12 13:52:34 -05:00
committed by GitHub
parent 038e456c6a
commit 18c821d8eb
12 changed files with 84 additions and 108 deletions

View File

@@ -0,0 +1,19 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
const PrivateUserNameV1 = 'StandardNotes-PrivateUsername-V1'
export async function ComputePrivateUsername(
crypto: PureCryptoInterface,
usernameInput: string,
): Promise<string | undefined> {
const result = await crypto.hmac256(
await crypto.sha256(PrivateUserNameV1),
await crypto.sha256(usernameInput.trim().toLowerCase()),
)
if (result == undefined) {
return undefined
}
return result
}

View File

@@ -1,18 +0,0 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
export async function ComputePrivateWorkspaceIdentifier(
crypto: PureCryptoInterface,
userphrase: string,
name: string,
): Promise<string | undefined> {
const identifier = await crypto.hmac256(
await crypto.sha256(name.trim().toLowerCase()),
await crypto.sha256(userphrase.trim().toLowerCase()),
)
if (identifier == undefined) {
return undefined
}
return identifier
}

View File

@@ -34,4 +34,4 @@ export * from './Types/EncryptedParameters'
export * from './Types/ItemAuthenticatedData' export * from './Types/ItemAuthenticatedData'
export * from './Types/LegacyAttachedData' export * from './Types/LegacyAttachedData'
export * from './Types/RootKeyEncryptedAuthenticatedData' export * from './Types/RootKeyEncryptedAuthenticatedData'
export * from './Workspace/PrivateWorkspace' export * from './Username/PrivateUsername'

View File

@@ -53,7 +53,7 @@ import {
WorkspaceManager, WorkspaceManager,
} from '@standardnotes/services' } from '@standardnotes/services'
import { FilesClientInterface } from '@standardnotes/files' import { FilesClientInterface } from '@standardnotes/files'
import { ComputePrivateWorkspaceIdentifier } from '@standardnotes/encryption' import { ComputePrivateUsername } from '@standardnotes/encryption'
import { useBoolean } from '@standardnotes/utils' import { useBoolean } from '@standardnotes/utils'
import { import {
BackupFile, BackupFile,
@@ -272,8 +272,8 @@ export class SNApplication
return this.componentManagerService return this.componentManagerService
} }
public computePrivateWorkspaceIdentifier(userphrase: string, name: string): Promise<string | undefined> { public computePrivateUsername(username: string): Promise<string | undefined> {
return ComputePrivateWorkspaceIdentifier(this.options.crypto, userphrase, name) return ComputePrivateUsername(this.options.crypto, username)
} }
/** /**

View File

@@ -43,7 +43,7 @@
<script type="module" src="002.test.js"></script> <script type="module" src="002.test.js"></script>
<script type="module" src="003.test.js"></script> <script type="module" src="003.test.js"></script>
<script type="module" src="004.test.js"></script> <script type="module" src="004.test.js"></script>
<script type="module" src="workspaces.test.js"></script> <script type="module" src="username.test.js"></script>
<script type="module" src="app-group.test.js"></script> <script type="module" src="app-group.test.js"></script>
<script type="module" src="application.test.js"></script> <script type="module" src="application.test.js"></script>
<script type="module" src="payload.test.js"></script> <script type="module" src="payload.test.js"></script>

View File

@@ -0,0 +1,12 @@
chai.use(chaiAsPromised)
const expect = chai.expect
describe('private username', () => {
it('generates private username', async () => {
const username = 'myusername'
const result = await ComputePrivateUsername(new SNWebCrypto(), username)
expect(result).to.equal('9aae57db8dbb233291a49cb7b8ab902336ec785e04f3be70157b8c1669014d0d')
})
})

View File

@@ -1,25 +0,0 @@
chai.use(chaiAsPromised)
const expect = chai.expect
import * as Factory from './lib/factory.js'
describe('private workspaces', () => {
it('generates identifier', async () => {
const userphrase = 'myworkspaceuserphrase'
const name = 'myworkspacename'
const result = await ComputePrivateWorkspaceIdentifier(new SNWebCrypto(), userphrase, name)
expect(result).to.equal('5155c13a44f333790f6564fbcee0c35a16d26a8359dd77d67d8ecc6ad5d399bb')
})
it('application result matches direct function call', async () => {
const userphrase = 'myworkspaceuserphrase'
const name = 'myworkspacename'
const application = (await Factory.createAppContextWithRealCrypto()).application
const appResult = await application.computePrivateWorkspaceIdentifier(userphrase, name)
const directResult = await ComputePrivateWorkspaceIdentifier(new SNWebCrypto(), userphrase, name)
expect(appResult).to.equal(directResult)
})
})

View File

@@ -10,7 +10,7 @@ type Props = {
application: WebApplication application: WebApplication
viewControllerManager: ViewControllerManager viewControllerManager: ViewControllerManager
disabled?: boolean disabled?: boolean
onPrivateWorkspaceChange?: (isPrivate: boolean, identifier?: string) => void onPrivateUsernameModeChange?: (isPrivate: boolean, identifier?: string) => void
onStrictSignInChange?: (isStrictSignIn: boolean) => void onStrictSignInChange?: (isStrictSignIn: boolean) => void
children?: ReactNode children?: ReactNode
} }
@@ -19,54 +19,46 @@ const AdvancedOptions: FunctionComponent<Props> = ({
viewControllerManager, viewControllerManager,
application, application,
disabled = false, disabled = false,
onPrivateWorkspaceChange, onPrivateUsernameModeChange,
onStrictSignInChange, onStrictSignInChange,
children, children,
}) => { }) => {
const { server, setServer, enableServerOption, setEnableServerOption } = viewControllerManager.accountMenuController const { server, setServer, enableServerOption, setEnableServerOption } = viewControllerManager.accountMenuController
const [showAdvanced, setShowAdvanced] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false)
const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false) const [isPrivateUsername, setIsPrivateUsername] = useState(false)
const [privateWorkspaceName, setPrivateWorkspaceName] = useState('') const [privateUsername, setPrivateUsername] = useState('')
const [privateWorkspaceUserphrase, setPrivateWorkspaceUserphrase] = useState('')
const [isStrictSignin, setIsStrictSignin] = useState(false) const [isStrictSignin, setIsStrictSignin] = useState(false)
useEffect(() => { useEffect(() => {
const recomputePrivateWorkspaceIdentifier = async () => { const recomputePrivateUsername = async () => {
const identifier = await application.computePrivateWorkspaceIdentifier( const identifier = await application.computePrivateUsername(privateUsername)
privateWorkspaceName,
privateWorkspaceUserphrase,
)
if (!identifier) { if (!identifier) {
if (privateWorkspaceName?.length > 0 && privateWorkspaceUserphrase?.length > 0) { if (privateUsername?.length > 0) {
application.alertService.alert('Unable to compute private workspace name.').catch(console.error) application.alertService.alert('Unable to compute private username.').catch(console.error)
} }
return return
} }
onPrivateWorkspaceChange?.(true, identifier) onPrivateUsernameModeChange?.(true, identifier)
} }
if (privateWorkspaceName && privateWorkspaceUserphrase) { if (privateUsername) {
recomputePrivateWorkspaceIdentifier().catch(console.error) recomputePrivateUsername().catch(console.error)
} }
}, [privateWorkspaceName, privateWorkspaceUserphrase, application, onPrivateWorkspaceChange]) }, [privateUsername, application, onPrivateUsernameModeChange])
useEffect(() => { useEffect(() => {
onPrivateWorkspaceChange?.(isPrivateWorkspace) onPrivateUsernameModeChange?.(isPrivateUsername)
}, [isPrivateWorkspace, onPrivateWorkspaceChange]) }, [isPrivateUsername, onPrivateUsernameModeChange])
const handleIsPrivateWorkspaceChange = useCallback(() => { const handleIsPrivateUsernameChange = useCallback(() => {
setIsPrivateWorkspace(!isPrivateWorkspace) setIsPrivateUsername(!isPrivateUsername)
}, [isPrivateWorkspace]) }, [isPrivateUsername])
const handlePrivateWorkspaceNameChange = useCallback((name: string) => { const handlePrivateUsernameNameChange = useCallback((name: string) => {
setPrivateWorkspaceName(name) setPrivateUsername(name)
}, [])
const handlePrivateWorkspaceUserphraseChange = useCallback((userphrase: string) => {
setPrivateWorkspaceUserphrase(userphrase)
}, []) }, [])
const handleServerOptionChange: ChangeEventHandler<HTMLInputElement> = useCallback( const handleServerOptionChange: ChangeEventHandler<HTMLInputElement> = useCallback(
@@ -114,35 +106,28 @@ const AdvancedOptions: FunctionComponent<Props> = ({
<div className="mb-1 flex items-center justify-between"> <div className="mb-1 flex items-center justify-between">
<Checkbox <Checkbox
name="private-workspace" name="private-workspace"
label="Private workspace" label="Private username mode"
checked={isPrivateWorkspace} checked={isPrivateUsername}
disabled={disabled} disabled={disabled}
onChange={handleIsPrivateWorkspaceChange} onChange={handleIsPrivateUsernameChange}
/> />
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more"> <a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
<Icon type="info" className="text-neutral" /> <Icon type="info" className="text-neutral" />
</a> </a>
</div> </div>
{isPrivateWorkspace && ( {isPrivateUsername && (
<> <>
<DecoratedInput <DecoratedInput
className={{ container: 'mb-2' }} className={{ container: 'mb-2' }}
left={[<Icon type="server" className="text-neutral" />]} left={[<Icon type="account-circle" className="text-neutral" />]}
type="text" type="text"
placeholder="Userphrase" placeholder="Username"
value={privateWorkspaceUserphrase} value={privateUsername}
onChange={handlePrivateWorkspaceUserphraseChange} onChange={handlePrivateUsernameNameChange}
disabled={disabled}
/>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="folder" className="text-neutral" />]}
type="text"
placeholder="Name"
value={privateWorkspaceName}
onChange={handlePrivateWorkspaceNameChange}
disabled={disabled} disabled={disabled}
spellcheck={false}
autocomplete={false}
/> />
</> </>
)} )}

View File

@@ -40,7 +40,7 @@ const CreateAccount: FunctionComponent<Props> = ({
}) => { }) => {
const emailInputRef = useRef<HTMLInputElement>(null) const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null) const passwordInputRef = useRef<HTMLInputElement>(null)
const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false) const [isPrivateUsername, setIsPrivateUsername] = useState(false)
useEffect(() => { useEffect(() => {
if (emailInputRef.current) { if (emailInputRef.current) {
@@ -98,11 +98,11 @@ const CreateAccount: FunctionComponent<Props> = ({
setPassword('') setPassword('')
}, [setEmail, setMenuPane, setPassword]) }, [setEmail, setMenuPane, setPassword])
const onPrivateWorkspaceChange = useCallback( const onPrivateUsernameChange = useCallback(
(isPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => { (isPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
setIsPrivateWorkspace(isPrivateWorkspace) setIsPrivateUsername(isPrivateUsername)
if (isPrivateWorkspace && privateWorkspaceIdentifier) { if (isPrivateUsername && privateUsernameIdentifier) {
setEmail(privateWorkspaceIdentifier) setEmail(privateUsernameIdentifier)
} }
}, },
[setEmail], [setEmail],
@@ -123,7 +123,7 @@ const CreateAccount: FunctionComponent<Props> = ({
<form onSubmit={handleRegisterFormSubmit} className="mb-1 px-3"> <form onSubmit={handleRegisterFormSubmit} className="mb-1 px-3">
<DecoratedInput <DecoratedInput
className={{ container: 'mb-2' }} className={{ container: 'mb-2' }}
disabled={isPrivateWorkspace} disabled={isPrivateUsername}
left={[<Icon type="email" className="text-neutral" />]} left={[<Icon type="email" className="text-neutral" />]}
onChange={handleEmailChange} onChange={handleEmailChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -147,7 +147,7 @@ const CreateAccount: FunctionComponent<Props> = ({
<AdvancedOptions <AdvancedOptions
application={application} application={application}
viewControllerManager={viewControllerManager} viewControllerManager={viewControllerManager}
onPrivateWorkspaceChange={onPrivateWorkspaceChange} onPrivateUsernameModeChange={onPrivateUsernameChange}
/> />
</> </>
) )

View File

@@ -29,7 +29,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
const [isStrictSignin, setIsStrictSignin] = useState(false) const [isStrictSignin, setIsStrictSignin] = useState(false)
const [isSigningIn, setIsSigningIn] = useState(false) const [isSigningIn, setIsSigningIn] = useState(false)
const [shouldMergeLocal, setShouldMergeLocal] = useState(true) const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false) const [isPrivateUsername, setIsPrivateUsername] = useState(false)
const emailInputRef = useRef<HTMLInputElement>(null) const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null) const passwordInputRef = useRef<HTMLInputElement>(null)
@@ -100,11 +100,11 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
}) })
}, [viewControllerManager, application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal]) }, [viewControllerManager, application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
const onPrivateWorkspaceChange = useCallback( const onPrivateUsernameChange = useCallback(
(newIsPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => { (newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
setIsPrivateWorkspace(newIsPrivateWorkspace) setIsPrivateUsername(newisPrivateUsername)
if (newIsPrivateWorkspace && privateWorkspaceIdentifier) { if (newisPrivateUsername && privateUsernameIdentifier) {
setEmail(privateWorkspaceIdentifier) setEmail(privateUsernameIdentifier)
} }
}, },
[setEmail], [setEmail],
@@ -161,7 +161,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
onChange={handleEmailChange} onChange={handleEmailChange}
onFocus={resetInvalid} onFocus={resetInvalid}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={isSigningIn || isPrivateWorkspace} disabled={isSigningIn || isPrivateUsername}
ref={emailInputRef} ref={emailInputRef}
/> />
<DecoratedPasswordInput <DecoratedPasswordInput
@@ -206,7 +206,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
viewControllerManager={viewControllerManager} viewControllerManager={viewControllerManager}
application={application} application={application}
disabled={isSigningIn} disabled={isSigningIn}
onPrivateWorkspaceChange={onPrivateWorkspaceChange} onPrivateUsernameModeChange={onPrivateUsernameChange}
onStrictSignInChange={handleStrictSigninChange} onStrictSignInChange={handleStrictSigninChange}
/> />
</> </>

View File

@@ -20,6 +20,7 @@ const DecoratedInput = forwardRef(
( (
{ {
autocomplete = false, autocomplete = false,
spellcheck = true,
className, className,
disabled = false, disabled = false,
id, id,
@@ -68,6 +69,7 @@ const DecoratedInput = forwardRef(
title={title} title={title}
type={type} type={type}
value={value} value={value}
spellCheck={spellcheck}
/> />
{right && ( {right && (

View File

@@ -2,6 +2,7 @@ import { FocusEventHandler, KeyboardEventHandler, ReactNode } from 'react'
export type DecoratedInputProps = { export type DecoratedInputProps = {
autocomplete?: boolean autocomplete?: boolean
spellcheck?: boolean
className?: { className?: {
container?: string container?: string
input?: string input?: string