fix: nvim sops lua
This commit is contained in:
@@ -9,6 +9,17 @@ local secrets_patterns = {
|
|||||||
"*secrets*.yaml",
|
"*secrets*.yaml",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Size limits to prevent memory issues and UI freezes
|
||||||
|
-- 5MB encrypted file limit (typical secrets files are <100KB)
|
||||||
|
local MAX_SOPS_FILE_SIZE = 5 * 1024 * 1024 -- 5MB
|
||||||
|
-- 10MB decrypted content limit (allows for expansion during decryption)
|
||||||
|
local MAX_DECRYPTED_SIZE = 10 * 1024 * 1024 -- 10MB
|
||||||
|
-- Timeout for SOPS operations to prevent infinite hangs
|
||||||
|
local SOPS_TIMEOUT = 30 -- seconds
|
||||||
|
|
||||||
|
-- Guard against double-execution (patterns overlap, causing callback to fire twice)
|
||||||
|
local currently_saving = {}
|
||||||
|
|
||||||
-- Helper function to check if file matches secrets pattern
|
-- Helper function to check if file matches secrets pattern
|
||||||
local function is_secrets_file(filepath)
|
local function is_secrets_file(filepath)
|
||||||
for _, pattern in ipairs(secrets_patterns) do
|
for _, pattern in ipairs(secrets_patterns) do
|
||||||
@@ -38,13 +49,47 @@ vim.api.nvim_create_autocmd("BufReadPost", {
|
|||||||
|
|
||||||
-- Only decrypt if file exists and has content
|
-- Only decrypt if file exists and has content
|
||||||
if vim.fn.filereadable(filepath) == 1 and vim.fn.getfsize(filepath) > 0 then
|
if vim.fn.filereadable(filepath) == 1 and vim.fn.getfsize(filepath) > 0 then
|
||||||
|
-- Check file size before attempting to decrypt
|
||||||
|
local filesize = vim.fn.getfsize(filepath)
|
||||||
|
if filesize > MAX_SOPS_FILE_SIZE then
|
||||||
|
local size_mb = string.format("%.1f", filesize / (1024 * 1024))
|
||||||
|
local limit_mb = string.format("%.1f", MAX_SOPS_FILE_SIZE / (1024 * 1024))
|
||||||
|
vim.notify(
|
||||||
|
string.format("SOPS: File too large (%sMB > %sMB limit). Skipping decryption to prevent freeze.", size_mb, limit_mb),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Save cursor position
|
-- Save cursor position
|
||||||
local cursor_pos = vim.api.nvim_win_get_cursor(0)
|
local cursor_pos = vim.api.nvim_win_get_cursor(0)
|
||||||
|
|
||||||
-- Decrypt file content
|
-- Decrypt file content with timeout to prevent hanging
|
||||||
local result = vim.fn.system("sops --decrypt " .. vim.fn.shellescape(filepath))
|
local cmd = string.format("timeout %d sops --decrypt %s", SOPS_TIMEOUT, vim.fn.shellescape(filepath))
|
||||||
|
local result = vim.fn.system(cmd)
|
||||||
|
local exit_code = vim.v.shell_error
|
||||||
|
|
||||||
|
-- Check for timeout (exit code 124 from timeout command)
|
||||||
|
if exit_code == 124 then
|
||||||
|
vim.notify(
|
||||||
|
string.format("SOPS: Decryption timed out after %d seconds. Check your SOPS configuration and age keys.", SOPS_TIMEOUT),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if exit_code == 0 then
|
||||||
|
-- Validate decrypted content size before loading into buffer
|
||||||
|
if #result > MAX_DECRYPTED_SIZE then
|
||||||
|
local size_mb = string.format("%.1f", #result / (1024 * 1024))
|
||||||
|
local limit_mb = string.format("%.1f", MAX_DECRYPTED_SIZE / (1024 * 1024))
|
||||||
|
vim.notify(
|
||||||
|
string.format("SOPS: Decrypted content too large (%sMB > %sMB limit). Skipping to prevent freeze.", size_mb, limit_mb),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if vim.v.shell_error == 0 then
|
|
||||||
-- Replace buffer content with decrypted content
|
-- Replace buffer content with decrypted content
|
||||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, "\n"))
|
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, "\n"))
|
||||||
|
|
||||||
@@ -71,8 +116,8 @@ vim.api.nvim_create_autocmd("BufReadPost", {
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Encrypt file before writing
|
-- Override write command for secrets files
|
||||||
vim.api.nvim_create_autocmd("BufWritePre", {
|
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||||
group = sops_group,
|
group = sops_group,
|
||||||
pattern = secrets_patterns,
|
pattern = secrets_patterns,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
@@ -83,58 +128,88 @@ vim.api.nvim_create_autocmd("BufWritePre", {
|
|||||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||||
local content = table.concat(lines, "\n")
|
local content = table.concat(lines, "\n")
|
||||||
|
|
||||||
-- Encrypt content using SOPS
|
-- Check buffer content size before encrypting
|
||||||
local encrypted = vim.fn.system("sops --encrypt /dev/stdin", content)
|
if #content > MAX_DECRYPTED_SIZE then
|
||||||
|
local size_mb = string.format("%.1f", #content / (1024 * 1024))
|
||||||
|
local limit_mb = string.format("%.1f", MAX_DECRYPTED_SIZE / (1024 * 1024))
|
||||||
|
vim.notify(
|
||||||
|
string.format("SOPS: Buffer content too large (%sMB > %sMB limit). Cannot encrypt.", size_mb, limit_mb),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
-- Don't write anything, leave buffer marked as modified
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if vim.v.shell_error == 0 then
|
-- Encrypt content using SOPS via temporary file in same directory
|
||||||
|
-- This avoids /dev/stdin issues while keeping secrets secure (not in /tmp)
|
||||||
|
local dir = vim.fn.fnamemodify(filepath, ":h")
|
||||||
|
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||||
|
local temp_file = string.format("%s/.%s.sops_tmp_%d", dir, filename, os.time())
|
||||||
|
|
||||||
|
-- Write plaintext content to temp file
|
||||||
|
local temp_f, temp_err = io.open(temp_file, "w")
|
||||||
|
if not temp_f then
|
||||||
|
vim.notify("SOPS: Failed to create temp file: " .. (temp_err or "unknown error"), vim.log.levels.ERROR)
|
||||||
|
-- Don't write anything, leave buffer marked as modified
|
||||||
|
return
|
||||||
|
end
|
||||||
|
temp_f:write(content)
|
||||||
|
temp_f:close()
|
||||||
|
|
||||||
|
-- Encrypt temp file with filename override so SOPS matches .sops.yaml rules
|
||||||
|
-- Uses real filepath for rule matching, temp file for content
|
||||||
|
local cmd = string.format("sops --encrypt --filename-override %s %s",
|
||||||
|
vim.fn.shellescape(filepath),
|
||||||
|
vim.fn.shellescape(temp_file))
|
||||||
|
local encrypted = vim.fn.system(cmd)
|
||||||
|
local sops_exit_code = vim.v.shell_error
|
||||||
|
|
||||||
|
-- Always clean up temp file, even on error
|
||||||
|
os.remove(temp_file)
|
||||||
|
|
||||||
|
if sops_exit_code == 0 then
|
||||||
-- Write encrypted content directly to file
|
-- Write encrypted content directly to file
|
||||||
local file = io.open(filepath, "w")
|
local file, err = io.open(filepath, "w")
|
||||||
if file then
|
if file then
|
||||||
file:write(encrypted)
|
local success, write_err = file:write(encrypted)
|
||||||
file:close()
|
file:close()
|
||||||
|
|
||||||
-- Mark buffer as saved (prevent Vim from writing again)
|
if success then
|
||||||
|
-- Mark buffer as saved
|
||||||
vim.bo.modified = false
|
vim.bo.modified = false
|
||||||
|
|
||||||
vim.notify("SOPS: File encrypted and saved successfully", vim.log.levels.INFO)
|
vim.notify("SOPS: File encrypted and saved successfully", vim.log.levels.INFO)
|
||||||
else
|
|
||||||
vim.notify("SOPS: Failed to write encrypted file", vim.log.levels.ERROR)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
vim.notify("SOPS: Failed to encrypt file: " .. encrypted, vim.log.levels.ERROR)
|
|
||||||
-- Prevent write on encryption failure
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Prevent default write behavior since we handled it
|
-- Re-decrypt to show plaintext in buffer
|
||||||
return true
|
local decrypt_cmd = string.format("timeout %d sops --decrypt %s", SOPS_TIMEOUT, vim.fn.shellescape(filepath))
|
||||||
end
|
local decrypted = vim.fn.system(decrypt_cmd)
|
||||||
end,
|
local decrypt_exit = vim.v.shell_error
|
||||||
})
|
|
||||||
|
|
||||||
-- Re-decrypt after writing to show plaintext in buffer
|
if decrypt_exit == 0 then
|
||||||
vim.api.nvim_create_autocmd("BufWritePost", {
|
|
||||||
group = sops_group,
|
|
||||||
pattern = secrets_patterns,
|
|
||||||
callback = function(args)
|
|
||||||
local filepath = vim.fn.expand("%:p")
|
|
||||||
|
|
||||||
if is_secrets_file(filepath) and vim.fn.filereadable(filepath) == 1 then
|
|
||||||
-- Decrypt and reload buffer content
|
|
||||||
local result = vim.fn.system("sops --decrypt " .. vim.fn.shellescape(filepath))
|
|
||||||
|
|
||||||
if vim.v.shell_error == 0 then
|
|
||||||
-- Save cursor position
|
-- Save cursor position
|
||||||
local cursor_pos = vim.api.nvim_win_get_cursor(0)
|
local cursor_pos = vim.api.nvim_win_get_cursor(0)
|
||||||
|
|
||||||
-- Replace buffer with decrypted content
|
-- Replace buffer with decrypted content
|
||||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, "\n"))
|
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(decrypted, "\n"))
|
||||||
|
|
||||||
-- Mark as not modified
|
-- Mark as not modified since we just saved
|
||||||
vim.bo.modified = false
|
vim.bo.modified = false
|
||||||
|
|
||||||
-- Restore cursor position
|
-- Restore cursor position
|
||||||
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
|
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
|
||||||
|
else
|
||||||
|
vim.notify("SOPS: Could not re-decrypt after save. Buffer may show encrypted content.", vim.log.levels.WARN)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
vim.notify("SOPS: Failed to write encrypted content: " .. (write_err or "unknown error"), vim.log.levels.ERROR)
|
||||||
|
-- Don't mark as saved, keep buffer marked as modified
|
||||||
|
end
|
||||||
|
else
|
||||||
|
vim.notify("SOPS: Failed to open file for writing: " .. (err or "unknown error"), vim.log.levels.ERROR)
|
||||||
|
-- Don't mark as saved, keep buffer marked as modified
|
||||||
|
end
|
||||||
|
else
|
||||||
|
vim.notify("SOPS: Failed to encrypt file - NOT SAVED! Error: " .. encrypted, vim.log.levels.ERROR)
|
||||||
|
-- Don't write anything, leave buffer marked as modified
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|||||||
Reference in New Issue
Block a user