Skip to main content

src/barrel_p2p_file.erl

%%% -*- erlang -*-
%%% Copyright (c) 2026 Benoit Chesneau
%%% SPDX-License-Identifier: Apache-2.0
%%%
%%% Shared filesystem helpers.
%%%
%%% `write_secure/2' is the atomic-write-with-restrictive-permissions
%%% primitive used by every on-disk secret in the tree (trust-store
%%% pins, Ed25519 keypair, TLS private key). Two race-closures:
%%%
%%%   1. Permissions are set on the temp file *before* any plaintext
%%%      bytes are written, so the file never lives on disk with
%%%      world-readable mode while it contains secret data.
%%%
%%%   2. The reveal is a `file:rename/2', which is atomic within a
%%%      single filesystem on the POSIX targets we support. A crash
%%%      mid-write leaves the temp file behind, never a half-written
%%%      destination.
%%%
-module(barrel_p2p_file).

-export([write_secure/2]).

%% @doc Atomically write `Data' to `Path' with 0600 permissions.
%% Writes through a `Path ++ ".tmp"' shadow, chmods the empty file
%% to 0600, then writes the payload and renames. Cleans up the
%% temp file on any failure.
-spec write_secure(file:name_all(), iodata()) -> ok | {error, term()}.
write_secure(Path, Data) ->
    TmpPath = iolist_to_binary([Path, <<".tmp">>]),
    case file:open(TmpPath, [write, raw, binary]) of
        {ok, F} ->
            try
                ok = file:change_mode(TmpPath, 8#600),
                ok = file:write(F, Data),
                ok = file:close(F),
                case file:rename(TmpPath, Path) of
                    ok ->
                        ok;
                    {error, _} = E ->
                        _ = file:delete(TmpPath),
                        E
                end
            catch
                _:Reason ->
                    catch file:close(F),
                    _ = file:delete(TmpPath),
                    {error, Reason}
            end;
        {error, _} = E ->
            E
    end.