Works off the security audit in docs/prs/2026-07-08-PR#000: TODO 1, 8, 9 and 10, the four items that need no design decision. New `SecretFiles` creates files holding secrets with mode 0600 and their directories with 0700 *at creation*, as a file attribute, instead of writing at the umask default and chmod-ing afterwards — that left a window in which the Gitea token was world-readable, which matters on a multi-tenant host. It is used by `init` for .git/gittally/.gittally.yml and by `ControlTokenService` for the control token; the shell setup script now writes its YAML in a `umask 077` subshell for the same reason. `ControlTokenService.matches` hashes both sides with SHA-256 before `MessageDigest.isEqual`, so the comparison always runs over two 32-byte buffers and cannot return early on a length mismatch. `GitService.checkout` and `fetchBranch` pass `--` before the refname, so a branch named like an option cannot be read as one. `resetHardToOrigin` keeps its plain form: `git reset --hard -- <commit>` is rejected outright and its argument is already `origin/`-prefixed. Co-Authored-By: Claude <noreply@anthropic.com>
50 lines
1.9 KiB
Kotlin
50 lines
1.9 KiB
Kotlin
package de.hoennig.gittally
|
|
|
|
import java.nio.ByteBuffer
|
|
import java.nio.file.Files
|
|
import java.nio.file.Path
|
|
import java.nio.file.StandardOpenOption
|
|
import java.nio.file.attribute.PosixFilePermissions
|
|
|
|
/**
|
|
* Creation of files and directories that hold secrets — the Gitea token in
|
|
* `.git/gittally/.gittally.yml` and the control token.
|
|
*
|
|
* The permissions are set *at creation*, never with a `chmod` after the write:
|
|
* writing at the umask default first (typically `0644`) would leave a window in
|
|
* which the secret is world-readable, which matters on multi-tenant hosts.
|
|
* On non-POSIX filesystems the permissions are silently skipped.
|
|
*/
|
|
object SecretFiles {
|
|
private val OWNER_ONLY_FILE = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"))
|
|
private val OWNER_ONLY_DIRECTORY = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))
|
|
|
|
/** Writes [content] as a `0600` file, replacing an existing file. */
|
|
fun writeOwnerOnly(
|
|
file: Path,
|
|
content: String,
|
|
) {
|
|
val bytes = content.toByteArray()
|
|
Files.deleteIfExists(file)
|
|
try {
|
|
Files
|
|
.newByteChannel(file, setOf(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), OWNER_ONLY_FILE)
|
|
.use { it.write(ByteBuffer.wrap(bytes)) }
|
|
} catch (_: UnsupportedOperationException) {
|
|
Files.write(file, bytes)
|
|
}
|
|
}
|
|
|
|
/** Creates [directory] and its parents; a directory created here gets mode `0700`. */
|
|
fun createDirectoriesOwnerOnly(directory: Path) {
|
|
val missing = generateSequence(directory) { it.parent }.takeWhile { !Files.exists(it) }.toList().asReversed()
|
|
missing.forEach { path ->
|
|
try {
|
|
Files.createDirectory(path, OWNER_ONLY_DIRECTORY)
|
|
} catch (_: UnsupportedOperationException) {
|
|
Files.createDirectory(path)
|
|
}
|
|
}
|
|
}
|
|
}
|