feat: replace private workspaces with private usernames (#1783)
This commit is contained in:
19
packages/encryption/src/Domain/Username/PrivateUsername.ts
Normal file
19
packages/encryption/src/Domain/Username/PrivateUsername.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
12
packages/snjs/mocha/username.test.js
Normal file
12
packages/snjs/mocha/username.test.js
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user