Update davemhammer/gocryptfs to 1.3.1 (#326)
Secure short-lived password files under a private 0700 directory (under /dev/shm when available) before write, closing a umask/chmod race.
This commit is contained in:
+1
-1
@@ -94,7 +94,7 @@ noctalia msg plugin davemhammer/gocryptfs:service all automount
|
|||||||
|
|
||||||
On mount / auto-mount, the service prefers the session key; if missing, it hydrates from `secret-tool` into `keyctl`, then runs `gocryptfs -extpass keyctl pipe <id>`. Fallback: `gocryptfs -extpass secret-tool lookup …`.
|
On mount / auto-mount, the service prefers the session key; if missing, it hydrates from `secret-tool` into `keyctl`, then runs `gocryptfs -extpass keyctl pipe <id>`. Fallback: `gocryptfs -extpass secret-tool lookup …`.
|
||||||
|
|
||||||
- One-shot typed passwords use a short-lived file under `/dev/shm` (tmpfs) when available, then delete it.
|
- One-shot typed passwords use a short-lived file under a **private** tmpfs dir (`/dev/shm/noctalia-gocryptfs.$USER`, mode `0700`) when available, then delete it. Parent mode blocks other local users even if the file briefly inherits umask.
|
||||||
- Optional **advanced** passfile paths remain supported for users who manage their own files (plaintext by user choice; not recommended).
|
- Optional **advanced** passfile paths remain supported for users who manage their own files (plaintext by user choice; not recommended).
|
||||||
- **Forget** and volume remove clear both the desktop keyring entry and the session key.
|
- **Forget** and volume remove clear both the desktop keyring entry and the session key.
|
||||||
- Passwords are not logged.
|
- Passwords are not logged.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
id = "davemhammer/gocryptfs"
|
id = "davemhammer/gocryptfs"
|
||||||
name = "Gocryptfs"
|
name = "Gocryptfs"
|
||||||
version = "1.3.0"
|
version = "1.3.1"
|
||||||
plugin_api = 10
|
plugin_api = 10
|
||||||
author = "davemhammer"
|
author = "davemhammer"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+113
-63
@@ -504,20 +504,57 @@ local function removePassfile(path)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Short-lived password material on tmpfs when possible (never long-term secret storage).
|
-- Private dir for short-lived password material. Prefer tmpfs (/dev/shm) when present.
|
||||||
local function tempPassPath(suffix)
|
-- Parent is mode 0700 (like lyrics' requestDir chmod) so a world-listable /dev/shm (1777)
|
||||||
|
-- cannot expose files even if writeFile lands at umask 0644 before a per-file chmod.
|
||||||
|
local secureTempDirPath = nil
|
||||||
|
local secureTempDirReady = false
|
||||||
|
|
||||||
|
local function secureTempDir()
|
||||||
|
if secureTempDirPath and noctalia.fileExists(secureTempDirPath) and secureTempDirReady then
|
||||||
|
return secureTempDirPath
|
||||||
|
end
|
||||||
local base = "/dev/shm"
|
local base = "/dev/shm"
|
||||||
if not noctalia.fileExists(base) then
|
if not noctalia.fileExists(base) then
|
||||||
base = noctalia.pluginDataDir() or "/tmp"
|
base = noctalia.pluginDataDir() or "/tmp"
|
||||||
end
|
end
|
||||||
return base .. "/noctalia-gocryptfs-" .. tostring(suffix)
|
-- Per-user private subdirectory under the base (not a flat file on world-writable shm).
|
||||||
|
local dir = base .. "/noctalia-gocryptfs." .. tostring(os.getenv("USER") or "user")
|
||||||
|
noctalia.mkdirAll(dir)
|
||||||
|
secureTempDirPath = dir
|
||||||
|
return dir
|
||||||
end
|
end
|
||||||
|
|
||||||
local function writeSecurePassfile(path, password)
|
-- Ensure dir is 0700, then run callback(dir). Same pattern as lyrics_service securing requestDir.
|
||||||
local written, werr = noctalia.writeFile(path, password .. "\n")
|
local function withSecureTempDir(callback)
|
||||||
|
local dir = secureTempDir()
|
||||||
|
noctalia.runAsync(shellCommand({ "chmod", "700", dir }), function(result)
|
||||||
|
local ok = result ~= nil and result.exitCode == 0 and not result.timedOut
|
||||||
|
if ok then
|
||||||
|
secureTempDirReady = true
|
||||||
|
else
|
||||||
|
-- Still proceed under pluginDataDir/tmp if chmod failed; log for diagnostics.
|
||||||
|
noctalia.log(`gocryptfs: chmod 700 on temp dir failed: {dir}`)
|
||||||
|
end
|
||||||
|
callback(dir)
|
||||||
|
end, 5000)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tempPassPath(suffix)
|
||||||
|
return secureTempDir() .. "/p-" .. tostring(suffix)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write password material under the private temp dir. withNewline defaults true (gocryptfs -passfile).
|
||||||
|
local function writeSecurePassfile(path, password, withNewline)
|
||||||
|
local body = password
|
||||||
|
if withNewline ~= false then
|
||||||
|
body = password .. "\n"
|
||||||
|
end
|
||||||
|
local written, werr = noctalia.writeFile(path, body)
|
||||||
if not written then
|
if not written then
|
||||||
return false, werr or "write failed"
|
return false, werr or "write failed"
|
||||||
end
|
end
|
||||||
|
-- Defense in depth; parent dir 0700 is the real multi-user protection.
|
||||||
noctalia.runAsync(shellCommand({ "chmod", "600", path }))
|
noctalia.runAsync(shellCommand({ "chmod", "600", path }))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -553,13 +590,13 @@ local function storeSessionKeyring(volumeId, password, callback)
|
|||||||
callback(false, "invalid keyring store")
|
callback(false, "invalid keyring store")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
withSecureTempDir(function()
|
||||||
local tmp = tempPassPath("kr-" .. tostring(volumeId) .. "-" .. tostring(os.time()))
|
local tmp = tempPassPath("kr-" .. tostring(volumeId) .. "-" .. tostring(os.time()))
|
||||||
local written, werr = noctalia.writeFile(tmp, password)
|
local written, werr = writeSecurePassfile(tmp, password, false)
|
||||||
if not written then
|
if not written then
|
||||||
callback(false, werr or "temp write failed")
|
callback(false, werr or "temp write failed")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
noctalia.runAsync(shellCommand({ "chmod", "600", tmp }), function()
|
|
||||||
local cmd = "OLD=$(keyctl search @u user "
|
local cmd = "OLD=$(keyctl search @u user "
|
||||||
.. shellQuote(desc)
|
.. shellQuote(desc)
|
||||||
.. " 2>/dev/null); "
|
.. " 2>/dev/null); "
|
||||||
@@ -599,14 +636,14 @@ local function storePersistentPassword(volumeId, password, callback)
|
|||||||
callback(false, "invalid persistent store")
|
callback(false, "invalid persistent store")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local label = "noctalia-gocryptfs:" .. vid
|
||||||
|
withSecureTempDir(function()
|
||||||
local tmp = tempPassPath("sec-" .. vid .. "-" .. tostring(os.time()))
|
local tmp = tempPassPath("sec-" .. vid .. "-" .. tostring(os.time()))
|
||||||
local written, werr = noctalia.writeFile(tmp, password)
|
local written, werr = writeSecurePassfile(tmp, password, false)
|
||||||
if not written then
|
if not written then
|
||||||
callback(false, werr or "temp write failed")
|
callback(false, werr or "temp write failed")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local label = "noctalia-gocryptfs:" .. vid
|
|
||||||
noctalia.runAsync(shellCommand({ "chmod", "600", tmp }), function()
|
|
||||||
-- Clear any previous entry first so store does not leave duplicates.
|
-- Clear any previous entry first so store does not leave duplicates.
|
||||||
local cmd = "secret-tool clear service "
|
local cmd = "secret-tool clear service "
|
||||||
.. shellQuote(SECRET_SERVICE)
|
.. shellQuote(SECRET_SERVICE)
|
||||||
@@ -867,8 +904,60 @@ mountVolume = function(command)
|
|||||||
local storeKeyring = command.storeKeyring == true or useKeyring
|
local storeKeyring = command.storeKeyring == true or useKeyring
|
||||||
local mountCmd = nil -- full shell when using keyring (search + extpass)
|
local mountCmd = nil -- full shell when using keyring (search + extpass)
|
||||||
|
|
||||||
|
local function launchMount()
|
||||||
|
if mountCmd == nil then
|
||||||
|
table.insert(args, cipher)
|
||||||
|
table.insert(args, mount)
|
||||||
|
mountCmd = shellCommand(args)
|
||||||
|
end
|
||||||
|
|
||||||
|
actionBusy = true
|
||||||
|
publishSnapshot()
|
||||||
|
|
||||||
|
local launched = noctalia.runAsync(mountCmd, function(result)
|
||||||
|
removePassfile(tempPassfile)
|
||||||
|
local ok = result ~= nil and result.exitCode == 0 and not result.timedOut
|
||||||
|
local message
|
||||||
|
if ok then
|
||||||
|
vol.mounted = true
|
||||||
|
message = noctalia.tr("result.mounted", { name = vol.name })
|
||||||
|
if password ~= "" and storeKeyring then
|
||||||
|
storeKeyringPassword(vol.id, password, function(krOk, krErr)
|
||||||
|
if krOk then
|
||||||
|
vol.useKeyring = true
|
||||||
|
saveVolumes()
|
||||||
|
publishSnapshot()
|
||||||
|
else
|
||||||
|
noctalia.log(`gocryptfs: keyring store failed: {krErr}`)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local err = trim(result and (result.stderr ~= "" and result.stderr or result.stdout))
|
||||||
|
if err == "" then
|
||||||
|
if result and result.exitCode == 2 and useKeyring and password == "" then
|
||||||
|
err = "keyring password missing (Remember once, or unlock desktop keyring)"
|
||||||
|
else
|
||||||
|
err = result and result.timedOut and "timed out" or "unknown error"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
message = noctalia.tr("result.failed", { error = err })
|
||||||
|
end
|
||||||
|
finishAction(command, ok, message)
|
||||||
|
end, 60000)
|
||||||
|
|
||||||
|
if not launched then
|
||||||
|
removePassfile(tempPassfile)
|
||||||
|
actionBusy = false
|
||||||
|
actionResult(command, false, noctalia.tr("result.failed", { error = "could not start gocryptfs" }))
|
||||||
|
publishSnapshot()
|
||||||
|
kickAutoMount()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if password ~= "" then
|
if password ~= "" then
|
||||||
-- One-shot: password via tmpfs temp passfile (deleted after mount).
|
-- One-shot: password via private temp dir (chmod 700 parent first — lyrics-style).
|
||||||
|
withSecureTempDir(function()
|
||||||
tempPassfile = tempPassPath("pass-" .. vol.id .. "-" .. tostring(os.time()))
|
tempPassfile = tempPassPath("pass-" .. vol.id .. "-" .. tostring(os.time()))
|
||||||
local written, werr = writeSecurePassfile(tempPassfile, password)
|
local written, werr = writeSecurePassfile(tempPassfile, password)
|
||||||
if not written then
|
if not written then
|
||||||
@@ -878,6 +967,9 @@ mountVolume = function(command)
|
|||||||
end
|
end
|
||||||
table.insert(args, "-passfile")
|
table.insert(args, "-passfile")
|
||||||
table.insert(args, tempPassfile)
|
table.insert(args, tempPassfile)
|
||||||
|
launchMount()
|
||||||
|
end)
|
||||||
|
return
|
||||||
elseif useKeyring then
|
elseif useKeyring then
|
||||||
if not keyctlAvailable() and not secretToolAvailable() then
|
if not keyctlAvailable() and not secretToolAvailable() then
|
||||||
local msg = noctalia.tr("result.failed", {
|
local msg = noctalia.tr("result.failed", {
|
||||||
@@ -996,55 +1088,7 @@ mountVolume = function(command)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if mountCmd == nil then
|
launchMount()
|
||||||
table.insert(args, cipher)
|
|
||||||
table.insert(args, mount)
|
|
||||||
mountCmd = shellCommand(args)
|
|
||||||
end
|
|
||||||
|
|
||||||
actionBusy = true
|
|
||||||
publishSnapshot()
|
|
||||||
|
|
||||||
local launched = noctalia.runAsync(mountCmd, function(result)
|
|
||||||
removePassfile(tempPassfile)
|
|
||||||
local ok = result ~= nil and result.exitCode == 0 and not result.timedOut
|
|
||||||
local message
|
|
||||||
if ok then
|
|
||||||
vol.mounted = true
|
|
||||||
message = noctalia.tr("result.mounted", { name = vol.name })
|
|
||||||
-- After a typed-password mount, keep password in keyring for this login session.
|
|
||||||
if password ~= "" and storeKeyring then
|
|
||||||
storeKeyringPassword(vol.id, password, function(krOk, krErr)
|
|
||||||
if krOk then
|
|
||||||
vol.useKeyring = true
|
|
||||||
saveVolumes()
|
|
||||||
publishSnapshot()
|
|
||||||
else
|
|
||||||
noctalia.log(`gocryptfs: keyring store failed: {krErr}`)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local err = trim(result and (result.stderr ~= "" and result.stderr or result.stdout))
|
|
||||||
if err == "" then
|
|
||||||
if result and result.exitCode == 2 and useKeyring and password == "" then
|
|
||||||
err = "keyring password missing (Remember once, or unlock desktop keyring)"
|
|
||||||
else
|
|
||||||
err = result and result.timedOut and "timed out" or "unknown error"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
message = noctalia.tr("result.failed", { error = err })
|
|
||||||
end
|
|
||||||
finishAction(command, ok, message)
|
|
||||||
end, 60000)
|
|
||||||
|
|
||||||
if not launched then
|
|
||||||
removePassfile(tempPassfile)
|
|
||||||
actionBusy = false
|
|
||||||
actionResult(command, false, noctalia.tr("result.failed", { error = "could not start gocryptfs" }))
|
|
||||||
publishSnapshot()
|
|
||||||
kickAutoMount()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
kickAutoMount = function()
|
kickAutoMount = function()
|
||||||
@@ -1354,10 +1398,16 @@ local function initVolume(command)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
actionBusy = true
|
||||||
|
publishSnapshot()
|
||||||
|
|
||||||
|
withSecureTempDir(function()
|
||||||
local tempPassfile = tempPassPath("init-" .. vol.id .. "-" .. tostring(os.time()))
|
local tempPassfile = tempPassPath("init-" .. vol.id .. "-" .. tostring(os.time()))
|
||||||
local written, werr = writeSecurePassfile(tempPassfile, password)
|
local written, werr = writeSecurePassfile(tempPassfile, password)
|
||||||
if not written then
|
if not written then
|
||||||
|
actionBusy = false
|
||||||
actionResult(command, false, noctalia.tr("result.failed", { error = werr or "could not write passfile" }))
|
actionResult(command, false, noctalia.tr("result.failed", { error = werr or "could not write passfile" }))
|
||||||
|
publishSnapshot()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1370,9 +1420,6 @@ local function initVolume(command)
|
|||||||
end
|
end
|
||||||
table.insert(args, cipher)
|
table.insert(args, cipher)
|
||||||
|
|
||||||
actionBusy = true
|
|
||||||
publishSnapshot()
|
|
||||||
|
|
||||||
local launched = noctalia.runAsync(shellCommand(args), function(result)
|
local launched = noctalia.runAsync(shellCommand(args), function(result)
|
||||||
removePassfile(tempPassfile)
|
removePassfile(tempPassfile)
|
||||||
local ok = result ~= nil and result.exitCode == 0 and not result.timedOut
|
local ok = result ~= nil and result.exitCode == 0 and not result.timedOut
|
||||||
@@ -1418,6 +1465,7 @@ local function initVolume(command)
|
|||||||
actionResult(command, false, noctalia.tr("result.failed", { error = "could not start gocryptfs -init" }))
|
actionResult(command, false, noctalia.tr("result.failed", { error = "could not start gocryptfs -init" }))
|
||||||
publishSnapshot()
|
publishSnapshot()
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Store password in kernel keyring for an existing volume (no mount required).
|
-- Store password in kernel keyring for an existing volume (no mount required).
|
||||||
@@ -1562,6 +1610,8 @@ end
|
|||||||
|
|
||||||
-- boot
|
-- boot
|
||||||
loadVolumes()
|
loadVolumes()
|
||||||
|
-- Prime private temp dir (0700) early so first mount/keyring write is not a race.
|
||||||
|
withSecureTempDir(function() end)
|
||||||
noctalia.state.watch(COMMAND_KEY, executeAction)
|
noctalia.state.watch(COMMAND_KEY, executeAction)
|
||||||
noctalia.setUpdateInterval(refreshIntervalMs())
|
noctalia.setUpdateInterval(refreshIntervalMs())
|
||||||
refreshAll()
|
refreshAll()
|
||||||
|
|||||||
Reference in New Issue
Block a user