Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Add support for deleting account
Browse files Browse the repository at this point in the history
  • Loading branch information
M3DZIK committed Nov 14, 2023
1 parent 9768a44 commit 63cf411
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 4 deletions.
23 changes: 23 additions & 0 deletions client/src/main/kotlin/dev/medzik/librepass/client/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,29 @@ class Client(
return executeAndExtractBody(request)
}

/**
* Send a DELETE request to the API.
* @param endpoint endpoint of the API
* @param json JSON body of the request
* @return response body
*/
@Throws(ClientException::class, ApiException::class)
fun delete(
endpoint: String,
json: String
): String {
val body = json.toRequestBody(httpMediaTypeJson)

val request =
Request.Builder()
.url(apiURL + endpoint)
.addHeader("Authorization", authorizationHeader)
.delete(body)
.build()

return executeAndExtractBody(request)
}

/**
* Send a POST request to the API.
* @param endpoint endpoint of the API
Expand Down
34 changes: 30 additions & 4 deletions client/src/main/kotlin/dev/medzik/librepass/client/api/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import dev.medzik.librepass.client.utils.Cryptography.computeSecretKey
import dev.medzik.librepass.client.utils.Cryptography.computeSecretKeyFromPassword
import dev.medzik.librepass.client.utils.Cryptography.computeSharedKey
import dev.medzik.librepass.client.utils.JsonUtils
import dev.medzik.librepass.types.api.ChangePasswordCipherData
import dev.medzik.librepass.types.api.ChangePasswordRequest
import dev.medzik.librepass.types.api.SetupTwoFactorRequest
import dev.medzik.librepass.types.api.SetupTwoFactorResponse
import dev.medzik.librepass.types.api.*
import dev.medzik.librepass.utils.fromHexString
import dev.medzik.librepass.utils.toHexString

Expand Down Expand Up @@ -168,4 +165,33 @@ class UserClient(
)
return JsonUtils.deserialize(response)
}

@Throws(ClientException::class, ApiException::class)
fun deleteAccount(
password: String,
tfaCode: String? = null
) {
val preLogin = AuthClient(apiUrl).preLogin(email)

val passwordHash =
computePasswordHash(
email = email,
password = password,
argon2Function = preLogin.toArgon2()
)

val serverPublicKey = preLogin.serverPublicKey.fromHexString()
val sharedKey = computeSharedKey(passwordHash.hash, serverPublicKey)

val request =
DeleteAccountRequest(
sharedKey = sharedKey.toHexString(),
code = tfaCode
)

client.delete(
"$API_ENDPOINT/delete",
JsonUtils.serialize(request)
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.medzik.librepass.client.api

import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

Expand All @@ -17,6 +18,13 @@ class AuthClientTests {
// wait for 1 second to prevent unauthorized error
Thread.sleep(1000)
}

@AfterAll
@JvmStatic
fun delete() {
val credentials = authClient.login(EMAIL, PASSWORD)
UserClient(EMAIL, credentials.apiKey, API_URL).deleteAccount(PASSWORD)
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ class CipherClientTests {
cipherClient.delete(it.id)
}
}

@AfterAll
@JvmStatic
fun delete() {
val authClient = AuthClient(API_URL)
val credentials = authClient.login(EMAIL, PASSWORD)
UserClient(EMAIL, credentials.apiKey, API_URL).deleteAccount(PASSWORD)
}
}

private lateinit var secretKey: ByteArray
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.medzik.librepass.client.api

import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeAll
Expand All @@ -21,6 +22,14 @@ class CollectionClientTests {
// wait for 1 second to prevent unauthorized error
Thread.sleep(1000)
}

@AfterAll
@JvmStatic
fun delete() {
val authClient = AuthClient(API_URL)
val credentials = authClient.login(EMAIL, PASSWORD)
UserClient(EMAIL, credentials.apiKey, API_URL).deleteAccount(PASSWORD)
}
}

@BeforeEach
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.medzik.librepass.client.api

import dev.medzik.librepass.utils.TOTP
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
Expand All @@ -26,6 +27,17 @@ class TwoFactorTests {
val code = TOTP.getTOTPCode(twoFactorSecret)
UserClient(EMAIL, auth.apiKey, API_URL).setupTwoFactor(PASSWORD, twoFactorSecret, code)
}

@AfterAll
@JvmStatic
fun delete() {
val credentials = authClient.login(EMAIL, PASSWORD)

val code = TOTP.getTOTPCode(twoFactorSecret)
authClient.loginTwoFactor(credentials.apiKey, code)

UserClient(EMAIL, credentials.apiKey, API_URL).deleteAccount(PASSWORD, code)
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dev.medzik.librepass.types.cipher.CipherType
import dev.medzik.librepass.types.cipher.EncryptedCipher
import dev.medzik.librepass.types.cipher.data.CipherLoginData
import dev.medzik.librepass.utils.fromHexString
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
Expand All @@ -24,6 +25,14 @@ class UserClientTests {
// wait for 1 second to prevent unauthorized error
Thread.sleep(1000)
}

@AfterAll
@JvmStatic
fun delete() {
val authClient = AuthClient(API_URL)
val credentials = authClient.login(EMAIL, PASSWORD)
UserClient(EMAIL, credentials.apiKey, API_URL).deleteAccount(PASSWORD)
}
}

private lateinit var userId: UUID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import dev.medzik.librepass.responses.ResponseError
import dev.medzik.librepass.server.components.AuthorizedUser
import dev.medzik.librepass.server.controllers.advice.InvalidTwoFactorCodeException
import dev.medzik.librepass.server.database.CipherRepository
import dev.medzik.librepass.server.database.TokenRepository
import dev.medzik.librepass.server.database.UserRepository
import dev.medzik.librepass.server.database.UserTable
import dev.medzik.librepass.server.utils.Response
import dev.medzik.librepass.server.utils.ResponseHandler
import dev.medzik.librepass.server.utils.Validator.validateSharedKey
import dev.medzik.librepass.server.utils.toResponse
import dev.medzik.librepass.types.api.ChangePasswordRequest
import dev.medzik.librepass.types.api.DeleteAccountRequest
import dev.medzik.librepass.types.api.SetupTwoFactorRequest
import dev.medzik.librepass.types.api.SetupTwoFactorResponse
import dev.medzik.librepass.utils.TOTP
Expand All @@ -25,6 +27,7 @@ class UserController
@Autowired
constructor(
private val userRepository: UserRepository,
private val tokenRepository: TokenRepository,
private val cipherRepository: CipherRepository
) {
@PatchMapping("/password")
Expand Down Expand Up @@ -100,4 +103,22 @@ class UserController
val response = SetupTwoFactorResponse(recoveryCode = recoveryCode)
return ResponseHandler.generateResponse(response, HttpStatus.OK)
}

@DeleteMapping("/delete")
fun deleteAccount(
@AuthorizedUser user: UserTable,
@RequestBody body: DeleteAccountRequest
): Response {
if (!validateSharedKey(user, body.sharedKey))
return ResponseError.INVALID_CREDENTIALS.toResponse()

if (user.twoFactorEnabled && body.code != TOTP.getTOTPCode(user.twoFactorSecret!!))
throw InvalidTwoFactorCodeException()

tokenRepository.deleteAllByOwner(user.id)
cipherRepository.deleteAllByOwner(user.id)
userRepository.delete(user)

return ResponseHandler.generateResponse(HttpStatus.OK)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,9 @@ interface CipherRepository : CrudRepository<CipherTable, UUID> {
@Param("id") id: UUID,
@Param("data") data: String
)

/** Remove all tokens owned by the user */
@Transactional
@Modifying
fun deleteAllByOwner(owner: UUID)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ interface TokenRepository : CrudRepository<TokenTable, String> {
@Modifying
@Query("DELETE FROM #{#entityName} t WHERE t.lastUsed < :lastUsedBefore")
fun deleteUnused(lastUsedBefore: Date)

/** Remove all tokens owned by the user */
@Transactional
@Modifying
fun deleteAllByOwner(owner: UUID)
}
11 changes: 11 additions & 0 deletions shared/src/main/kotlin/dev/medzik/librepass/types/api/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ data class SetupTwoFactorRequest(
val code: String
)

/**
* Request for endpoint that setups two-factor authentication.
*
* @property sharedKey The shared key with server, to verify authentication.
* @property code The OTP code (if 2fa is set)
*/
data class DeleteAccountRequest(
val sharedKey: String,
val code: String?
)

/**
* Response from endpoint that setups two-factor authentication.
*
Expand Down

0 comments on commit 63cf411

Please sign in to comment.