Skip to content

Commit

Permalink
feat(accounts): Very dirty partial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
phinner committed Jun 28, 2023
1 parent 42c5318 commit 67c1c78
Show file tree
Hide file tree
Showing 28 changed files with 585 additions and 136 deletions.
1 change: 1 addition & 0 deletions foundation-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ dependencies {
api(libs.password4j)
api(libs.kryo)
api(libs.rabbitmq.client)
api("org.litote.kmongo:kmongo-async-native:4.9.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import com.xpdustry.foundation.common.misc.ExitStatus
import com.xpdustry.foundation.common.misc.LoggerDelegate
import kotlin.reflect.KClass

open class SimpleFoundationApplication(common: Module, implementation: Module) : FoundationApplication {
open class SimpleFoundationApplication(common: Module, implementation: Module, production: Boolean) : FoundationApplication {

private val listeners = arrayListOf<FoundationListener>()
private val injector: Injector = Guice.createInjector(
Stage.PRODUCTION,
if (production) Stage.PRODUCTION else Stage.DEVELOPMENT,
FoundationAwareModule(Modules.override(common).with(implementation)),
)

Expand All @@ -45,7 +45,7 @@ open class SimpleFoundationApplication(common: Module, implementation: Module) :
fun register(listener: KClass<out FoundationListener>) =
register(injector.getInstance(listener.java))

fun <T : Any> instance(clazz: KClass<*>): Any =
fun <T : Any> instance(clazz: KClass<T>): T =
injector.getInstance(clazz.java)

protected open fun onListenerRegistration(listener: FoundationListener) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,32 @@
package com.xpdustry.foundation.common.database.model

import com.xpdustry.foundation.common.database.Entity
import com.xpdustry.foundation.common.hash.HashWithParams
import com.xpdustry.foundation.common.hash.Hash
import org.bson.types.ObjectId

// This structure stores the users bound to an account.
// The HashWithParams is the hashed usid (NOTE: It is nullable because the previous database did not it)
typealias BoundUsers = MutableMap<MindustryUUID, HashWithParams?>
import java.time.Instant

data class Account(
override val id: ObjectId,
var username: String,
var password: HashWithParams,
override val id: ObjectId = ObjectId(),
val uuids: MutableSet<String> = mutableSetOf(),
var password: Hash,
val hashedUsername: String? = null,
var rank: Rank = Rank.NEWBIE,
var steam: String? = null,
var discord: String? = null,
val users: BoundUsers = mutableMapOf(),
val sessions: MutableMap<String, Instant> = mutableMapOf(),
) : Entity<ObjectId> {
val verified: Boolean
get() = steam != null || discord != null

val verified: Boolean get() = steam != null || discord != null

enum class Rank {
NEWBIE,
ACTIVE,
HYPER_ACTIVE,
CONTRIBUTOR,
OVERSEER,
MODERATOR,
ADMINISTRATOR,
OWNER,
}

data class UuidData(val sessions: MutableMap<String, Instant> = mutableMapOf())
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ package com.xpdustry.foundation.common.database.model

import com.xpdustry.foundation.common.database.EntityManager
import org.bson.types.ObjectId
import reactor.core.publisher.Mono

interface AccountManager : EntityManager<ObjectId, Account>
interface AccountManager : EntityManager<ObjectId, Account> {
fun findByUuid(uuid: String): Mono<Account>
fun findByHashedUsername(hashedUsername: String): Mono<Account>
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import java.time.Duration
import java.time.Instant

data class Punishment(
override val id: ObjectId,
override val id: ObjectId = ObjectId(),
var targetIp: InetAddress,
var targetUuid: MindustryUUID,
var reason: String = "Unknown",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import java.net.InetAddress
import java.time.Duration

typealias MindustryUUID = String
typealias MindustryUSID = String

data class User(
override val id: MindustryUUID,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Foundation, the software collection powering the Xpdustry network.
* Copyright (C) 2023 Xpdustry
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.xpdustry.foundation.common.database.mongo

import com.xpdustry.foundation.common.hash.Hash
import com.xpdustry.foundation.common.hash.HashParams
import org.bson.BsonBinary
import org.bson.BsonReader
import org.bson.BsonWriter
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext

class HashCodec : Codec<Hash> {
override fun getEncoderClass(): Class<Hash> = Hash::class.java

override fun encode(writer: BsonWriter, value: Hash, encoderContext: EncoderContext) {
writer.writeStartDocument()
writer.writeBinaryData("hash", BsonBinary(value.hash))
writer.writeBinaryData("salt", BsonBinary(value.salt))
writer.writeString("params", value.params.toString())
writer.writeEndDocument()
}

// TODO: Handle unordered fields
override fun decode(reader: BsonReader, decoderContext: DecoderContext): Hash {
reader.readStartDocument()
val hash = reader.readBinaryData("hash").data
val salt = reader.readBinaryData("salt").data
val params = HashParams.fromString(reader.readString("params"))
reader.readEndDocument()
return Hash(hash, salt, params)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ package com.xpdustry.foundation.common.database.mongo
import com.mongodb.reactivestreams.client.MongoCollection
import com.xpdustry.foundation.common.database.model.Account
import com.xpdustry.foundation.common.database.model.AccountManager
import com.xpdustry.foundation.common.misc.toValueMono
import org.bson.types.ObjectId
import org.litote.kmongo.contains
import org.litote.kmongo.eq
import org.litote.kmongo.reactivestreams.findOne
import reactor.core.publisher.Mono

class MongoAccountManager(
collection: MongoCollection<Account>,
) : MongoEntityManager<Account, ObjectId>(collection), AccountManager
class MongoAccountManager(collection: MongoCollection<Account>) : MongoEntityManager<Account, ObjectId>(collection), AccountManager {

override fun findByUuid(uuid: String): Mono<Account> =
collection.findOne(Account::uuids contains uuid).toValueMono()

override fun findByHashedUsername(hashedUsername: String): Mono<Account> =
collection.findOne(Account::hashedUsername eq hashedUsername).toValueMono()
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import com.mongodb.ServerApi
import com.mongodb.ServerApiVersion
import com.mongodb.connection.SslSettings
import com.mongodb.reactivestreams.client.MongoClient
import com.mongodb.reactivestreams.client.MongoClients
import com.xpdustry.foundation.common.application.FoundationListener
import com.xpdustry.foundation.common.application.FoundationMetadata
import com.xpdustry.foundation.common.config.FoundationConfig
Expand All @@ -40,6 +39,7 @@ import com.xpdustry.foundation.common.misc.toValueFlux
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.pojo.Conventions
import org.bson.codecs.pojo.PojoCodecProvider
import org.litote.kmongo.reactivestreams.KMongo
import java.time.Duration

class MongoDatabase @Inject constructor(private val config: FoundationConfig, private val metadata: FoundationMetadata) : Database, FoundationListener {
Expand All @@ -50,7 +50,7 @@ class MongoDatabase @Inject constructor(private val config: FoundationConfig, pr
override lateinit var accounts: AccountManager

override fun onFoundationInit() {
client = MongoClients.create(
client = KMongo.createClient(
MongoClientSettings.builder()
.applicationName("foundation-${metadata.name}")
.applyToClusterSettings {
Expand Down Expand Up @@ -95,6 +95,7 @@ class MongoDatabase @Inject constructor(private val config: FoundationConfig, pr
CodecRegistries.fromCodecs(
DurationCodec(),
InetAddressCodec(),
HashCodec(),
),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import com.xpdustry.foundation.common.database.Entity
import com.xpdustry.foundation.common.database.EntityManager
import com.xpdustry.foundation.common.misc.toValueFlux
import com.xpdustry.foundation.common.misc.toValueMono
import org.litote.kmongo.eq
import org.litote.kmongo.`in`
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

Expand All @@ -35,32 +37,34 @@ abstract class MongoEntityManager<E : Entity<I>, I> protected constructor(
) : EntityManager<I, E> {

override fun save(entity: E): Mono<Void> =
collection.replaceOne(Filters.eq(ID_FIELD, entity.id), entity, ReplaceOptions().upsert(true)).toValueMono().then()
collection.replaceOne(Entity<I>::id eq entity.id, entity, ReplaceOptions().upsert(true)).toValueMono().then()

override fun saveAll(entities: Iterable<E>): Mono<Void> =
entities.toValueFlux()
.map { ReplaceOneModel(Filters.eq(ID_FIELD, it.id), it, ReplaceOptions().upsert(true)) }
.map { ReplaceOneModel(Entity<I>::id eq it.id, it, ReplaceOptions().upsert(true)) }
.collectList()
.flatMap { collection.bulkWrite(it).toValueMono().then() }

override fun findById(id: I): Mono<E> =
collection.find(Filters.eq(ID_FIELD, id)).first().toValueMono()
collection.find(
Entity<I>::id eq id,
).first().toValueMono()

override fun findAll(): Flux<E> = collection.find().toValueFlux()

override fun exists(entity: E): Mono<Boolean> =
collection.countDocuments(Filters.eq(ID_FIELD, entity.id)).toValueMono().map { it > 0 }
collection.countDocuments(Entity<I>::id eq entity.id).toValueMono().map { it > 0 }

override fun count(): Mono<Long> = collection.countDocuments().toValueMono()

override fun deleteById(id: I): Mono<Void> =
collection.deleteOne(Filters.eq(ID_FIELD, id)).toValueMono().then()
collection.deleteOne(Entity<I>::id eq id).toValueMono().then()

override fun deleteAll(): Mono<Void> = collection.deleteMany(Filters.empty()).toValueMono().then()

override fun deleteAll(entities: Iterable<E>): Mono<Void> =
entities.toValueFlux()
.map(Entity<I>::id)
.collectList()
.flatMap { collection.deleteMany(Filters.`in`(ID_FIELD, it)).toValueMono().then() }
.flatMap { ids -> collection.deleteMany(Entity<I>::id `in` ids).toValueMono().then() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,19 @@
*/
package com.xpdustry.foundation.common.database.mongo

import com.mongodb.client.model.Filters
import com.mongodb.reactivestreams.client.MongoCollection
import com.xpdustry.foundation.common.database.model.MindustryUUID
import com.xpdustry.foundation.common.database.model.Punishment
import com.xpdustry.foundation.common.database.model.PunishmentManager
import com.xpdustry.foundation.common.misc.toValueFlux
import org.bson.types.ObjectId
import org.litote.kmongo.eq
import reactor.core.publisher.Flux
import java.net.InetAddress

class MongoPunishmentManager(
collection: MongoCollection<Punishment>,
) : MongoEntityManager<Punishment, ObjectId>(collection), PunishmentManager {

class MongoPunishmentManager(collection: MongoCollection<Punishment>) : MongoEntityManager<Punishment, ObjectId>(collection), PunishmentManager {
override fun findAllByTargetIp(target: InetAddress): Flux<Punishment> =
collection.find(Filters.eq("target_ip", target.hostAddress)).toValueFlux()

collection.find(Punishment::targetIp eq target).toValueFlux()
override fun findAllByTargetUuid(target: MindustryUUID): Flux<Punishment> =
collection.find(Filters.eq("target_uuid", target)).toValueFlux()
collection.find(Punishment::targetUuid eq target).toValueFlux()
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ data class Argon2Params(
val parallelism: Int,
val type: Type,
val version: Version,
val saltLength: Int,
) : HashParams {
init {
require(memory > 0) { "memory must be positive" }
Expand All @@ -42,11 +43,7 @@ data class Argon2Params(
}

override fun toString(): String {
return "argon2/m=$memory,i=$iterations,p=$parallelism,l=$length,t=${type.name.lowercase(Locale.ROOT)},v=${
version.name.lowercase(
Locale.ROOT,
)
}"
return "argon2/m=$memory,i=$iterations,p=$parallelism,l=$length,t=${type.name.lowercase(Locale.ROOT)},v=${version.name.lowercase()},s=$saltLength"
}

enum class Type {
Expand All @@ -62,31 +59,41 @@ data class Argon2Params(
if (!str.startsWith("argon2/")) {
throw IllegalArgumentException("Invalid argon2 params: $str")
}

val params = str.substring("argon2/".length)
.split(",")
.map { it.trim().split("=") }
.associate { it[0] to it[1] }

return Argon2Params(
params["m"]!!.toInt(),
params["i"]!!.toInt(),
params["l"]!!.toInt(),
params["p"]!!.toInt(),
Type.valueOf(params["t"]!!.uppercase(Locale.ROOT)),
Version.valueOf(params["v"]!!.uppercase(Locale.ROOT)),
params["s"]!!.toInt(),
)
}
}
}

object Argon2HashFunction : HashFunction<Argon2Params> {
object Argon2HashFunction : SaltyHashFunction<Argon2Params> {

override fun create(password: CharArray, params: Argon2Params, salt: ByteArray): Mono<Hash> {
return create0(Password.hash(SecureString(password)).addSalt(salt), params)
}
override fun create(chars: CharArray, params: Argon2Params): Mono<Hash> =
create0(Password.hash(SecureString(chars)).addRandomSalt(params.saltLength), params)

override fun create(password: CharArray, params: Argon2Params, saltLength: Int): Mono<Hash> {
return create0(Password.hash(SecureString(password)).addRandomSalt(saltLength), params)
}
override fun create(bytes: ByteArray, params: Argon2Params): Mono<Hash> =
create0(Password.hash(bytes).addRandomSalt(params.saltLength), params)

override fun create(chars: CharArray, params: Argon2Params, salt: CharArray): Mono<Hash> =
create0(Password.hash(SecureString(chars)).addSalt(salt.toString()), params)

override fun create(chars: CharArray, params: Argon2Params, salt: ByteArray): Mono<Hash> =
create0(Password.hash(SecureString(chars)).addSalt(salt), params)

override fun create(bytes: ByteArray, params: Argon2Params, salt: ByteArray): Mono<Hash> =
create0(Password.hash(bytes).addSalt(salt), params)

private fun create0(builder: HashBuilder, params: Argon2Params): Mono<Hash> {
return Mono.fromSupplier {
Expand All @@ -101,7 +108,7 @@ object Argon2HashFunction : HashFunction<Argon2Params> {
),
)
}
.map { Hash(it.bytes, it.saltBytes) }
.map { Hash(it.bytes, it.saltBytes, params) }
.subscribeOn(Schedulers.boundedElastic())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,21 @@ package com.xpdustry.foundation.common.hash
import java.util.Base64
import java.util.Objects

class Hash(hash: ByteArray, salt: ByteArray) {
class Hash(hash: ByteArray, salt: ByteArray, val params: HashParams) {
private val _hash: ByteArray = hash.clone()
private val _salt: ByteArray = salt.clone()

val hash: ByteArray
get() = _hash.clone()
val salt: ByteArray
get() = _salt.clone()
val hash: ByteArray get() = _hash.clone()
val salt: ByteArray get() = _salt.clone()

override fun equals(other: Any?): Boolean =
other is Hash && timeConstantEquals(hash, other.hash) && timeConstantEquals(salt, other.salt)
other is Hash && timeConstantEquals(hash, other.hash) && timeConstantEquals(salt, other.salt) && params == other.params

override fun hashCode() =
Objects.hash(_hash.contentHashCode(), _salt.contentHashCode())
Objects.hash(_hash.contentHashCode(), _salt.contentHashCode(), params)

override fun toString() =
"Password(hash=${_hash.toBase64()}, salt=${_salt.toBase64()})"
"Password(hash=${_hash.toBase64()}, salt=${_salt.toBase64()}, params=$params)"
}

// Stolen from password4j
Expand Down
Loading

0 comments on commit 67c1c78

Please sign in to comment.