Skip to content

Commit

Permalink
Add encryptedPassphraseKey to credentials on session resume
Browse files Browse the repository at this point in the history
#7223

Co-authored-by: paw <[email protected]>
  • Loading branch information
charlag and paw-hub committed Jul 25, 2024
1 parent a1993d5 commit 318d26c
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 52 deletions.
2 changes: 1 addition & 1 deletion src/api/main/MainLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,8 +737,8 @@ class MainLocator {
this.loginFacade,
this.domainConfigProvider(),
)
this.loginListener = new PageContextLoginListener(this.secondFactorHandler)
this.credentialsProvider = await this.createCredentialsProvider()
this.loginListener = new PageContextLoginListener(this.secondFactorHandler, this.credentialsProvider)
this.random = random

this.usageTestModel = new UsageTestModel(
Expand Down
18 changes: 15 additions & 3 deletions src/api/main/PageContextLoginListener.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { SecondFactorHandler } from "../../misc/2fa/SecondFactorHandler.js"
import { defer, DeferredObject } from "@tutao/tutanota-utils"
import { Challenge } from "../entities/sys/TypeRefs.js"
import { LoginListener } from "../worker/facades/LoginFacade.js"
import { CacheInfo, LoginListener } from "../worker/facades/LoginFacade.js"
import { SessionType } from "../common/SessionType.js"
import { CredentialsProvider } from "../../misc/credentials/CredentialsProvider.js"
import { Credentials, credentialsToUnencrypted } from "../../misc/credentials/Credentials.js"

export const enum LoginFailReason {
SessionExpired,
Expand All @@ -13,7 +16,7 @@ export class PageContextLoginListener implements LoginListener {
private loginPromise: DeferredObject<void> = defer()
private fullLoginFailed: boolean = false

constructor(private readonly secondFactorHandler: SecondFactorHandler) {}
constructor(private readonly secondFactorHandler: SecondFactorHandler, private readonly credentialsProvider: CredentialsProvider) {}

/** e.g. after temp logout */
reset() {
Expand All @@ -28,8 +31,17 @@ export class PageContextLoginListener implements LoginListener {
/**
* Full login reached: any network requests can be made
*/
async onFullLoginSuccess(): Promise<void> {
async onFullLoginSuccess(_sessionType: SessionType, _cacheInfo: CacheInfo, credentials: Credentials): Promise<void> {
this.fullLoginFailed = false
// Update the credentials after the full login.
// It is needed because we added encryptedPassphraseKey to credentials which is only
// available after the full login which happens async.
const storedCredentials = await this.credentialsProvider.getDecryptedCredentialsByUserId(credentials.userId)
if (storedCredentials != null) {
const updatedCredentials = credentialsToUnencrypted(credentials, storedCredentials.databaseKey)
await this.credentialsProvider.store(updatedCredentials)
}

this.loginPromise.resolve()
}

Expand Down
5 changes: 3 additions & 2 deletions src/api/worker/WorkerLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { KeyCache } from "./facades/KeyCache.js"
import { cryptoWrapper } from "./crypto/CryptoWrapper.js"
import { RecoverCodeFacade } from "./facades/lazy/RecoverCodeFacade.js"
import { CacheManagementFacade } from "./facades/lazy/CacheManagementFacade.js"
import type { Credentials } from "../../misc/credentials/Credentials.js"

assertWorkerOrNode()

Expand Down Expand Up @@ -271,15 +272,15 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
)

const loginListener: LoginListener = {
onFullLoginSuccess(sessionType: SessionType, cacheInfo: CacheInfo): Promise<void> {
onFullLoginSuccess(sessionType: SessionType, cacheInfo: CacheInfo, credentials: Credentials): Promise<void> {
if (!isTest() && sessionType !== SessionType.Temporary && !isAdminClient()) {
// index new items in background
console.log("initIndexer after log in")

initIndexer(worker, cacheInfo, locator.keyLoader)
}

return mainInterface.loginListener.onFullLoginSuccess(sessionType, cacheInfo)
return mainInterface.loginListener.onFullLoginSuccess(sessionType, cacheInfo, credentials)
},

onLoginFailure(reason: LoginFailReason): Promise<void> {
Expand Down
73 changes: 31 additions & 42 deletions src/api/worker/facades/LoginFacade.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { Base64Url, DeferredObject, Hex, uint8ArrayToString, utf8Uint8ArrayToString } from "@tutao/tutanota-utils"
import {
arrayEquals,
assertNotNull,
Base64,
base64ToBase64Ext,
base64ToBase64Url,
base64ToUint8Array,
Base64Url,
base64UrlToBase64,
defer,
DeferredObject,
Hex,
hexToUint8Array,
neverNull,
ofClass,
uint8ArrayToBase64,
utf8Uint8ArrayToString,
} from "@tutao/tutanota-utils"
import {
ChangeKdfService,
Expand Down Expand Up @@ -146,7 +149,7 @@ export interface LoginListener {
/**
* Full login reached: any network requests can be made
*/
onFullLoginSuccess(sessionType: SessionType, cacheInfo: CacheInfo): Promise<void>
onFullLoginSuccess(sessionType: SessionType, cacheInfo: CacheInfo, credentials: Credentials): Promise<void>

/**
* call when the login fails for invalid session or other reasons
Expand Down Expand Up @@ -257,18 +260,23 @@ export class LoginFacade {
timeRangeDays: null,
forceNewDatabase,
})
const { user, userGroupInfo, accessToken } = await this.initSession(
sessionData.userId,
sessionData.accessToken,
userPassphraseKey,
sessionType,
cacheInfo,
)
const { user, userGroupInfo, accessToken } = await this.initSession(sessionData.userId, sessionData.accessToken, userPassphraseKey)

const modernKdfType = this.isModernKdfType(kdfType)
if (!modernKdfType) {
await this.migrateKdfType(KdfType.Argon2id, passphrase, user)
}

const credentials = {
login: mailAddress,
accessToken,
encryptedPassword: sessionType === SessionType.Persistent ? uint8ArrayToBase64(encryptString(neverNull(accessKey), passphrase)) : null,
encryptedPassphraseKey: sessionType === SessionType.Persistent ? encryptKey(neverNull(accessKey), userPassphraseKey) : null,
userId: sessionData.userId,
type: CredentialType.Internal,
}
this.loginListener.onFullLoginSuccess(sessionType, cacheInfo, credentials)

if (!isAdminClient()) {
await this.keyRotationFacade.initialize(userPassphraseKey, modernKdfType)
}
Expand All @@ -277,14 +285,7 @@ export class LoginFacade {
user,
userGroupInfo,
sessionId: sessionData.sessionId,
credentials: {
login: mailAddress,
accessToken,
encryptedPassword: sessionType === SessionType.Persistent ? uint8ArrayToBase64(encryptString(neverNull(accessKey), passphrase)) : null,
encryptedPassphraseKey: sessionType === SessionType.Persistent ? encryptKey(neverNull(accessKey), userPassphraseKey) : null,
userId: sessionData.userId,
type: CredentialType.Internal,
},
credentials: credentials,
// we always try to make a persistent cache with a key for persistent session, but this
// falls back to ephemeral cache in browsers. no point storing the key then.
databaseKey: cacheInfo.isPersistent ? databaseKey : null,
Expand Down Expand Up @@ -445,25 +446,21 @@ export class LoginFacade {
timeRangeDays: null,
forceNewDatabase: true,
})
const { user, userGroupInfo, accessToken } = await this.initSession(
createSessionReturn.user,
createSessionReturn.accessToken,
userPassphraseKey,
SessionType.Login,
cacheInfo,
)
const { user, userGroupInfo, accessToken } = await this.initSession(createSessionReturn.user, createSessionReturn.accessToken, userPassphraseKey)
const credentials = {
login: userId,
accessToken,
encryptedPassword: accessKey ? uint8ArrayToBase64(encryptString(accessKey, passphrase)) : null,
encryptedPassphraseKey: accessKey ? encryptKey(accessKey, userPassphraseKey) : null,
userId,
type: CredentialType.External,
}
this.loginListener.onFullLoginSuccess(SessionType.Login, cacheInfo, credentials)
return {
user,
userGroupInfo,
sessionId,
credentials: {
login: userId,
accessToken,
encryptedPassword: accessKey ? uint8ArrayToBase64(encryptString(accessKey, passphrase)) : null,
encryptedPassphraseKey: accessKey ? encryptKey(accessKey, userPassphraseKey) : null,
userId,
type: CredentialType.External,
},
credentials: credentials,
databaseKey: null,
}
}
Expand Down Expand Up @@ -672,13 +669,8 @@ export class LoginFacade {
throw new ProgrammingError("no key or password stored in credentials!")
}

const { user, userGroupInfo } = await this.initSession(
sessionData.userId,
credentials.accessToken,
userPassphraseKey,
SessionType.Persistent,
cacheInfo,
)
const { user, userGroupInfo } = await this.initSession(sessionData.userId, credentials.accessToken, userPassphraseKey)
this.loginListener.onFullLoginSuccess(SessionType.Persistent, cacheInfo, credentials)

this.asyncLoginState = { state: "idle" }

Expand Down Expand Up @@ -707,8 +699,6 @@ export class LoginFacade {
userId: Id,
accessToken: Base64Url,
userPassphraseKey: AesKey,
sessionType: SessionType,
cacheInfo: CacheInfo,
): Promise<{ user: User; accessToken: string; userGroupInfo: GroupInfo }> {
// We might have userId already if:
// - session has expired and a new one was created
Expand Down Expand Up @@ -746,7 +736,6 @@ export class LoginFacade {
}

await this.entropyFacade.storeEntropy()
this.loginListener.onFullLoginSuccess(sessionType, cacheInfo)
return { user, accessToken, userGroupInfo }
} catch (e) {
this.resetSession()
Expand Down
8 changes: 4 additions & 4 deletions test/tests/api/worker/facades/LoginFacadeTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ o.spec("LoginFacadeTest", function () {
when(userFacade.isPartiallyLoggedIn()).thenDo(() => calls.includes("setUser"))

fullLoginDeferred = defer()
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything())).thenDo(() => fullLoginDeferred.resolve())
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything(), matchers.anything())).thenDo(() => fullLoginDeferred.resolve())
})

o("When using offline as a free user and with stable connection, login sync", async function () {
Expand Down Expand Up @@ -377,7 +377,7 @@ o.spec("LoginFacadeTest", function () {
})

const deferred = defer()
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything())).thenDo(() => deferred.resolve(null))
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything(), matchers.anything())).thenDo(() => deferred.resolve(null))

const result = await facade.resumeSession(credentials, { salt: user.salt!, kdfType: DEFAULT_KDF_TYPE }, dbKey, timeRangeDays)

Expand Down Expand Up @@ -509,7 +509,7 @@ o.spec("LoginFacadeTest", function () {
when(userFacade.isPartiallyLoggedIn()).thenDo(() => calls.includes("setUser"))

fullLoginDeferred = defer()
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything())).thenDo(() => fullLoginDeferred.resolve())
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything(), matchers.anything())).thenDo(() => fullLoginDeferred.resolve())
})

o("When successfully logged in, userFacade is initialised", async function () {
Expand Down Expand Up @@ -613,7 +613,7 @@ o.spec("LoginFacadeTest", function () {
when(userFacade.isPartiallyLoggedIn()).thenDo(() => calls.includes("setUser"))

fullLoginDeferred = defer()
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything())).thenDo(() => fullLoginDeferred.resolve())
when(loginListener.onFullLoginSuccess(matchers.anything(), matchers.anything(), matchers.anything())).thenDo(() => fullLoginDeferred.resolve())
})

o("When successfully logged in, userFacade is initialised", async function () {
Expand Down

0 comments on commit 318d26c

Please sign in to comment.