1a9c518f96
Provide TOTP-authenticated, role-controlled user and credit workflows with paginated audit data and SQL-backed statistics so operations can manage growth safely.
105 lines
4.1 KiB
Kotlin
105 lines
4.1 KiB
Kotlin
package com.osglab.account.tools
|
||
|
||
import com.osglab.account.features.admin.security.BouncyCastleArgon2idPasswordHasher
|
||
import java.nio.file.Files
|
||
import java.nio.file.Path
|
||
import java.nio.file.StandardOpenOption.CREATE_NEW
|
||
import java.nio.file.attribute.PosixFilePermission
|
||
import java.security.SecureRandom
|
||
import java.util.Base64
|
||
import java.util.UUID
|
||
|
||
/**
|
||
* Generates one bootstrap operator without printing credentials to stdout.
|
||
* Both output files are created with owner-only permissions and must not exist.
|
||
*/
|
||
object AdminCredentialGenerator {
|
||
@JvmStatic
|
||
fun main(arguments: Array<String>) {
|
||
require(arguments.size == 3) {
|
||
"Usage: <username> <runtime-env-output> <operator-handoff-output>"
|
||
}
|
||
val username = arguments[0].trim().lowercase()
|
||
require(USERNAME.matches(username)) { "Username must match ${USERNAME.pattern}" }
|
||
val runtimeOutput = Path.of(arguments[1]).toAbsolutePath()
|
||
val handoffOutput = Path.of(arguments[2]).toAbsolutePath()
|
||
require(runtimeOutput != handoffOutput) { "Output paths must be different" }
|
||
require(Files.notExists(runtimeOutput) && Files.notExists(handoffOutput)) {
|
||
"Output files must not already exist"
|
||
}
|
||
|
||
val random = SecureRandom()
|
||
val password = Base64.getUrlEncoder().withoutPadding()
|
||
.encodeToString(ByteArray(24).also(random::nextBytes))
|
||
val passwordChars = password.toCharArray()
|
||
val passwordHash = try {
|
||
BouncyCastleArgon2idPasswordHasher(secureRandom = random).hash(passwordChars)
|
||
} finally {
|
||
passwordChars.fill('\u0000')
|
||
}
|
||
val totpSecret = base32(ByteArray(20).also(random::nextBytes))
|
||
val operatorId = UUID.randomUUID()
|
||
|
||
writePrivate(
|
||
runtimeOutput,
|
||
"""
|
||
# One-time admin bootstrap values. Never commit, upload, or screenshot.
|
||
# After the first successful startup, set ADMIN_BOOTSTRAP_ENABLED=false
|
||
# and permanently remove all ADMIN_BOOTSTRAP_* credential values.
|
||
ADMIN_ENABLED=true
|
||
ADMIN_BOOTSTRAP_ENABLED=true
|
||
ADMIN_BOOTSTRAP_OPERATOR_ID=$operatorId
|
||
ADMIN_BOOTSTRAP_USERNAME=$username
|
||
ADMIN_BOOTSTRAP_PASSWORD_HASH='$passwordHash'
|
||
ADMIN_BOOTSTRAP_TOTP_SECRET_BASE32=$totpSecret
|
||
ADMIN_SESSION_HOURS=8
|
||
ADMIN_MAXIMUM_MANUAL_GRANT=100000
|
||
""".trimIndent() + "\n",
|
||
)
|
||
writePrivate(
|
||
handoffOutput,
|
||
"""
|
||
OSG 运营后台初始管理员
|
||
用户名:$username
|
||
密码:$password
|
||
TOTP 密钥:$totpSecret
|
||
认证器 URI:otpauth://totp/OSG%20Admin:$username?secret=$totpSecret&issuer=OSG%20Admin&algorithm=SHA1&digits=6&period=30
|
||
|
||
仅保存在受信设备。首次部署成功后,请将密码录入密码管理器,并删除本文件。
|
||
""".trimIndent() + "\n",
|
||
)
|
||
}
|
||
|
||
private fun writePrivate(path: Path, content: String) {
|
||
Files.writeString(path, content, CREATE_NEW)
|
||
runCatching {
|
||
Files.setPosixFilePermissions(
|
||
path,
|
||
setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE),
|
||
)
|
||
}
|
||
}
|
||
|
||
private fun base32(bytes: ByteArray): String {
|
||
val output = StringBuilder((bytes.size * 8 + 4) / 5)
|
||
var buffer = 0
|
||
var bufferedBits = 0
|
||
bytes.forEach { byte ->
|
||
buffer = (buffer shl 8) or (byte.toInt() and 0xff)
|
||
bufferedBits += 8
|
||
while (bufferedBits >= 5) {
|
||
bufferedBits -= 5
|
||
output.append(BASE32_ALPHABET[(buffer shr bufferedBits) and 0x1f])
|
||
buffer = if (bufferedBits == 0) 0 else buffer and ((1 shl bufferedBits) - 1)
|
||
}
|
||
}
|
||
if (bufferedBits > 0) {
|
||
output.append(BASE32_ALPHABET[(buffer shl (5 - bufferedBits)) and 0x1f])
|
||
}
|
||
return output.toString()
|
||
}
|
||
|
||
private val USERNAME = Regex("^[a-z0-9][a-z0-9._@-]{2,63}$")
|
||
private const val BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
||
}
|