diff --git a/client/src/main/kotlin/dev/medzik/librepass/client/Client.kt b/client/src/main/kotlin/dev/medzik/librepass/client/Client.kt index 28c761c5..01363b63 100644 --- a/client/src/main/kotlin/dev/medzik/librepass/client/Client.kt +++ b/client/src/main/kotlin/dev/medzik/librepass/client/Client.kt @@ -36,9 +36,10 @@ class Client( private val apiURL: String, private val accessToken: String? = null, ) { - private val httpClient = OkHttpClient.Builder() - .callTimeout(30, TimeUnit.SECONDS) - .build() + private val httpClient = + OkHttpClient.Builder() + .callTimeout(30, TimeUnit.SECONDS) + .build() private val httpMediaTypeJson = "application/json; charset=utf-8".toMediaType() // create authorization header if access token is provided @@ -51,11 +52,12 @@ class Client( */ @Throws(ClientException::class, ApiException::class) fun get(endpoint: String): String { - val request = Request.Builder() - .url(apiURL + endpoint) - .addHeader("Authorization", authorizationHeader) - .get() - .build() + val request = + Request.Builder() + .url(apiURL + endpoint) + .addHeader("Authorization", authorizationHeader) + .get() + .build() return executeAndExtractBody(request) } @@ -67,11 +69,12 @@ class Client( */ @Throws(ClientException::class, ApiException::class) fun delete(endpoint: String): String { - val request = Request.Builder() - .url(apiURL + endpoint) - .addHeader("Authorization", authorizationHeader) - .delete() - .build() + val request = + Request.Builder() + .url(apiURL + endpoint) + .addHeader("Authorization", authorizationHeader) + .delete() + .build() return executeAndExtractBody(request) } @@ -83,14 +86,18 @@ class Client( * @return response body */ @Throws(ClientException::class, ApiException::class) - fun post(endpoint: String, json: String): String { + fun post( + endpoint: String, + json: String + ): String { val body = json.toRequestBody(httpMediaTypeJson) - val request = Request.Builder() - .url(apiURL + endpoint) - .addHeader("Authorization", authorizationHeader) - .post(body) - .build() + val request = + Request.Builder() + .url(apiURL + endpoint) + .addHeader("Authorization", authorizationHeader) + .post(body) + .build() return executeAndExtractBody(request) } @@ -102,14 +109,18 @@ class Client( * @return response body */ @Throws(ClientException::class, ApiException::class) - fun patch(endpoint: String, json: String): String { + fun patch( + endpoint: String, + json: String + ): String { val body = json.toRequestBody(httpMediaTypeJson) - val request = Request.Builder() - .url(apiURL + endpoint) - .addHeader("Authorization", authorizationHeader) - .patch(body) - .build() + val request = + Request.Builder() + .url(apiURL + endpoint) + .addHeader("Authorization", authorizationHeader) + .patch(body) + .build() return executeAndExtractBody(request) } @@ -121,14 +132,18 @@ class Client( * @return response body */ @Throws(ClientException::class, ApiException::class) - fun put(endpoint: String, json: String): String { + fun put( + endpoint: String, + json: String + ): String { val body = json.toRequestBody(httpMediaTypeJson) - val request = Request.Builder() - .url(apiURL + endpoint) - .addHeader("Authorization", authorizationHeader) - .put(body) - .build() + val request = + Request.Builder() + .url(apiURL + endpoint) + .addHeader("Authorization", authorizationHeader) + .put(body) + .build() return executeAndExtractBody(request) } diff --git a/client/src/main/kotlin/dev/medzik/librepass/client/api/AuthClient.kt b/client/src/main/kotlin/dev/medzik/librepass/client/api/AuthClient.kt index b5126938..fe924699 100644 --- a/client/src/main/kotlin/dev/medzik/librepass/client/api/AuthClient.kt +++ b/client/src/main/kotlin/dev/medzik/librepass/client/api/AuthClient.kt @@ -28,7 +28,11 @@ class AuthClient(apiUrl: String = Server.PRODUCTION) { private val client = Client(apiUrl) @Throws(ClientException::class, ApiException::class) - fun register(email: String, password: String, passwordHint: String? = null) { + fun register( + email: String, + password: String, + passwordHint: String? = null + ) { val serverPreLogin = preLogin("") val passwordHash = computePasswordHash(password, email, serverPreLogin.toArgon2()) @@ -37,17 +41,18 @@ class AuthClient(apiUrl: String = Server.PRODUCTION) { // compute shared key val sharedKey = computeSharedKey(passwordHash.hash, serverPreLogin.serverPublicKey.fromHexString()) - val request = RegisterRequest( - email = email, - passwordHint = passwordHint, - sharedKey = sharedKey.toHexString(), - // Argon2id parameters - parallelism = passwordHash.parallelism, - memory = passwordHash.memory, - iterations = passwordHash.iterations, - // Curve25519 public key - publicKey = publicKey.toHexString() - ) + val request = + RegisterRequest( + email = email, + passwordHint = passwordHint, + sharedKey = sharedKey.toHexString(), + // Argon2id parameters + parallelism = passwordHash.parallelism, + memory = passwordHash.memory, + iterations = passwordHash.iterations, + // Curve25519 public key + publicKey = publicKey.toHexString() + ) client.post("$API_ENDPOINT/register", JsonUtils.serialize(request)) } @@ -59,29 +64,38 @@ class AuthClient(apiUrl: String = Server.PRODUCTION) { } @Throws(ClientException::class, ApiException::class) - fun login(email: String, password: String): UserCredentials { + fun login( + email: String, + password: String + ): UserCredentials { val preLoginData = preLogin(email) - val passwordHash = computePasswordHash( - password = password, - email = email, - argon2Function = preLoginData.toArgon2() - ) + val passwordHash = + computePasswordHash( + password = password, + email = email, + argon2Function = preLoginData.toArgon2() + ) return login(email, passwordHash, preLoginData) } @Throws(ClientException::class, ApiException::class) - fun login(email: String, passwordHash: Argon2Hash, preLogin: PreLoginResponse? = null): UserCredentials { + fun login( + email: String, + passwordHash: Argon2Hash, + preLogin: PreLoginResponse? = null + ): UserCredentials { val serverPublicKey = preLogin?.serverPublicKey ?: preLogin(email).serverPublicKey val publicKey = X25519.publicFromPrivate(passwordHash.hash) val sharedKey = computeSharedKey(passwordHash.hash, serverPublicKey.fromHexString()) - val request = LoginRequest( - email = email, - sharedKey = sharedKey.toHexString(), - ) + val request = + LoginRequest( + email = email, + sharedKey = sharedKey.toHexString(), + ) val responseBody = client.post("$API_ENDPOINT/oauth?grantType=login", JsonUtils.serialize(request)) val response = JsonUtils.deserialize(responseBody) @@ -99,11 +113,15 @@ class AuthClient(apiUrl: String = Server.PRODUCTION) { } @Throws(ClientException::class, ApiException::class) - fun loginTwoFactor(apiKey: String, code: String) { - val request = TwoFactorRequest( - apiKey = apiKey, - code = code - ) + fun loginTwoFactor( + apiKey: String, + code: String + ) { + val request = + TwoFactorRequest( + apiKey = apiKey, + code = code + ) client.post("$API_ENDPOINT/oauth?grantType=2fa", JsonUtils.serialize(request)) } diff --git a/client/src/main/kotlin/dev/medzik/librepass/client/api/CipherClient.kt b/client/src/main/kotlin/dev/medzik/librepass/client/api/CipherClient.kt index 6163b39b..f97f51b8 100644 --- a/client/src/main/kotlin/dev/medzik/librepass/client/api/CipherClient.kt +++ b/client/src/main/kotlin/dev/medzik/librepass/client/api/CipherClient.kt @@ -45,7 +45,10 @@ class CipherClient( * @return [IdResponse] */ @Throws(ClientException::class, ApiException::class) - fun insert(cipher: Cipher, secretKey: String): IdResponse { + fun insert( + cipher: Cipher, + secretKey: String + ): IdResponse { return insert( EncryptedCipher( cipher = cipher, @@ -114,7 +117,10 @@ class CipherClient( * @return [IdResponse] */ @Throws(ClientException::class, ApiException::class) - fun update(cipher: Cipher, secretKey: String): IdResponse { + fun update( + cipher: Cipher, + secretKey: String + ): IdResponse { return update( EncryptedCipher( cipher = cipher, diff --git a/client/src/main/kotlin/dev/medzik/librepass/client/api/CollectionClient.kt b/client/src/main/kotlin/dev/medzik/librepass/client/api/CollectionClient.kt index 19a90524..938dbe11 100644 --- a/client/src/main/kotlin/dev/medzik/librepass/client/api/CollectionClient.kt +++ b/client/src/main/kotlin/dev/medzik/librepass/client/api/CollectionClient.kt @@ -75,7 +75,10 @@ class CollectionClient( * @return [IdResponse] */ @Throws(ClientException::class, ApiException::class) - fun updateCollection(id: UUID, name: String): IdResponse { + fun updateCollection( + id: UUID, + name: String + ): IdResponse { return updateCollection(id.toString(), name) } @@ -86,7 +89,10 @@ class CollectionClient( * @return [IdResponse] */ @Throws(ClientException::class, ApiException::class) - fun updateCollection(id: String, name: String): IdResponse { + fun updateCollection( + id: String, + name: String + ): IdResponse { val request = CreateCollectionRequest(name = name) val response = client.patch("$API_ENDPOINT/$id", JsonUtils.serialize(request)) return JsonUtils.deserialize(response) diff --git a/client/src/main/kotlin/dev/medzik/librepass/client/api/UserClient.kt b/client/src/main/kotlin/dev/medzik/librepass/client/api/UserClient.kt index 8a5954ec..2fe84271 100644 --- a/client/src/main/kotlin/dev/medzik/librepass/client/api/UserClient.kt +++ b/client/src/main/kotlin/dev/medzik/librepass/client/api/UserClient.kt @@ -53,24 +53,27 @@ class UserClient( val oldPreLogin = AuthClient(apiUrl).preLogin(email) // compute old secret key - val oldSecretKey = computeSecretKeyFromPassword( - email = email, - password = oldPassword, - argon2Function = oldPreLogin.toArgon2() - ) + val oldSecretKey = + computeSecretKeyFromPassword( + email = email, + password = oldPassword, + argon2Function = oldPreLogin.toArgon2() + ) - val oldPasswordHash = computePasswordHash( - email = email, - password = oldPassword, - argon2Function = oldPreLogin.toArgon2() - ) + val oldPasswordHash = + computePasswordHash( + email = email, + password = oldPassword, + argon2Function = oldPreLogin.toArgon2() + ) // compute new password hashes - val newPasswordHash = computePasswordHash( - email = email, - password = newPassword, - argon2Function = argon2Function - ) + val newPasswordHash = + computePasswordHash( + email = email, + password = newPassword, + argon2Function = argon2Function + ) val newPublicKey = X25519.publicFromPrivate(newPasswordHash.hash) @@ -96,25 +99,27 @@ class UserClient( // encrypt cipher data with a new secret key val newData = Aes.encrypt(Aes.GCM, newSecretKey, oldData) - ciphers += ChangePasswordCipherData( - id = cipher.id, - data = newData - ) + ciphers += + ChangePasswordCipherData( + id = cipher.id, + data = newData + ) } - val request = ChangePasswordRequest( - oldSharedKey = oldSharedKey.toHexString(), - newPasswordHint = newPasswordHint, - newSharedKey = newSharedKey.toHexString(), - // Argon2id parameters - parallelism = newPasswordHash.parallelism, - memory = newPasswordHash.memory, - iterations = newPasswordHash.iterations, - // Curve25519 public key - newPublicKey = newPublicKey.toHexString(), - // ciphers data re-encrypted with new password - ciphers = ciphers - ) + val request = + ChangePasswordRequest( + oldSharedKey = oldSharedKey.toHexString(), + newPasswordHint = newPasswordHint, + newSharedKey = newSharedKey.toHexString(), + // Argon2id parameters + parallelism = newPasswordHash.parallelism, + memory = newPasswordHash.memory, + iterations = newPasswordHash.iterations, + // Curve25519 public key + newPublicKey = newPublicKey.toHexString(), + // ciphers data re-encrypted with new password + ciphers = ciphers + ) client.patch( "$API_ENDPOINT/password", @@ -131,25 +136,28 @@ class UserClient( ): SetupTwoFactorResponse { val preLogin = AuthClient(apiUrl).preLogin(email) - val passwordHash = computePasswordHash( - email = email, - password = password, - argon2Function = preLogin.toArgon2() - ) + val passwordHash = + computePasswordHash( + email = email, + password = password, + argon2Function = preLogin.toArgon2() + ) val serverPublicKey = preLogin.serverPublicKey.fromHexString() val sharedKey = computeSharedKey(passwordHash.hash, serverPublicKey) - val request = SetupTwoFactorRequest( - sharedKey = sharedKey.toHexString(), - secret = secret, - code = code - ) + val request = + SetupTwoFactorRequest( + sharedKey = sharedKey.toHexString(), + secret = secret, + code = code + ) - val response = client.post( - "$API_ENDPOINT/setup/2fa", - JsonUtils.serialize(request) - ) + val response = + client.post( + "$API_ENDPOINT/setup/2fa", + JsonUtils.serialize(request) + ) return JsonUtils.deserialize(response) } } diff --git a/client/src/main/kotlin/dev/medzik/librepass/client/utils/Cryptography.kt b/client/src/main/kotlin/dev/medzik/librepass/client/utils/Cryptography.kt index 2f3aad7e..334354bb 100644 --- a/client/src/main/kotlin/dev/medzik/librepass/client/utils/Cryptography.kt +++ b/client/src/main/kotlin/dev/medzik/librepass/client/utils/Cryptography.kt @@ -16,7 +16,10 @@ object Cryptography { * Used for AES encryption. */ @JvmStatic - fun computeSharedKey(privateKey: ByteArray, publicKey: ByteArray): ByteArray { + fun computeSharedKey( + privateKey: ByteArray, + publicKey: ByteArray + ): ByteArray { return X25519.computeSharedSecret(privateKey, publicKey) } @@ -33,7 +36,11 @@ object Cryptography { /** Compute secret key from password. */ @JvmStatic - fun computeSecretKeyFromPassword(email: String, password: String, argon2Function: Argon2): ByteArray { + fun computeSecretKeyFromPassword( + email: String, + password: String, + argon2Function: Argon2 + ): ByteArray { val passwordHash = computePasswordHash(password, email, argon2Function) return computeSecretKey(passwordHash.hash) } diff --git a/client/src/main/kotlin/dev/medzik/librepass/client/utils/JsonUtils.kt b/client/src/main/kotlin/dev/medzik/librepass/client/utils/JsonUtils.kt index bdeb855b..b98922b8 100644 --- a/client/src/main/kotlin/dev/medzik/librepass/client/utils/JsonUtils.kt +++ b/client/src/main/kotlin/dev/medzik/librepass/client/utils/JsonUtils.kt @@ -7,12 +7,10 @@ object JsonUtils { /** * Serialize an object to JSON string. */ - fun serialize(data: Any): String = - Gson().toJson(data) + fun serialize(data: Any): String = Gson().toJson(data) /** * Deserialize JSON string to object. */ - inline fun deserialize(data: String): T = - Gson().fromJson(data, object : TypeToken() {}.type) + inline fun deserialize(data: String): T = Gson().fromJson(data, object : TypeToken() {}.type) } diff --git a/client/src/test/kotlin/dev/medzik/librepass/client/api/CipherClientTests.kt b/client/src/test/kotlin/dev/medzik/librepass/client/api/CipherClientTests.kt index e1ce4274..573f8fa4 100644 --- a/client/src/test/kotlin/dev/medzik/librepass/client/api/CipherClientTests.kt +++ b/client/src/test/kotlin/dev/medzik/librepass/client/api/CipherClientTests.kt @@ -59,14 +59,16 @@ class CipherClientTests { @Test fun insertCipher() { - val cipher = Cipher( - id = UUID.randomUUID(), - owner = userId, - type = CipherType.Login, - loginData = CipherLoginData( - name = "test_cipher" + val cipher = + Cipher( + id = UUID.randomUUID(), + owner = userId, + type = CipherType.Login, + loginData = + CipherLoginData( + name = "test_cipher" + ) ) - ) val response = cipherClient.insert(cipher, secretKey) diff --git a/client/src/test/kotlin/dev/medzik/librepass/client/api/UserClientTests.kt b/client/src/test/kotlin/dev/medzik/librepass/client/api/UserClientTests.kt index 1168bbc7..7afa3860 100644 --- a/client/src/test/kotlin/dev/medzik/librepass/client/api/UserClientTests.kt +++ b/client/src/test/kotlin/dev/medzik/librepass/client/api/UserClientTests.kt @@ -42,16 +42,18 @@ class UserClientTests { @Test fun changePassword() { - val testCipher = Cipher( - id = UUID.randomUUID(), - owner = userId, - type = CipherType.Login, - loginData = CipherLoginData( - name = "test", - username = "test", - password = "test" + val testCipher = + Cipher( + id = UUID.randomUUID(), + owner = userId, + type = CipherType.Login, + loginData = + CipherLoginData( + name = "test", + username = "test", + password = "test" + ) ) - ) fun insertTestCipher() { cipherClient.insert(EncryptedCipher(testCipher, secretKey)) diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/WebConfig.kt b/server/src/main/kotlin/dev/medzik/librepass/server/WebConfig.kt index 72275ceb..aedc80d1 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/WebConfig.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/WebConfig.kt @@ -10,30 +10,32 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration -class WebConfig @Autowired constructor( - // For @AuthorizedUser annotation - private val authorizedUserArgumentResolver: AuthorizedUserArgumentResolver, - // For @RequestIP annotation - private val requestIPArgumentResolver: RequestIPArgumentResolver -) : WebMvcConfigurer { - override fun addArgumentResolvers(resolvers: MutableList) { - // Add @AuthorizedUser annotation support - resolvers.add(authorizedUserArgumentResolver) - // Add @RequestIP annotation support - resolvers.add(requestIPArgumentResolver) - } +class WebConfig + @Autowired + constructor( + // For @AuthorizedUser annotation + private val authorizedUserArgumentResolver: AuthorizedUserArgumentResolver, + // For @RequestIP annotation + private val requestIPArgumentResolver: RequestIPArgumentResolver + ) : WebMvcConfigurer { + override fun addArgumentResolvers(resolvers: MutableList) { + // Add @AuthorizedUser annotation support + resolvers.add(authorizedUserArgumentResolver) + // Add @RequestIP annotation support + resolvers.add(requestIPArgumentResolver) + } - // CORS configuration - @Value("\${server.cors.allowedOrigins}") - private lateinit var allowedOrigins: String + // CORS configuration + @Value("\${server.cors.allowedOrigins}") + private lateinit var allowedOrigins: String - override fun addCorsMappings(registry: CorsRegistry) { - val allowedOrigin = allowedOrigins.split(",").toTypedArray() + override fun addCorsMappings(registry: CorsRegistry) { + val allowedOrigin = allowedOrigins.split(",").toTypedArray() - registry.addMapping("/**") - .allowedOrigins(*allowedOrigin) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true) + registry.addMapping("/**") + .allowedOrigins(*allowedOrigin) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/components/AuthorizedUser.kt b/server/src/main/kotlin/dev/medzik/librepass/server/components/AuthorizedUser.kt index fe67ac4e..212b5998 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/components/AuthorizedUser.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/components/AuthorizedUser.kt @@ -27,74 +27,82 @@ import java.util.* annotation class AuthorizedUser @Component -class AuthorizedUserArgumentResolver @Autowired constructor( - private val userRepository: UserRepository, - private val tokenRepository: TokenRepository, - @Value("\${http.ip.header}") private val ipHeader: String -) : HandlerMethodArgumentResolver { - override fun supportsParameter(parameter: MethodParameter): Boolean { - return parameter.hasParameterAnnotation(AuthorizedUser::class.java) - } +class AuthorizedUserArgumentResolver + @Autowired + constructor( + private val userRepository: UserRepository, + private val tokenRepository: TokenRepository, + @Value("\${http.ip.header}") private val ipHeader: String + ) : HandlerMethodArgumentResolver { + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(AuthorizedUser::class.java) + } - override fun resolveArgument( - parameter: MethodParameter, - mavContainer: ModelAndViewContainer?, - webRequest: NativeWebRequest, - binderFactory: WebDataBinderFactory? - ): UserTable { - val request = webRequest.getNativeRequest(HttpServletRequest::class.java) - ?: throw Exception("Failed to get native HttpServletRequest") + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): UserTable { + val request = + webRequest.getNativeRequest(HttpServletRequest::class.java) + ?: throw Exception("Failed to get native HttpServletRequest") - val authorizationHeader = request.getHeader("Authorization") - ?: throw AuthorizedUserException() - val token = authorizationHeader.removePrefix("Bearer ") + val authorizationHeader = + request.getHeader("Authorization") + ?: throw AuthorizedUserException() + val token = authorizationHeader.removePrefix("Bearer ") - val tokenTable = tokenRepository.findByIdOrNull(token) - ?: throw AuthorizedUserException() + val tokenTable = + tokenRepository.findByIdOrNull(token) + ?: throw AuthorizedUserException() - val user = userRepository - .findById(tokenTable.owner) - .orElse(null) - ?: throw AuthorizedUserException() + val user = + userRepository + .findById(tokenTable.owner) + .orElse(null) + ?: throw AuthorizedUserException() - // TODO: Expire inactive tokens after some time. + // TODO: Expire inactive tokens after some time. - // check if user changed password after the token was created - if (user.lastPasswordChange > tokenTable.created) - throw AuthorizedUserException() + // check if user changed password after the token was created + if (user.lastPasswordChange > tokenTable.created) + throw AuthorizedUserException() - val ip = if (ipHeader == "remoteAddr") - request.remoteAddr - else request.getHeader(ipHeader) - ?: request.remoteAddr + val ip = + if (ipHeader == "remoteAddr") + request.remoteAddr + else + request.getHeader(ipHeader) + ?: request.remoteAddr - // check if the IP address has been changed - // or if 5 minutes elapsed since the date of last use - val currentDate = Date() - if (tokenTable.lastIp != ip || - checkIfElapsed(tokenTable.lastUsed, currentDate, 5) - ) { - tokenRepository.save( - tokenTable.copy( - lastIp = ip, - lastUsed = currentDate + // check if the IP address has been changed + // or if 5 minutes elapsed since the date of last use + val currentDate = Date() + if (tokenTable.lastIp != ip || + checkIfElapsed(tokenTable.lastUsed, currentDate, 5) + ) { + tokenRepository.save( + tokenTable.copy( + lastIp = ip, + lastUsed = currentDate + ) ) - ) - } + } - return user - } + return user + } - /** - * Delete expired tokens every 30 minutes. - */ - @Scheduled(cron = "0 */30 * ? * *") - fun deleteExpiredTokens() { - val currentDate = Date() - val date30DaysAgo = Date(currentDate.time - 1000L * 60 * 60 * 24 * 30) + /** + * Delete expired tokens every 30 minutes. + */ + @Scheduled(cron = "0 */30 * ? * *") + fun deleteExpiredTokens() { + val currentDate = Date() + val date30DaysAgo = Date(currentDate.time - 1000L * 60 * 60 * 24 * 30) - tokenRepository.findAllByLastUsedBefore(date30DaysAgo).forEach { token -> - tokenRepository.delete(token) + tokenRepository.findAllByLastUsedBefore(date30DaysAgo).forEach { token -> + tokenRepository.delete(token) + } } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/components/RequestIP.kt b/server/src/main/kotlin/dev/medzik/librepass/server/components/RequestIP.kt index e56d6fc5..58a1667d 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/components/RequestIP.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/components/RequestIP.kt @@ -19,25 +19,29 @@ import org.springframework.web.method.support.ModelAndViewContainer annotation class RequestIP @Component -class RequestIPArgumentResolver @Autowired constructor( - @Value("\${http.ip.header}") private val ipHeader: String -) : HandlerMethodArgumentResolver { - override fun supportsParameter(parameter: MethodParameter): Boolean { - return parameter.hasParameterAnnotation(RequestIP::class.java) - } +class RequestIPArgumentResolver + @Autowired + constructor( + @Value("\${http.ip.header}") private val ipHeader: String + ) : HandlerMethodArgumentResolver { + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(RequestIP::class.java) + } - override fun resolveArgument( - parameter: MethodParameter, - mavContainer: ModelAndViewContainer?, - webRequest: NativeWebRequest, - binderFactory: WebDataBinderFactory? - ): String? { - val request = webRequest.getNativeRequest(HttpServletRequest::class.java) - ?: return null + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): String? { + val request = + webRequest.getNativeRequest(HttpServletRequest::class.java) + ?: return null - return if (ipHeader == "remoteAddr") - request.remoteAddr - else request.getHeader(ipHeader) - ?: request.remoteAddr + return if (ipHeader == "remoteAddr") + request.remoteAddr + else + request.getHeader(ipHeader) + ?: request.remoteAddr + } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/advice/CustomExceptions.kt b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/advice/CustomExceptions.kt index bb94dffc..6cae6a59 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/advice/CustomExceptions.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/advice/CustomExceptions.kt @@ -6,7 +6,9 @@ import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler class AuthorizedUserException : RuntimeException() + class InvalidTwoFactorCodeException : RuntimeException() + class RateLimitException : RuntimeException() @ControllerAdvice diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/AuthController.kt b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/AuthController.kt index cbdea858..bb6804e4 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/AuthController.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/AuthController.kt @@ -61,269 +61,285 @@ class AuthRateLimitConfig { @RestController @RequestMapping("/api/auth") -class AuthController @Autowired constructor( - private val userRepository: UserRepository, - private val tokenRepository: TokenRepository, - private val emailService: EmailService, - @Value("\${server.api.rateLimit.enabled}") - private val rateLimitEnabled: Boolean, - @Value("\${email.verification.required}") - private val emailVerificationRequired: Boolean, - @Value("\${web.url}") - private val webUrl: String -) { - private val logger = LoggerFactory.getLogger(this::class.java) - private val rateLimit = AuthRateLimitConfig() - private val coroutineScope = CoroutineScope(Dispatchers.IO) - - @PostMapping("/register") - fun register( - @RequestIP ip: String, - @RequestBody request: RegisterRequest - ): Response { - consumeRateLimit(ip) - - if (!Validator.emailValidator(request.email) || - !Validator.hexValidator(request.sharedKey, 32) || - !Validator.hexValidator(request.publicKey, 32) || - request.parallelism < 0 || - request.memory < 19 * 1024 || - request.iterations < 0 - ) return ResponseError.INVALID_BODY.toResponse() - - val sharedKey = X25519.computeSharedSecret(ServerPrivateKey, request.publicKey.fromHexString()) - if (!request.sharedKey.fromHexString().contentEquals(sharedKey)) - return ResponseError.INVALID_CREDENTIALS.toResponse() - - val verificationToken = Random.randBytes(16).toHexString() - - val user = userRepository.save( - UserTable( - email = request.email.lowercase(), - passwordHint = request.passwordHint, - // Argon2id parameters - parallelism = request.parallelism, - memory = request.memory, - iterations = request.iterations, - // Curve25519 key pair - publicKey = request.publicKey, - // Email verification token - emailVerificationCode = verificationToken, - emailVerificationCodeExpiresAt = Date.from( - Calendar.getInstance().apply { - add(Calendar.HOUR, 24) - }.toInstant() - ) +class AuthController + @Autowired + constructor( + private val userRepository: UserRepository, + private val tokenRepository: TokenRepository, + private val emailService: EmailService, + @Value("\${server.api.rateLimit.enabled}") + private val rateLimitEnabled: Boolean, + @Value("\${email.verification.required}") + private val emailVerificationRequired: Boolean, + @Value("\${web.url}") + private val webUrl: String + ) { + private val logger = LoggerFactory.getLogger(this::class.java) + private val rateLimit = AuthRateLimitConfig() + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + @PostMapping("/register") + fun register( + @RequestIP ip: String, + @RequestBody request: RegisterRequest + ): Response { + consumeRateLimit(ip) + + if (!Validator.emailValidator(request.email) || + !Validator.hexValidator(request.sharedKey, 32) || + !Validator.hexValidator(request.publicKey, 32) || + request.parallelism < 0 || + request.memory < 19 * 1024 || + request.iterations < 0 ) - ) - - coroutineScope.launch { - try { - emailService.sendEmailVerification( - to = request.email, - user = user.id.toString(), - code = user.emailVerificationCode.toString() + return ResponseError.INVALID_BODY.toResponse() + + val sharedKey = X25519.computeSharedSecret(ServerPrivateKey, request.publicKey.fromHexString()) + if (!request.sharedKey.fromHexString().contentEquals(sharedKey)) + return ResponseError.INVALID_CREDENTIALS.toResponse() + + val verificationToken = Random.randBytes(16).toHexString() + + val user = + userRepository.save( + UserTable( + email = request.email.lowercase(), + passwordHint = request.passwordHint, + // Argon2id parameters + parallelism = request.parallelism, + memory = request.memory, + iterations = request.iterations, + // Curve25519 key pair + publicKey = request.publicKey, + // Email verification token + emailVerificationCode = verificationToken, + emailVerificationCodeExpiresAt = + Date.from( + Calendar.getInstance().apply { + add(Calendar.HOUR, 24) + }.toInstant() + ) + ) ) - } catch (e: Throwable) { - logger.error("Error sending email verification", e) + + coroutineScope.launch { + try { + emailService.sendEmailVerification( + to = request.email, + user = user.id.toString(), + code = user.emailVerificationCode.toString() + ) + } catch (e: Throwable) { + logger.error("Error sending email verification", e) + } } + + return ResponseHandler.generateResponse(HttpStatus.CREATED) } - return ResponseHandler.generateResponse(HttpStatus.CREATED) - } + @GetMapping("/preLogin") + fun preLogin( + @RequestIP ip: String, + @RequestParam("email") email: String? + ): Response { + consumeRateLimit(ip) + + if (email.isNullOrEmpty()) + return ResponseHandler.generateResponse( + PreLoginResponse( + // Default argon2id parameters + parallelism = 3, + memory = 65536, // 64MB + iterations = 4, + // Server Curve25519 public key + serverPublicKey = ServerPublicKey.toHexString() + ), + HttpStatus.OK + ) - @GetMapping("/preLogin") - fun preLogin( - @RequestIP ip: String, - @RequestParam("email") email: String? - ): Response { - consumeRateLimit(ip) + val user = + userRepository.findByEmail(email.lowercase()) + ?: return ResponseError.INVALID_CREDENTIALS.toResponse() - if (email.isNullOrEmpty()) return ResponseHandler.generateResponse( PreLoginResponse( - // Default argon2id parameters - parallelism = 3, - memory = 65536, // 64MB - iterations = 4, + parallelism = user.parallelism, + memory = user.memory, + iterations = user.iterations, // Server Curve25519 public key serverPublicKey = ServerPublicKey.toHexString() ), HttpStatus.OK ) + } - val user = userRepository.findByEmail(email.lowercase()) - ?: return ResponseError.INVALID_CREDENTIALS.toResponse() - - return ResponseHandler.generateResponse( - PreLoginResponse( - parallelism = user.parallelism, - memory = user.memory, - iterations = user.iterations, - // Server Curve25519 public key - serverPublicKey = ServerPublicKey.toHexString() - ), - HttpStatus.OK - ) - } - - @PostMapping("/oauth") - fun auth( - @RequestIP ip: String, - @RequestParam("grantType") grantType: String, - @RequestBody request: String - ): Response { - consumeRateLimit(ip) - - when (grantType) { - "login" -> { - try { - return oauthLogin( - ip = ip, - request = Gson().fromJson(request, LoginRequest::class.java) - ) - } catch (e: JsonSyntaxException) { - ResponseError.INVALID_CREDENTIALS.toResponse() + @PostMapping("/oauth") + fun auth( + @RequestIP ip: String, + @RequestParam("grantType") grantType: String, + @RequestBody request: String + ): Response { + consumeRateLimit(ip) + + when (grantType) { + "login" -> { + try { + return oauthLogin( + ip = ip, + request = Gson().fromJson(request, LoginRequest::class.java) + ) + } catch (e: JsonSyntaxException) { + ResponseError.INVALID_CREDENTIALS.toResponse() + } } - } - "2fa" -> { - try { - return oauth2FA( - request = Gson().fromJson(request, TwoFactorRequest::class.java) - ) - } catch (e: JsonSyntaxException) { - ResponseError.INVALID_CREDENTIALS.toResponse() + "2fa" -> { + try { + return oauth2FA( + request = Gson().fromJson(request, TwoFactorRequest::class.java) + ) + } catch (e: JsonSyntaxException) { + ResponseError.INVALID_CREDENTIALS.toResponse() + } } } - } - - return ResponseError.INVALID_BODY.toResponse() - } - - private fun oauthLogin(ip: String, request: LoginRequest): Response { - consumeRateLimit(request.email.lowercase()) - val user = userRepository.findByEmail(request.email.lowercase()) - ?: return ResponseError.INVALID_CREDENTIALS.toResponse() - - if (emailVerificationRequired && !user.emailVerified) - return ResponseError.EMAIL_NOT_VERIFIED.toResponse() - - val sharedKey = X25519.computeSharedSecret(ServerPrivateKey, user.publicKey.fromHexString()) - if (!request.sharedKey.fromHexString().contentEquals(sharedKey)) - return ResponseError.INVALID_CREDENTIALS.toResponse() + return ResponseError.INVALID_BODY.toResponse() + } - val apiToken = tokenRepository.save( - TokenTable( - owner = user.id, - lastIp = ip, - // Allow use of an api key only if two-factor authentication has been successful - confirmed = !user.twoFactorEnabled - ) - ) + private fun oauthLogin( + ip: String, + request: LoginRequest + ): Response { + consumeRateLimit(request.email.lowercase()) + + val user = + userRepository.findByEmail(request.email.lowercase()) + ?: return ResponseError.INVALID_CREDENTIALS.toResponse() + + if (emailVerificationRequired && !user.emailVerified) + return ResponseError.EMAIL_NOT_VERIFIED.toResponse() + + val sharedKey = X25519.computeSharedSecret(ServerPrivateKey, user.publicKey.fromHexString()) + if (!request.sharedKey.fromHexString().contentEquals(sharedKey)) + return ResponseError.INVALID_CREDENTIALS.toResponse() + + val apiToken = + tokenRepository.save( + TokenTable( + owner = user.id, + lastIp = ip, + // Allow use of an api key only if two-factor authentication has been successful + confirmed = !user.twoFactorEnabled + ) + ) - return ResponseHandler.generateResponse( - UserCredentialsResponse( - userId = user.id, - apiKey = apiToken.token, - verified = apiToken.confirmed + return ResponseHandler.generateResponse( + UserCredentialsResponse( + userId = user.id, + apiKey = apiToken.token, + verified = apiToken.confirmed + ) ) - ) - } - - private fun oauth2FA(request: TwoFactorRequest): Response { - consumeRateLimit(request.apiKey) - - val token = tokenRepository.findByIdOrNull(request.apiKey) - ?: throw AuthorizedUserException() - - if (token.confirmed) - return ResponseHandler.generateResponse(HttpStatus.OK) - - val user = userRepository.findByIdOrNull(token.owner) - ?: throw UnsupportedOperationException() - - if (user.twoFactorSecret == null) - return ResponseHandler.generateResponse(HttpStatus.OK) - - consumeRateLimit(user.email) + } - if (!TOTP.validate(user.twoFactorSecret, request.code) && - request.code != user.twoFactorRecoveryCode - ) throw InvalidTwoFactorCodeException() + private fun oauth2FA(request: TwoFactorRequest): Response { + consumeRateLimit(request.apiKey) - tokenRepository.save(token.copy(confirmed = true)) + val token = + tokenRepository.findByIdOrNull(request.apiKey) + ?: throw AuthorizedUserException() - return ResponseHandler.generateResponse(HttpStatus.OK) - } + if (token.confirmed) + return ResponseHandler.generateResponse(HttpStatus.OK) - @GetMapping("/passwordHint") - fun requestPasswordHint( - @RequestIP ip: String, - @RequestParam("email") email: String? - ): Response { - if (email == null) - return ResponseError.INVALID_BODY.toResponse() + val user = + userRepository.findByIdOrNull(token.owner) + ?: throw UnsupportedOperationException() - consumeRateLimit(ip) - consumeRateLimit(email) + if (user.twoFactorSecret == null) + return ResponseHandler.generateResponse(HttpStatus.OK) - val user = userRepository.findByEmail(email) - ?: return ResponseError.INVALID_CREDENTIALS.toResponse() + consumeRateLimit(user.email) - try { - emailService.sendPasswordHint( - to = user.email, - hint = user.passwordHint + if (!TOTP.validate(user.twoFactorSecret, request.code) && + request.code != user.twoFactorRecoveryCode ) - } catch (e: Exception) { - logger.error("Failed to send password hint", e) + throw InvalidTwoFactorCodeException() - return ResponseError.UNEXPECTED_SERVER_ERROR.toResponse() + tokenRepository.save(token.copy(confirmed = true)) + + return ResponseHandler.generateResponse(HttpStatus.OK) } - return ResponseHandler.generateResponse(HttpStatus.OK) - } + @GetMapping("/passwordHint") + fun requestPasswordHint( + @RequestIP ip: String, + @RequestParam("email") email: String? + ): Response { + if (email == null) + return ResponseError.INVALID_BODY.toResponse() - @GetMapping("/verifyEmail") - fun verifyEmail( - @RequestIP ip: String, - @RequestParam("user") userID: String, - @RequestParam("code") verificationCode: String - ): Response { - consumeRateLimit(ip) - consumeRateLimit(userID) + consumeRateLimit(ip) + consumeRateLimit(email) - val user = userRepository.findById(UUID.fromString(userID)).orElse(null) - ?: return ResponseError.INVALID_BODY.toResponse() + val user = + userRepository.findByEmail(email) + ?: return ResponseError.INVALID_CREDENTIALS.toResponse() - // check if user email is already verified - if (user.emailVerified) - return ResponseHandler.redirectResponse("$webUrl/verification/email") + try { + emailService.sendPasswordHint( + to = user.email, + hint = user.passwordHint + ) + } catch (e: Exception) { + logger.error("Failed to send password hint", e) - // check if the code is valid - if (user.emailVerificationCode.toString() != verificationCode) - return ResponseError.INVALID_BODY.toResponse() + return ResponseError.UNEXPECTED_SERVER_ERROR.toResponse() + } - // check if the code is expired - if (user.emailVerificationCodeExpiresAt?.before(Date()) == true) - return ResponseError.INVALID_BODY.toResponse() + return ResponseHandler.generateResponse(HttpStatus.OK) + } - // set email as verified - userRepository.save( - user.copy( - emailVerified = true, - emailVerificationCode = null + @GetMapping("/verifyEmail") + fun verifyEmail( + @RequestIP ip: String, + @RequestParam("user") userID: String, + @RequestParam("code") verificationCode: String + ): Response { + consumeRateLimit(ip) + consumeRateLimit(userID) + + val user = + userRepository.findById(UUID.fromString(userID)).orElse(null) + ?: return ResponseError.INVALID_BODY.toResponse() + + // check if user email is already verified + if (user.emailVerified) + return ResponseHandler.redirectResponse("$webUrl/verification/email") + + // check if the code is valid + if (user.emailVerificationCode.toString() != verificationCode) + return ResponseError.INVALID_BODY.toResponse() + + // check if the code is expired + if (user.emailVerificationCodeExpiresAt?.before(Date()) == true) + return ResponseError.INVALID_BODY.toResponse() + + // set email as verified + userRepository.save( + user.copy( + emailVerified = true, + emailVerificationCode = null + ) ) - ) - return ResponseHandler.redirectResponse("$webUrl/verification/email") - } + return ResponseHandler.redirectResponse("$webUrl/verification/email") + } - private fun consumeRateLimit(key: String) { - if (!rateLimitEnabled) return + private fun consumeRateLimit(key: String) { + if (!rateLimitEnabled) return - if (!rateLimit.resolveBucket(key).tryConsume(1)) - throw RateLimitException() + if (!rateLimit.resolveBucket(key).tryConsume(1)) + throw RateLimitException() + } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CipherController.kt b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CipherController.kt index afecb31a..4dedc85a 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CipherController.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CipherController.kt @@ -20,133 +20,143 @@ import java.util.* @RestController @RequestMapping("/api/cipher") -class CipherController @Autowired constructor( - private val cipherRepository: CipherRepository -) { - @PutMapping - fun insertCipher( - @AuthorizedUser user: UserTable, - @RequestBody encryptedCipher: EncryptedCipher - ): Response { - if (!Validator.hexValidator(encryptedCipher.protectedData) || - encryptedCipher.owner != user.id - ) return ResponseError.INVALID_BODY.toResponse() - - val cipher = cipherRepository.save(CipherTable(encryptedCipher)) - - return ResponseHandler.generateResponse( - IdResponse(cipher.id), - HttpStatus.CREATED - ) - } +class CipherController + @Autowired + constructor( + private val cipherRepository: CipherRepository + ) { + @PutMapping + fun insertCipher( + @AuthorizedUser user: UserTable, + @RequestBody encryptedCipher: EncryptedCipher + ): Response { + if (!Validator.hexValidator(encryptedCipher.protectedData) || + encryptedCipher.owner != user.id + ) + return ResponseError.INVALID_BODY.toResponse() + + val cipher = cipherRepository.save(CipherTable(encryptedCipher)) + + return ResponseHandler.generateResponse( + IdResponse(cipher.id), + HttpStatus.CREATED + ) + } - @GetMapping - fun getAllCiphers(@AuthorizedUser user: UserTable): Response { - val ciphers = cipherRepository.getAll(owner = user.id) + @GetMapping + fun getAllCiphers( + @AuthorizedUser user: UserTable + ): Response { + val ciphers = cipherRepository.getAll(owner = user.id) - // convert to encrypted ciphers - val response = ciphers.map { it.toEncryptedCipher() } + // convert to encrypted ciphers + val response = ciphers.map { it.toEncryptedCipher() } - return ResponseHandler.generateResponse(response, HttpStatus.OK) - } + return ResponseHandler.generateResponse(response, HttpStatus.OK) + } - @GetMapping("/sync") - fun syncCiphers( - @AuthorizedUser user: UserTable, - @RequestParam("lastSync") lastSyncUnixTimestamp: Long - ): Response { - // convert timestamp to date - val lastSyncDate = Date(lastSyncUnixTimestamp * 1000) - - val ciphers = cipherRepository.getAll(owner = user.id) - - val syncResponse = SyncResponse( - // get ids of all ciphers - ids = ciphers.map { it.id }, - // get all ciphers that were updated after timestamp - ciphers = ciphers - // get all ciphers that were updated after timestamp - .filter { it.lastModified.after(lastSyncDate) } - // convert to encrypted ciphers - .map { it.toEncryptedCipher() } - ) - - return ResponseHandler.generateResponse(syncResponse, HttpStatus.OK) - } + @GetMapping("/sync") + fun syncCiphers( + @AuthorizedUser user: UserTable, + @RequestParam("lastSync") lastSyncUnixTimestamp: Long + ): Response { + // convert timestamp to date + val lastSyncDate = Date(lastSyncUnixTimestamp * 1000) + + val ciphers = cipherRepository.getAll(owner = user.id) + + val syncResponse = + SyncResponse( + // get ids of all ciphers + ids = ciphers.map { it.id }, + // get all ciphers that were updated after timestamp + ciphers = + ciphers + // get all ciphers that were updated after timestamp + .filter { it.lastModified.after(lastSyncDate) } + // convert to encrypted ciphers + .map { it.toEncryptedCipher() } + ) + + return ResponseHandler.generateResponse(syncResponse, HttpStatus.OK) + } - @GetMapping("/{id}") - fun getCipher( - @AuthorizedUser user: UserTable, - @PathVariable id: UUID - ): Response { - if (!checkIfCipherExistsAndOwnedBy(id, user.id)) - return ResponseError.NOT_FOUND.toResponse() + @GetMapping("/{id}") + fun getCipher( + @AuthorizedUser user: UserTable, + @PathVariable id: UUID + ): Response { + if (!checkIfCipherExistsAndOwnedBy(id, user.id)) + return ResponseError.NOT_FOUND.toResponse() - val cipher = cipherRepository.findById(id).get() + val cipher = cipherRepository.findById(id).get() - // convert to encrypted cipher - val encryptedCipher = cipher.toEncryptedCipher() + // convert to encrypted cipher + val encryptedCipher = cipher.toEncryptedCipher() - return ResponseHandler.generateResponse(encryptedCipher, HttpStatus.OK) - } + return ResponseHandler.generateResponse(encryptedCipher, HttpStatus.OK) + } - @PatchMapping("/{id}") - fun updateCipher( - @AuthorizedUser user: UserTable, - @PathVariable id: UUID, - @RequestBody encryptedCipher: EncryptedCipher - ): Response { - if (!checkIfCipherExistsAndOwnedBy(id, user.id)) - return ResponseError.NOT_FOUND.toResponse() + @PatchMapping("/{id}") + fun updateCipher( + @AuthorizedUser user: UserTable, + @PathVariable id: UUID, + @RequestBody encryptedCipher: EncryptedCipher + ): Response { + if (!checkIfCipherExistsAndOwnedBy(id, user.id)) + return ResponseError.NOT_FOUND.toResponse() - cipherRepository.save(CipherTable(encryptedCipher)) + cipherRepository.save(CipherTable(encryptedCipher)) - return ResponseHandler.generateResponse(IdResponse(id), HttpStatus.OK) - } + return ResponseHandler.generateResponse(IdResponse(id), HttpStatus.OK) + } - @DeleteMapping("/{id}") - fun deleteCipher( - @AuthorizedUser user: UserTable, - @PathVariable id: UUID - ): Response { - if (!checkIfCipherExistsAndOwnedBy(id, user.id)) - return ResponseError.NOT_FOUND.toResponse() + @DeleteMapping("/{id}") + fun deleteCipher( + @AuthorizedUser user: UserTable, + @PathVariable id: UUID + ): Response { + if (!checkIfCipherExistsAndOwnedBy(id, user.id)) + return ResponseError.NOT_FOUND.toResponse() - cipherRepository.deleteById(id) + cipherRepository.deleteById(id) - return ResponseHandler.generateResponse(IdResponse(id), HttpStatus.OK) - } + return ResponseHandler.generateResponse(IdResponse(id), HttpStatus.OK) + } - /** - * Checks if cipher exists and is owned by user. - */ - private fun checkIfCipherExistsAndOwnedBy(id: UUID, owner: UUID): Boolean { - return cipherRepository.checkIfCipherExistsAndOwnedBy(id, owner) - } + /** + * Checks if cipher exists and is owned by user. + */ + private fun checkIfCipherExistsAndOwnedBy( + id: UUID, + owner: UUID + ): Boolean { + return cipherRepository.checkIfCipherExistsAndOwnedBy(id, owner) + } - @GetMapping("/icon") - fun getWebsiteIcon( - @RequestParam("domain") domain: String - ): Any { - // Some APIs to get website icon: - // google api: https://www.google.com/s2/favicons?domain=$domain&sz=128 - // duckduckgo api: https://icons.duckduckgo.com/ip3/$domain.ico - // icon.horse: https://icon.horse/icon/$domain + @GetMapping("/icon") + fun getWebsiteIcon( + @RequestParam("domain") domain: String + ): Any { + // Some APIs to get website icon: + // google api: https://www.google.com/s2/favicons?domain=$domain&sz=128 + // duckduckgo api: https://icons.duckduckgo.com/ip3/$domain.ico + // icon.horse: https://icon.horse/icon/$domain - // get using google api - val uri = "https://www.google.com/s2/favicons?domain=$domain&sz=128" + // get using google api + val uri = "https://www.google.com/s2/favicons?domain=$domain&sz=128" - val restTemplate = RestTemplate() + val restTemplate = RestTemplate() - val headers = HttpHeaders() - headers.accept = listOf(MediaType.IMAGE_PNG) + val headers = HttpHeaders() + headers.accept = listOf(MediaType.IMAGE_PNG) - val entity = HttpEntity(headers) + val entity = HttpEntity(headers) - return try { - restTemplate.exchange(uri, HttpMethod.GET, entity, ByteArray::class.java) - } catch (e: Exception) { - ResponseError.NOT_FOUND.toResponse() + return try { + restTemplate.exchange(uri, HttpMethod.GET, entity, ByteArray::class.java) + } catch (e: Exception) { + ResponseError.NOT_FOUND.toResponse() + } } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CollectionController.kt b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CollectionController.kt index 8fa68981..6abb9726 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CollectionController.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/CollectionController.kt @@ -18,91 +18,99 @@ import java.util.* @RestController @RequestMapping("/api/collection") -class CollectionController @Autowired constructor( - private val collectionRepository: CollectionRepository -) { - @PutMapping - fun insertCollection( - @AuthorizedUser user: UserTable, - @RequestBody collection: CreateCollectionRequest - ): Response { - collectionRepository.save( - CollectionTable( - id = collection.id, - name = collection.name, - owner = user.id +class CollectionController + @Autowired + constructor( + private val collectionRepository: CollectionRepository + ) { + @PutMapping + fun insertCollection( + @AuthorizedUser user: UserTable, + @RequestBody collection: CreateCollectionRequest + ): Response { + collectionRepository.save( + CollectionTable( + id = collection.id, + name = collection.name, + owner = user.id + ) ) - ) - return ResponseHandler.generateResponse(IdResponse(collection.id), HttpStatus.CREATED) - } + return ResponseHandler.generateResponse(IdResponse(collection.id), HttpStatus.CREATED) + } + + @GetMapping + fun getAllCollections( + @AuthorizedUser user: UserTable + ): Response { + val collections = collectionRepository.findAllByOwner(user.id) - @GetMapping - fun getAllCollections(@AuthorizedUser user: UserTable): Response { - val collections = collectionRepository.findAllByOwner(user.id) + val cipherCollections = + collections.map { + CipherCollection( + id = it.id, + owner = it.owner, + name = it.name, + created = it.created, + lastModified = it.lastModified + ) + } - val cipherCollections = collections.map { - CipherCollection( - id = it.id, - owner = it.owner, - name = it.name, - created = it.created, - lastModified = it.lastModified + return ResponseHandler.generateResponse( + data = cipherCollections, + status = HttpStatus.OK ) } - return ResponseHandler.generateResponse( - data = cipherCollections, - status = HttpStatus.OK - ) - } - - @GetMapping("/{id}") - fun getCollection( - @AuthorizedUser user: UserTable, - @PathVariable id: UUID - ): Response { - val collection = collectionRepository.findByIdAndOwner(id, user.id) - ?: return ResponseError.NOT_FOUND.toResponse() + @GetMapping("/{id}") + fun getCollection( + @AuthorizedUser user: UserTable, + @PathVariable id: UUID + ): Response { + val collection = + collectionRepository.findByIdAndOwner(id, user.id) + ?: return ResponseError.NOT_FOUND.toResponse() - val cipherCollection = CipherCollection( - id = collection.id, - owner = collection.owner, - name = collection.name, - created = collection.created, - lastModified = collection.lastModified - ) + val cipherCollection = + CipherCollection( + id = collection.id, + owner = collection.owner, + name = collection.name, + created = collection.created, + lastModified = collection.lastModified + ) - return ResponseHandler.generateResponse(cipherCollection, HttpStatus.OK) - } + return ResponseHandler.generateResponse(cipherCollection, HttpStatus.OK) + } - @PatchMapping("/{id}") - fun updateCollection( - @AuthorizedUser user: UserTable, - @PathVariable id: UUID, - @RequestBody collection: CreateCollectionRequest - ): Response { - collectionRepository.save( - CollectionTable( - id = id, - name = collection.name, - owner = user.id + @PatchMapping("/{id}") + fun updateCollection( + @AuthorizedUser user: UserTable, + @PathVariable id: UUID, + @RequestBody collection: CreateCollectionRequest + ): Response { + collectionRepository.save( + CollectionTable( + id = id, + name = collection.name, + owner = user.id + ) ) - ) - return ResponseHandler.generateResponse(IdResponse(collection.id), HttpStatus.OK) - } + return ResponseHandler.generateResponse(IdResponse(collection.id), HttpStatus.OK) + } - @DeleteMapping("/{id}") - fun deleteCollection( - @AuthorizedUser user: UserTable, - @PathVariable id: UUID - ): Response { - val collection = collectionRepository.findByIdAndOwner(id, user.id) - ?: return ResponseError.NOT_FOUND.toResponse() + @DeleteMapping("/{id}") + fun deleteCollection( + @AuthorizedUser user: UserTable, + @PathVariable id: UUID + ): Response { + val collection = + collectionRepository.findByIdAndOwner(id, user.id) + ?: return ResponseError.NOT_FOUND.toResponse() - collectionRepository.delete(collection) + collectionRepository.delete(collection) - return ResponseHandler.generateResponse(IdResponse(collection.id), HttpStatus.OK) + return ResponseHandler.generateResponse(IdResponse(collection.id), HttpStatus.OK) + } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/UserController.kt b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/UserController.kt index 286c202b..41c63864 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/UserController.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/controllers/api/UserController.kt @@ -24,85 +24,87 @@ import java.util.* @RestController @RequestMapping("/api/user") -class UserController @Autowired constructor( - private val userRepository: UserRepository, - private val cipherRepository: CipherRepository -) { - @PatchMapping("/password") - fun changePassword( - @AuthorizedUser user: UserTable, - @RequestBody body: ChangePasswordRequest - ): Response { - // validate shared key with an old public key - val oldSharedKey = X25519.computeSharedSecret(ServerPrivateKey, user.publicKey.fromHexString()) - if (!body.oldSharedKey.fromHexString().contentEquals(oldSharedKey)) - return ResponseError.INVALID_CREDENTIALS.toResponse() +class UserController + @Autowired + constructor( + private val userRepository: UserRepository, + private val cipherRepository: CipherRepository + ) { + @PatchMapping("/password") + fun changePassword( + @AuthorizedUser user: UserTable, + @RequestBody body: ChangePasswordRequest + ): Response { + // validate shared key with an old public key + val oldSharedKey = X25519.computeSharedSecret(ServerPrivateKey, user.publicKey.fromHexString()) + if (!body.oldSharedKey.fromHexString().contentEquals(oldSharedKey)) + return ResponseError.INVALID_CREDENTIALS.toResponse() - // validate shared key with a new public key - val newSharedKey = X25519.computeSharedSecret(ServerPrivateKey, body.newPublicKey.fromHexString()) - if (!body.newSharedKey.fromHexString().contentEquals(newSharedKey)) - return ResponseError.INVALID_CREDENTIALS.toResponse() + // validate shared key with a new public key + val newSharedKey = X25519.computeSharedSecret(ServerPrivateKey, body.newPublicKey.fromHexString()) + if (!body.newSharedKey.fromHexString().contentEquals(newSharedKey)) + return ResponseError.INVALID_CREDENTIALS.toResponse() - // get all user cipher ids - val cipherIds = cipherRepository.getAllIds(user.id) + // get all user cipher ids + val cipherIds = cipherRepository.getAllIds(user.id) - // check if all ciphers are present - // by the way checks if they are owned by the user (because `cipherIds` is a list of user cipher ids) - body.ciphers.forEach { cipherData -> - if (!cipherIds.contains(cipherData.id)) - return ResponseError.INVALID_BODY.toResponse() - } + // check if all ciphers are present + // by the way checks if they are owned by the user (because `cipherIds` is a list of user cipher ids) + body.ciphers.forEach { cipherData -> + if (!cipherIds.contains(cipherData.id)) + return ResponseError.INVALID_BODY.toResponse() + } - // update ciphers data due to re-encryption with new password - body.ciphers.forEach { cipherData -> - cipherRepository.updateData( - cipherData.id, - cipherData.data - ) - } + // update ciphers data due to re-encryption with new password + body.ciphers.forEach { cipherData -> + cipherRepository.updateData( + cipherData.id, + cipherData.data + ) + } - // update user in database - userRepository.save( - user.copy( - passwordHint = body.newPasswordHint, - // Argon2id parameters - parallelism = body.parallelism, - memory = body.memory, - iterations = body.iterations, - // Curve25519 public key - publicKey = body.newPublicKey, - // set the last password change date to now - lastPasswordChange = Date() + // update user in database + userRepository.save( + user.copy( + passwordHint = body.newPasswordHint, + // Argon2id parameters + parallelism = body.parallelism, + memory = body.memory, + iterations = body.iterations, + // Curve25519 public key + publicKey = body.newPublicKey, + // set the last password change date to now + lastPasswordChange = Date() + ) ) - ) - return ResponseHandler.generateResponse(HttpStatus.OK) - } + return ResponseHandler.generateResponse(HttpStatus.OK) + } - @PostMapping("/setup/2fa") - fun setupTwoFactor( - @AuthorizedUser user: UserTable, - @RequestBody body: SetupTwoFactorRequest - ): Response { - // validate shared key with a new public key - val sharedKey = X25519.computeSharedSecret(ServerPrivateKey, user.publicKey.fromHexString()) - if (!body.sharedKey.fromHexString().contentEquals(sharedKey)) - return ResponseError.INVALID_CREDENTIALS.toResponse() + @PostMapping("/setup/2fa") + fun setupTwoFactor( + @AuthorizedUser user: UserTable, + @RequestBody body: SetupTwoFactorRequest + ): Response { + // validate shared key with a new public key + val sharedKey = X25519.computeSharedSecret(ServerPrivateKey, user.publicKey.fromHexString()) + if (!body.sharedKey.fromHexString().contentEquals(sharedKey)) + return ResponseError.INVALID_CREDENTIALS.toResponse() - if (body.code != TOTP.getTOTPCode(body.secret)) - throw InvalidTwoFactorCodeException() + if (body.code != TOTP.getTOTPCode(body.secret)) + throw InvalidTwoFactorCodeException() - val recoveryCode = Random.randBytes(32).toHexString() + val recoveryCode = Random.randBytes(32).toHexString() - userRepository.save( - user.copy( - twoFactorEnabled = true, - twoFactorSecret = body.secret, - twoFactorRecoveryCode = recoveryCode + userRepository.save( + user.copy( + twoFactorEnabled = true, + twoFactorSecret = body.secret, + twoFactorRecoveryCode = recoveryCode + ) ) - ) - val response = SetupTwoFactorResponse(recoveryCode = recoveryCode) - return ResponseHandler.generateResponse(response, HttpStatus.OK) + val response = SetupTwoFactorResponse(recoveryCode = recoveryCode) + return ResponseHandler.generateResponse(response, HttpStatus.OK) + } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherRepository.kt b/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherRepository.kt index 4303114d..e5415d9b 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherRepository.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherRepository.kt @@ -18,7 +18,9 @@ interface CipherRepository : CrudRepository { * @return A list of all ciphers owned by the user. */ @Query("SELECT p FROM #{#entityName} p WHERE p.owner = :owner ORDER BY p.lastModified DESC") - fun getAll(@Param("owner") owner: UUID): List + fun getAll( + @Param("owner") owner: UUID + ): List /** * Check if a cipher exists and is owned by the user. @@ -27,7 +29,10 @@ interface CipherRepository : CrudRepository { * @return True if the cipher exists and is owned by the user, false otherwise. */ @Query("SELECT EXISTS(SELECT 1 FROM #{#entityName} p WHERE p.id = :id AND p.owner = :owner)") - fun checkIfCipherExistsAndOwnedBy(@Param("id") id: UUID, @Param("owner") owner: UUID): Boolean + fun checkIfCipherExistsAndOwnedBy( + @Param("id") id: UUID, + @Param("owner") owner: UUID + ): Boolean /** * Get all user cipher ids. @@ -35,7 +40,9 @@ interface CipherRepository : CrudRepository { * @return A list of all user cipher ids. */ @Query("SELECT p.id FROM #{#entityName} p WHERE p.owner = :owner") - fun getAllIds(@Param("owner") owner: UUID): List + fun getAllIds( + @Param("owner") owner: UUID + ): List /** * Update cipher data. @@ -45,5 +52,8 @@ interface CipherRepository : CrudRepository { @Transactional @Modifying @Query("UPDATE #{#entityName} p SET p.data = :data WHERE p.id = :id") - fun updateData(@Param("id") id: UUID, @Param("data") data: String) + fun updateData( + @Param("id") id: UUID, + @Param("data") data: String + ) } diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherTable.kt b/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherTable.kt index 78a753e9..a8de786e 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherTable.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/database/CipherTable.kt @@ -11,19 +11,14 @@ import java.util.* data class CipherTable( @Id val id: UUID = UUID.randomUUID(), - val owner: UUID, - val type: Int, @Column(columnDefinition = "TEXT") val data: String, - val favorite: Boolean = false, val collection: UUID? = null, val rePrompt: Boolean = false, - val version: Int = 1, - @CreationTimestamp @Temporal(TemporalType.TIMESTAMP) val created: Date = Date(), @@ -47,16 +42,17 @@ data class CipherTable( /** * Convert to [EncryptedCipher] object. This is used to send data to the client. */ - fun toEncryptedCipher() = EncryptedCipher( - id = id, - owner = owner, - type = type, - protectedData = data, - favorite = favorite, - collection = collection, - rePrompt = rePrompt, - version = version, - created = created, - lastModified = lastModified - ) + fun toEncryptedCipher() = + EncryptedCipher( + id = id, + owner = owner, + type = type, + protectedData = data, + favorite = favorite, + collection = collection, + rePrompt = rePrompt, + version = version, + created = created, + lastModified = lastModified + ) } diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionRepository.kt b/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionRepository.kt index e3e0329b..a04dfee9 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionRepository.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionRepository.kt @@ -14,7 +14,10 @@ interface CollectionRepository : CrudRepository { * @param owner user identifier * @return The collection with the given ID and owner, or null if it doesn't exist. */ - fun findByIdAndOwner(id: UUID, owner: UUID): CollectionTable? + fun findByIdAndOwner( + id: UUID, + owner: UUID + ): CollectionTable? /** * Find all collections owned by the given user. diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionTable.kt b/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionTable.kt index dac117e8..1775327e 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionTable.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/database/CollectionTable.kt @@ -10,10 +10,8 @@ import java.util.* data class CollectionTable( @Id val id: UUID = UUID.randomUUID(), - val owner: UUID, val name: String, - @CreationTimestamp @Temporal(TemporalType.TIMESTAMP) val created: Date = Date(), diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/database/TokenTable.kt b/server/src/main/kotlin/dev/medzik/librepass/server/database/TokenTable.kt index 16dba96f..f16ca3dc 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/database/TokenTable.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/database/TokenTable.kt @@ -13,9 +13,7 @@ data class TokenTable( val token: String = generateToken(), val owner: UUID, val confirmed: Boolean, - val lastIp: String, - @CreationTimestamp @Temporal(TemporalType.TIMESTAMP) val created: Date = Date(), diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/database/UserTable.kt b/server/src/main/kotlin/dev/medzik/librepass/server/database/UserTable.kt index fb7a092d..e36c0687 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/database/UserTable.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/database/UserTable.kt @@ -10,31 +10,25 @@ import java.util.* data class UserTable( @Id val id: UUID = UUID.randomUUID(), - @Column(unique = true, columnDefinition = "TEXT") val email: String, val emailVerified: Boolean = false, val emailVerificationCode: String? = null, val emailVerificationCodeExpiresAt: Date? = null, - @Column(columnDefinition = "TEXT") val passwordHint: String? = null, @Temporal(TemporalType.TIMESTAMP) val lastPasswordChange: Date = Date(), - // 2FA val twoFactorEnabled: Boolean = false, val twoFactorSecret: String? = null, val twoFactorRecoveryCode: String? = null, - // Argon2id parameters val parallelism: Int, val memory: Int, val iterations: Int, - // Curve25519 public key val publicKey: String, - @CreationTimestamp @Temporal(TemporalType.TIMESTAMP) val created: Date = Date(), diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/services/EmailService.kt b/server/src/main/kotlin/dev/medzik/librepass/server/services/EmailService.kt index 0ef293f2..dd023067 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/services/EmailService.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/services/EmailService.kt @@ -10,56 +10,73 @@ import org.springframework.stereotype.Service * Service for sending email messages. */ @Service -class EmailService @Autowired constructor( - private val emailSender: JavaMailSender, - @Value("\${smtp.mail.address}") - private val senderAddress: String, - @Value("\${server.api.domain}") - private val apiDomain: String -) { - // get email templates - private val emailVerificationTemplate = this::class.java.getResource("/templates/email-verification.html")?.readText() - ?: throw Exception("Could not read `email verification` email template") - private val passwordHintTemplate = this::class.java.getResource("/templates/password-hint.html")?.readText() - ?: throw Exception("Could not read `password hint` email template") +class EmailService + @Autowired + constructor( + private val emailSender: JavaMailSender, + @Value("\${smtp.mail.address}") + private val senderAddress: String, + @Value("\${server.api.domain}") + private val apiDomain: String + ) { + // get email templates + private val emailVerificationTemplate = + this::class.java.getResource("/templates/email-verification.html")?.readText() + ?: throw Exception("Could not read `email verification` email template") + private val passwordHintTemplate = + this::class.java.getResource("/templates/password-hint.html")?.readText() + ?: throw Exception("Could not read `password hint` email template") - /** - * Email the given address. - * @param to email address to send to - * @param subject subject of the email - * @param body body of the email - */ - fun send(to: String, subject: String, body: String) { - val message = emailSender.createMimeMessage() - message.setFrom(senderAddress) - message.setRecipients(Message.RecipientType.TO, to) - message.subject = subject - message.setText(body, "utf-8", "html") + /** + * Email the given address. + * @param to email address to send to + * @param subject subject of the email + * @param body body of the email + */ + fun send( + to: String, + subject: String, + body: String + ) { + val message = emailSender.createMimeMessage() + message.setFrom(senderAddress) + message.setRecipients(Message.RecipientType.TO, to) + message.subject = subject + message.setText(body, "utf-8", "html") - emailSender.send(message) - } + emailSender.send(message) + } - /** - * Email the given address with the given code. - */ - fun sendEmailVerification(to: String, user: String, code: String) { - val url = "https://$apiDomain/api/auth/verifyEmail?user=$user&code=$code" + /** + * Email the given address with the given code. + */ + fun sendEmailVerification( + to: String, + user: String, + code: String + ) { + val url = "https://$apiDomain/api/auth/verifyEmail?user=$user&code=$code" - val subject = "Activate your LibrePass account" - val body = emailVerificationTemplate - .replace("{{url}}", url) + val subject = "Activate your LibrePass account" + val body = + emailVerificationTemplate + .replace("{{url}}", url) - send(to, subject, body) - } + send(to, subject, body) + } - /** - * Email the given address with the password hint. - */ - fun sendPasswordHint(to: String, hint: String?) { - val subject = "Your LibrePass password hint" - val body = passwordHintTemplate - .replace("{{passwordHint}}", hint ?: "[No password hint set]") + /** + * Email the given address with the password hint. + */ + fun sendPasswordHint( + to: String, + hint: String? + ) { + val subject = "Your LibrePass password hint" + val body = + passwordHintTemplate + .replace("{{passwordHint}}", hint ?: "[No password hint set]") - send(to, subject, body) + send(to, subject, body) + } } -} diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/utils/Date.kt b/server/src/main/kotlin/dev/medzik/librepass/server/utils/Date.kt index cd23bb99..9a4e8c2f 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/utils/Date.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/utils/Date.kt @@ -2,7 +2,11 @@ package dev.medzik.librepass.server.utils import java.util.* -fun checkIfElapsed(start: Date, end: Date, minutes: Int): Boolean { +fun checkIfElapsed( + start: Date, + end: Date, + minutes: Int +): Boolean { val elapsedMilliseconds = end.time - start.time val elapsedMinutes = elapsedMilliseconds / (60 * 1000) return elapsedMinutes >= minutes diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/utils/Response.kt b/server/src/main/kotlin/dev/medzik/librepass/server/utils/Response.kt index a6a0ba06..3d551c0b 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/utils/Response.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/utils/Response.kt @@ -16,34 +16,39 @@ typealias Response = ResponseEntity * ResponseHandler is a helper class for generating responses. */ object ResponseHandler { - fun generateResponse(status: HttpStatus) = - createResponse(HashMap(), status.value()) + fun generateResponse(status: HttpStatus) = createResponse(HashMap(), status.value()) - fun generateResponse(data: Any, status: HttpStatus = HttpStatus.OK) = - createResponse(data, status.value()) + fun generateResponse( + data: Any, + status: HttpStatus = HttpStatus.OK + ) = createResponse(data, status.value()) - fun generateErrorResponse(error: String, status: Int): Response { - val map = mapOf( - "error" to error, - "status" to status - ) + fun generateErrorResponse( + error: String, + status: Int + ): Response { + val map = + mapOf( + "error" to error, + "status" to status + ) return createResponse(map, status) } - fun redirectResponse( - url: String - ): Response = ResponseEntity - .status(HttpStatus.FOUND) - .location(URI.create(url)) - .build() + fun redirectResponse(url: String): Response = + ResponseEntity + .status(HttpStatus.FOUND) + .location(URI.create(url)) + .build() private fun createResponse( data: Any, status: Int - ): Response = ResponseEntity - .status(status) - .body(data) + ): Response = + ResponseEntity + .status(status) + .body(data) } fun ResponseError.toResponse() = ResponseHandler.generateErrorResponse(this.name, this.statusCode.code) diff --git a/server/src/main/kotlin/dev/medzik/librepass/server/utils/Validator.kt b/server/src/main/kotlin/dev/medzik/librepass/server/utils/Validator.kt index c851b5d8..00a235d2 100644 --- a/server/src/main/kotlin/dev/medzik/librepass/server/utils/Validator.kt +++ b/server/src/main/kotlin/dev/medzik/librepass/server/utils/Validator.kt @@ -7,6 +7,11 @@ object Validator { private val REGEX_PATTERN = Pattern.compile("^\\p{XDigit}+$") fun emailValidator(email: String) = EmailValidator.getInstance().isValid(email) + fun hexValidator(hex: String) = REGEX_PATTERN.matcher(hex).matches() - fun hexValidator(hex: String, length: Int) = REGEX_PATTERN.matcher(hex).matches() && hex.length == length * 2 + + fun hexValidator( + hex: String, + length: Int + ) = REGEX_PATTERN.matcher(hex).matches() && hex.length == length * 2 } diff --git a/shared/src/main/kotlin/dev/medzik/librepass/types/api/auth/AuthTypes.kt b/shared/src/main/kotlin/dev/medzik/librepass/types/api/auth/AuthTypes.kt index 26650fe9..cf557ce8 100644 --- a/shared/src/main/kotlin/dev/medzik/librepass/types/api/auth/AuthTypes.kt +++ b/shared/src/main/kotlin/dev/medzik/librepass/types/api/auth/AuthTypes.kt @@ -6,14 +6,11 @@ import java.util.* data class RegisterRequest( val email: String, val passwordHint: String? = null, - val sharedKey: String, - // Argon2id parameters val parallelism: Int, val memory: Int, val iterations: Int, - // Curve25519 public key val publicKey: String ) diff --git a/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/Cipher.kt b/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/Cipher.kt index 193d2377..dce1fdcd 100644 --- a/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/Cipher.kt +++ b/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/Cipher.kt @@ -91,6 +91,7 @@ data class Cipher( ): T? = if (type.ordinal == encryptedCipher.type) Gson().fromJson(encryptedCipher.decryptData(secretKey), T::class.java) - else null + else + null } } diff --git a/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/EncryptedCipher.kt b/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/EncryptedCipher.kt index f7229a6f..e751a50b 100644 --- a/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/EncryptedCipher.kt +++ b/shared/src/main/kotlin/dev/medzik/librepass/types/cipher/EncryptedCipher.kt @@ -49,17 +49,18 @@ data class EncryptedCipher( id = cipher.id, owner = cipher.owner, type = cipher.type.ordinal, - protectedData = Aes.encrypt( - Aes.GCM, - secretKey.fromHexString(), - Gson().toJson( - when (cipher.type) { - CipherType.Login -> cipher.loginData - CipherType.Card -> cipher.cardData - CipherType.SecureNote -> cipher.secureNoteData - } - ).toByteArray() - ), + protectedData = + Aes.encrypt( + Aes.GCM, + secretKey.fromHexString(), + Gson().toJson( + when (cipher.type) { + CipherType.Login -> cipher.loginData + CipherType.Card -> cipher.cardData + CipherType.SecureNote -> cipher.secureNoteData + } + ).toByteArray() + ), collection = cipher.collection, favorite = cipher.favorite, rePrompt = cipher.rePrompt, diff --git a/shared/src/main/kotlin/dev/medzik/librepass/utils/Hex.kt b/shared/src/main/kotlin/dev/medzik/librepass/utils/Hex.kt index d4afc64c..8cdf0246 100644 --- a/shared/src/main/kotlin/dev/medzik/librepass/utils/Hex.kt +++ b/shared/src/main/kotlin/dev/medzik/librepass/utils/Hex.kt @@ -3,4 +3,5 @@ package dev.medzik.librepass.utils import org.apache.commons.codec.binary.Hex fun ByteArray.toHexString(): String = Hex.encodeHexString(this) + fun String.fromHexString(): ByteArray = Hex.decodeHex(this) diff --git a/shared/src/main/kotlin/dev/medzik/librepass/utils/TOTP.kt b/shared/src/main/kotlin/dev/medzik/librepass/utils/TOTP.kt index bdbf1e5b..9b8b3118 100644 --- a/shared/src/main/kotlin/dev/medzik/librepass/utils/TOTP.kt +++ b/shared/src/main/kotlin/dev/medzik/librepass/utils/TOTP.kt @@ -19,7 +19,10 @@ object TOTP { return totpGenerator.now() } - fun validate(secret: String, otpCode: String): Boolean { + fun validate( + secret: String, + otpCode: String + ): Boolean { val totpGenerator = initializeTOTPGenerator(secret) // Check the current code and the previous two and the next two. return totpGenerator.verify(otpCode, 2) @@ -29,12 +32,13 @@ object TOTP { val base32 = Base32() val bytes = base32.decode(secret) - val totp = TOTPGenerator.Builder(bytes) - .withHOTPGenerator { builder: HOTPGenerator.Builder -> - builder.withPasswordLength(6) - builder.withAlgorithm(HMACAlgorithm.SHA1) // SHA256 and SHA512 are also supported - } - .withPeriod(Duration.ofSeconds(30)) + val totp = + TOTPGenerator.Builder(bytes) + .withHOTPGenerator { builder: HOTPGenerator.Builder -> + builder.withPasswordLength(6) + builder.withAlgorithm(HMACAlgorithm.SHA1) // SHA256 and SHA512 are also supported + } + .withPeriod(Duration.ofSeconds(30)) return totp.build() } diff --git a/shared/src/test/kotlin/dev/medzik/librepass/types/CipherTests.kt b/shared/src/test/kotlin/dev/medzik/librepass/types/CipherTests.kt index ec7c3616..e7623bf5 100644 --- a/shared/src/test/kotlin/dev/medzik/librepass/types/CipherTests.kt +++ b/shared/src/test/kotlin/dev/medzik/librepass/types/CipherTests.kt @@ -16,33 +16,37 @@ class CipherTests { private val secretKey = "1234567890123456789012345678901212345678901234567890123456789012" // example cipher - private val cipher = Cipher( - id = UUID.randomUUID(), - owner = UUID.randomUUID(), - type = CipherType.Login, - loginData = CipherLoginData( - name = "Example", - username = "librepass@example.com", - password = "SomeSecretPassword123!", - passwordHistory = listOf( - PasswordHistory( - password = "very secret password", - // current date without milliseconds (because it is broken when comparing dates) - lastUsed = Date(System.currentTimeMillis() / 1000 * 1000) - ) - ), - fields = listOf( - CipherField( - name = "test", - type = CipherFieldType.Text, - value = "test" - ) - ) - ), - collection = UUID.randomUUID(), - favorite = true, - rePrompt = true - ) + private val cipher = + Cipher( + id = UUID.randomUUID(), + owner = UUID.randomUUID(), + type = CipherType.Login, + loginData = + CipherLoginData( + name = "Example", + username = "librepass@example.com", + password = "SomeSecretPassword123!", + passwordHistory = + listOf( + PasswordHistory( + password = "very secret password", + // current date without milliseconds (because it is broken when comparing dates) + lastUsed = Date(System.currentTimeMillis() / 1000 * 1000) + ) + ), + fields = + listOf( + CipherField( + name = "test", + type = CipherFieldType.Text, + value = "test" + ) + ) + ), + collection = UUID.randomUUID(), + favorite = true, + rePrompt = true + ) @Test fun `encrypt cipher`() {