Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/api/src/Domain/Client/Auth/AuthApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ export class AuthApiService implements AuthApiServiceInterface {
this.operationsInProgress = new Map()
}

async generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
async generateRecoveryCodes(dto: {
serverPassword: string
}): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
if (this.operationsInProgress.get(AuthApiOperations.GenerateRecoveryCodes)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}

this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true)

try {
const response = await this.authServer.generateRecoveryCodes()
const response = await this.authServer.generateRecoveryCodes({
headers: [{ key: 'x-server-password', value: dto.serverPassword }],
})

return response
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '../../Response'

export interface AuthApiServiceInterface {
generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
generateRecoveryCodes(dto: { serverPassword: string }): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
recoveryKeyParams(dto: {
username: string
codeChallenge: string
Expand Down
14 changes: 10 additions & 4 deletions packages/api/src/Domain/Client/User/UserApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,19 @@ export class UserApiService implements UserApiServiceInterface {
this.operationsInProgress = new Map()
}

async deleteAccount(userUuid: string): Promise<HttpResponse<UserDeletionResponseBody>> {
async deleteAccount(dto: {
userUuid: string
serverPassword: string
}): Promise<HttpResponse<UserDeletionResponseBody>> {
this.lockOperation(UserApiOperations.DeletingAccount)

try {
const response = await this.userServer.deleteAccount({
userUuid: userUuid,
})
const response = await this.userServer.deleteAccount(
{
userUuid: dto.userUuid,
},
{ headers: [{ key: 'x-server-password', value: dto.serverPassword }] },
)

this.unlockOperation(UserApiOperations.DeletingAccount)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,8 @@ export interface UserApiServiceInterface {
requestType: UserRequestType
}): Promise<HttpResponse<UserRequestResponseBody>>

deleteAccount(userUuid: string): Promise<HttpResponse<UserDeletionResponseBody>>
deleteAccount(dto: {
userUuid: string
serverPassword: string | undefined
}): Promise<HttpResponse<UserDeletionResponseBody>>
}
3 changes: 3 additions & 0 deletions packages/api/src/Domain/Http/HttpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class HttpService implements HttpServiceInterface {
params,
verb: HttpVerb.Get,
authentication: options?.authentication ?? this.getSessionAccessToken(),
customHeaders: options?.headers,
})
}

Expand Down Expand Up @@ -123,6 +124,7 @@ export class HttpService implements HttpServiceInterface {
params,
verb: HttpVerb.Put,
authentication: options?.authentication ?? this.getSessionAccessToken(),
customHeaders: options?.headers,
})
}

Expand All @@ -141,6 +143,7 @@ export class HttpService implements HttpServiceInterface {
params,
verb: HttpVerb.Delete,
authentication: options?.authentication ?? this.getSessionAccessToken(),
customHeaders: options?.headers,
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface GenerateRecoveryCodesRequestParams {
serverPassword: string
}
1 change: 1 addition & 0 deletions packages/api/src/Domain/Request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './Authenticator/DeleteAuthenticatorRequestParams'
export * from './Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams'
export * from './Authenticator/ListAuthenticatorsRequestParams'
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams'
export * from './Recovery/GenerateRecoveryCodesRequestParams'
export * from './Recovery/RecoveryKeyParamsRequestParams'
export * from './Recovery/SignInWithRecoveryCodesRequestParams'
export * from './Revision/DeleteRevisionRequestParams'
Expand Down
5 changes: 3 additions & 2 deletions packages/api/src/Domain/Server/Auth/AuthServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {
} from '../../Response'
import { AuthServerInterface } from './AuthServerInterface'
import { Paths } from './Paths'
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'

export class AuthServer implements AuthServerInterface {
constructor(private httpService: HttpServiceInterface) {}

async generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
return this.httpService.post(Paths.v1.generateRecoveryCodes)
async generateRecoveryCodes(options?: HttpRequestOptions): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
return this.httpService.post(Paths.v1.generateRecoveryCodes, undefined, options)
}

async recoveryKeyParams(
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/Domain/Server/Auth/AuthServerInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
RecoveryKeyParamsResponseBody,
SignInWithRecoveryCodesResponseBody,
} from '../../Response'
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'

export interface AuthServerInterface {
generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
generateRecoveryCodes(options?: HttpRequestOptions): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<HttpResponse<RecoveryKeyParamsResponseBody>>
signInWithRecoveryCodes(
params: SignInWithRecoveryCodesRequestParams,
Expand Down
8 changes: 6 additions & 2 deletions packages/api/src/Domain/Server/User/UserServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrati
import { Paths } from './Paths'
import { UserServerInterface } from './UserServerInterface'
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'

export class UserServer implements UserServerInterface {
constructor(private httpService: HttpServiceInterface) {}

async deleteAccount(params: UserDeletionRequestParams): Promise<HttpResponse<UserDeletionResponseBody>> {
return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params)
async deleteAccount(
params: UserDeletionRequestParams,
options?: HttpRequestOptions,
): Promise<HttpResponse<UserDeletionResponseBody>> {
return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params, options)
}

async register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>> {
Expand Down
6 changes: 5 additions & 1 deletion packages/api/src/Domain/Server/User/UserServerInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { UserDeletionResponseBody } from '../../Response/User/UserDeletionRespon
import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody'
import { UserUpdateResponse } from '../../Response/User/UserUpdateResponse'
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'

export interface UserServerInterface {
register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>>
deleteAccount(params: UserDeletionRequestParams): Promise<HttpResponse<UserDeletionResponseBody>>
deleteAccount(
params: UserDeletionRequestParams,
options?: HttpRequestOptions,
): Promise<HttpResponse<UserDeletionResponseBody>>
update(params: UserUpdateRequestParams): Promise<HttpResponse<UserUpdateResponse>>
}
2 changes: 1 addition & 1 deletion packages/services/src/Domain/Auth/AuthClientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AnyKeyParamsContent } from '@standardnotes/common'
import { SessionBody } from '@standardnotes/responses'

export interface AuthClientInterface {
generateRecoveryCodes(): Promise<string | false>
generateRecoveryCodes(dto: { serverPassword: string }): Promise<string | false>
recoveryKeyParams(dto: {
username: string
codeChallenge: string
Expand Down
4 changes: 2 additions & 2 deletions packages/services/src/Domain/Auth/AuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export class AuthManager extends AbstractService implements AuthClientInterface
super(internalEventBus)
}

async generateRecoveryCodes(): Promise<string | false> {
async generateRecoveryCodes(dto: { serverPassword: string }): Promise<string | false> {
try {
const result = await this.authApiService.generateRecoveryCodes()
const result = await this.authApiService.generateRecoveryCodes(dto)

if (isErrorResponse(result)) {
return false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models'
import { ChallengeInterface, ChallengeReason } from '../Challenge'
import { ChallengeInterface, ChallengeReason, ChallengeResponseInterface } from '../Challenge'
import { MobileUnlockTiming } from './MobileUnlockTiming'
import { TimingDisplayOption } from './TimingDisplayOption'
import { ProtectionEvent } from './ProtectionEvent'
Expand Down Expand Up @@ -28,6 +28,10 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface<
reason: ChallengeReason,
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
): Promise<boolean>
authorizeActionWithChallengeResponse(
reason: ChallengeReason,
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
authorizeAddingPasscode(): Promise<boolean>
authorizeRemovingPasscode(): Promise<boolean>
authorizeChangingPasscode(): Promise<boolean>
Expand All @@ -36,7 +40,8 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface<
authorizeAutolockIntervalChange(): Promise<boolean>
authorizeSearchingProtectedNotesText(): Promise<boolean>
authorizeBackupCreation(): Promise<boolean>
authorizeMfaDisable(): Promise<boolean>
authorizeMfaDisable(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>

protectItems<I extends DecryptedItemInterface>(items: I[]): Promise<I[]>
unprotectItems<I extends DecryptedItemInterface>(items: I[], reason: ChallengeReason): Promise<I[] | undefined>
Expand Down
18 changes: 10 additions & 8 deletions packages/services/src/Domain/User/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,21 +237,23 @@ export class UserService
error: boolean
message?: string
}> {
if (
!(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, {
fallBackToAccountPassword: true,
requireAccountPassword: true,
forcePrompt: false,
}))
) {
const { success, challengeResponse } = await this.protections.authorizeAccountDeletion()

if (!success) {
return {
error: true,
message: Messages.INVALID_PASSWORD,
}
}

const uuid = this.sessions.getSureUser().uuid
const response = await this.userApi.deleteAccount(uuid)
const password = challengeResponse?.getValueForType(ChallengeValidation.AccountPassword).value as string
const currentRootKey = await this.encryption.computeRootKey(
password,
this.encryption.getRootKeyParams() as SNRootKeyParams,
)
const serverPassword = currentRootKey.serverPassword
const response = await this.userApi.deleteAccount({ userUuid: uuid, serverPassword: serverPassword })
if (isErrorResponse(response)) {
return {
error: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/snjs/lib/Application/Dependencies/Dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,7 @@ export class Dependencies {
return new GetRecoveryCodes(
this.get<AuthManager>(TYPES.AuthManager),
this.get<SettingsService>(TYPES.SettingsService),
this.get<EncryptionService>(TYPES.EncryptionService),
)
})

Expand Down Expand Up @@ -1231,6 +1232,7 @@ export class Dependencies {
this.get<PureCryptoInterface>(TYPES.Crypto),
this.get<FeaturesService>(TYPES.FeaturesService),
this.get<ProtectionsClientInterface>(TYPES.ProtectionService),
this.get<EncryptionService>(TYPES.EncryptionService),
this.get<InternalEventBus>(TYPES.InternalEventBus),
)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { AuthClientInterface } from '@standardnotes/services'
import { AuthClientInterface, EncryptionService } from '@standardnotes/services'
import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'

import { GetRecoveryCodes } from './GetRecoveryCodes'

describe('GetRecoveryCodes', () => {
let authClient: AuthClientInterface
let settingsClient: SettingsClientInterface
let encryption: EncryptionService

const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient)
const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient, encryption)

beforeEach(() => {
authClient = {} as jest.Mocked<AuthClientInterface>
authClient.generateRecoveryCodes = jest.fn().mockResolvedValue('recovery-codes')

settingsClient = {} as jest.Mocked<SettingsClientInterface>
settingsClient.getSetting = jest.fn().mockResolvedValue('existing-recovery-codes')

encryption = {} as jest.Mocked<EncryptionService>
encryption.computeRootKey = jest.fn().mockResolvedValue({ serverPassword: 'test-server-password' })
encryption.getRootKeyParams = jest.fn().mockReturnValue({ algorithm: 'test-algorithm' })
})

it('should return existing recovery code if they exist', async () => {
const useCase = createUseCase()

const result = await useCase.execute()
const result = await useCase.execute({ password: 'test-password' })

expect(result.getValue()).toBe('existing-recovery-codes')
})
Expand All @@ -30,7 +35,7 @@ describe('GetRecoveryCodes', () => {

const useCase = createUseCase()

const result = await useCase.execute()
const result = await useCase.execute({ password: 'test-password' })

expect(result.getValue()).toBe('recovery-codes')
})
Expand All @@ -41,7 +46,7 @@ describe('GetRecoveryCodes', () => {

const useCase = createUseCase()

const result = await useCase.execute()
const result = await useCase.execute({ password: 'test-password' })

expect(result.isFailed()).toBe(true)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
import { AuthClientInterface } from '@standardnotes/services'
import { AuthClientInterface, EncryptionService } from '@standardnotes/services'
import { Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'

import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'
import { GetRecoveryCodesDTO } from './GetRecoveryCodesDTO'
import { SNRootKeyParams } from '@standardnotes/encryption'

export class GetRecoveryCodes implements UseCaseInterface<string> {
constructor(
private authClient: AuthClientInterface,
private settingsClient: SettingsClientInterface,
private encryption: EncryptionService,
) {}

async execute(): Promise<Result<string>> {
async execute(dto: GetRecoveryCodesDTO): Promise<Result<string>> {
if (!dto.password) {
return Result.fail('Password is required to get recovery code.')
}
const currentRootKey = await this.encryption.computeRootKey(
dto.password,
this.encryption.getRootKeyParams() as SNRootKeyParams,
)
const serverPassword = currentRootKey.serverPassword

if (!serverPassword) {
return Result.fail('Could not compute server password')
}

const existingRecoveryCodes = await this.settingsClient.getSetting(
SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
serverPassword,
)
if (existingRecoveryCodes !== undefined) {
return Result.ok(existingRecoveryCodes)
}

const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes()
const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes({ serverPassword })
if (generatedRecoveryCodes === false) {
return Result.fail('Could not generate recovery code')
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface GetRecoveryCodesDTO {
password?: string
}
Loading
Loading