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) { require(arguments.size == 3) { "Usage: " } 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" }