Nothing
# Regression tests for the file-output safety fixes: owner-only key files,
# same-file rejection, atomic writes that preserve a pre-existing destination
# on failure, and no leftover temp files.
temp_siblings <- function(path) {
list.files(dirname(path),
pattern = "\\.age-[0-9a-f]{16}$", full.names = TRUE
)
}
test_that("generated key files are not world/group readable", {
skip_on_os("windows") # POSIX permission bits only
kf <- withr::local_tempfile()
old <- Sys.umask("0000") # force a permissive umask
withr::defer(Sys.umask(old))
age_keygen(kf)
expect_identical(as.character(file.mode(kf)), "600")
})
test_that("file APIs reject output identical to input", {
p <- new_pair()
f <- withr::local_tempfile(fileext = ".txt")
writeBin(charToRaw("IMPORTANT PLAINTEXT"), f)
expect_error(
age_encrypt_file(f, output = f, recipients = p$rec, overwrite = TRUE),
class = "age_error_io"
)
# a relative alias of the same file is also rejected
withr::with_dir(dirname(f), {
base <- basename(f)
expect_error(
age_encrypt_file(base, output = file.path(".", base),
recipients = p$rec, overwrite = TRUE),
class = "age_error_io"
)
})
# the input is untouched
expect_identical(readBin(f, "raw", 100), charToRaw("IMPORTANT PLAINTEXT"))
})
test_that("a symlink pointing at the input is rejected as the same file", {
skip_on_os("windows")
p <- new_pair()
f <- withr::local_tempfile(fileext = ".txt")
writeBin(charToRaw("PLAINTEXT"), f)
link <- withr::local_tempfile(fileext = ".txt")
ok <- file.symlink(f, link)
skip_if_not(ok, "could not create a symlink")
expect_error(
age_encrypt_file(f, output = link, recipients = p$rec, overwrite = TRUE),
class = "age_error_io"
)
expect_identical(readBin(f, "raw", 100), charToRaw("PLAINTEXT"))
})
test_that("a failed decryption preserves a pre-existing destination", {
p <- new_pair()
out <- withr::local_tempfile(fileext = ".age")
writeBin(charToRaw("PRE-EXISTING DESTINATION"), out)
bad <- withr::local_tempfile()
writeBin(as.raw(1:8), bad) # not valid age ciphertext
expect_error(
age_decrypt_file(bad, output = out, identities = p$id, overwrite = TRUE),
class = "age_error_decrypt"
)
expect_true(file.exists(out))
expect_identical(readBin(out, "raw", 100), charToRaw("PRE-EXISTING DESTINATION"))
expect_length(temp_siblings(out), 0L) # no leftover temp file
})
test_that("a failed encryption preserves a pre-existing destination", {
# encrypt with an unusable recipient after a valid output already exists
out <- withr::local_tempfile(fileext = ".age")
writeBin(charToRaw("KEEP ME"), out)
f <- withr::local_tempfile(fileext = ".txt")
writeBin(charToRaw("plaintext"), f)
# a malformed recipient fails before any output is produced
expect_error(
age_encrypt_file(f, output = out, recipients = "age1nope", overwrite = TRUE),
class = "age_error_recipient"
)
expect_identical(readBin(out, "raw", 100), charToRaw("KEEP ME"))
})
test_that("successful overwrite replaces the destination and leaves no temp", {
p <- new_pair()
f <- withr::local_tempfile(fileext = ".txt")
writeBin(charToRaw("v2 contents"), f)
out <- withr::local_tempfile(fileext = ".age")
writeBin(charToRaw("stale ciphertext"), out)
age_encrypt_file(f, output = out, recipients = p$rec, overwrite = TRUE)
dec <- age_decrypt_file(out, output = withr::local_tempfile(), identities = p$id)
expect_identical(readBin(dec, "raw", 100), charToRaw("v2 contents"))
expect_length(temp_siblings(out), 0L)
})
test_that("native write refuses to clobber for overwrite = FALSE (no TOCTOU race)", {
# Bypass the R-side guard_output() by calling the native entry point directly
# with overwrite = FALSE, simulating a destination that appears between the
# R existence check and the C write. The C layer must be authoritative.
p <- new_pair()
f <- withr::local_tempfile(fileext = ".txt")
writeBin(charToRaw("plaintext"), f)
out <- withr::local_tempfile(fileext = ".age")
writeBin(charToRaw("PRE-EXISTING DESTINATION"), out)
status <- .Call(
agecrypt:::C_age_c_encrypt_path,
normalizePath(f), normalizePath(out), p$rec, FALSE, FALSE
)
expect_false(identical(status[[1L]], "")) # errored, did not clobber
expect_match(status[[2L]], "exists")
expect_identical(
readBin(out, "raw", 100), charToRaw("PRE-EXISTING DESTINATION")
)
expect_length(temp_siblings(out), 0L)
# overwrite = TRUE replaces atomically
status2 <- .Call(
agecrypt:::C_age_c_encrypt_path,
normalizePath(f), normalizePath(out), p$rec, FALSE, TRUE
)
expect_identical(status2[[1L]], "")
expect_identical(
rawToChar(age_decrypt_raw(readBin(out, "raw", 1e5), p$id)), "plaintext"
)
})
test_that("encrypting a file over itself is safe once distinctness is bypassed", {
# Even if a caller reaches C with input == output (e.g. an undetected alias),
# the atomic write must not destroy data. Exercise the native path directly.
p <- new_pair()
f <- withr::local_tempfile(fileext = ".bin")
writeBin(charToRaw("ATOMIC SELF ENCRYPT"), f)
status <- .Call(
agecrypt:::C_age_c_encrypt_path,
normalizePath(f), normalizePath(f), p$rec, FALSE, TRUE
)
expect_identical(status[[1L]], "") # success
# f now holds its own ciphertext, which decrypts back to the original
back <- age_decrypt_raw(readBin(f, "raw", 1e5), identities = p$id)
expect_identical(rawToChar(back), "ATOMIC SELF ENCRYPT")
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.