Skip to content

dev_secret.erl - Secret Key Management Device

Overview

Purpose: Create, export, and commit messages using node-hosted secrets
Module: dev_secret
Device Name: secret@1.0
Security: Requires pluggable access-control authentication

This device enables secure management of cryptographic secrets on a trusted node. Users can generate, import, and use secrets for message signing while maintaining access control through configurable authentication mechanisms. The default authentication uses the cookie@1.0 device.

Security Model

Secrets are protected by:

  1. Access Control: Pluggable authentication (default: HTTP cookies)
  2. Controllers: List of addresses authorized to manage secrets
  3. Persistence Modes: Client, in-memory, or non-volatile storage

This device is intended for trusted environments such as user-controlled machines or TEE-protected nodes.

Dependencies

  • HyperBEAM: hb_ao, hb_http, hb_opts, hb_message, hb_util, hb_maps, hb_cache, hb_private, hb_escape, hb_http_server
  • Arweave: ar_wallet
  • Testing: eunit
  • Includes: include/hb.hrl

API Endpoints

MethodPathFunctionDescription
GET/POST/generategenerate/3Generate a new secret
POST/importimport/3Import existing secret
GET/listlist/3List hosted secrets
POST/commitcommit/3Sign message with secret
GET/exportexport/3Export secret(s)
GET/syncsync/3Sync secrets from remote node

Public Functions Overview

%% Secret Management
-spec generate(Base, Request, Opts) -> {ok, Response} | {error, Reason}.
-spec import(Base, Request, Opts) -> {ok, Response} | {error, Reason}.
-spec list(Base, Request, Opts) -> {ok, WalletList}.
 
%% Secret Operations
-spec commit(Base, Request, Opts) -> {ok, SignedMessage} | {error, Reason}.
-spec export(Base, Request, Opts) -> {ok, ExportedSecrets} | {error, Reason}.
 
%% Synchronization
-spec sync(Base, Request, Opts) -> {ok, Response} | {error, Reason}.

Public Functions

1. generate/3

-spec generate(Base, Request, Opts) -> {ok, Response} | {error, Reason}
    when
        Base :: map(),
        Request :: map(),
        Opts :: map(),
        Response :: map(),
        Reason :: term().

Description: Generate a new secret and register it on the node. If a secret already exists for the given keyid, returns the existing wallet details. The response includes authentication setup from the access-control device.

Request Parameters:
  • access-control (optional): Authentication device configuration
  • keyid (optional): Identifier for the secret
  • persist (optional): client, in-memory, or non-volatile
  • controllers (optional): Authorized addresses
  • required-controllers (optional): Minimum signatures required
Test Code:
-module(dev_secret_generate_test).
-include_lib("eunit/include/eunit.hrl").
 
generate_in_memory_test() ->
    Node = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new()
    }),
    {ok, Response} = hb_http:get(
        Node,
        <<"/~secret@1.0/generate?persist=in-memory">>,
        #{}
    ),
    ?assert(maps:is_key(<<"body">>, Response)),
    ?assert(maps:is_key(<<"wallet-address">>, Response)).
 
generate_client_persist_test() ->
    Node = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new()
    }),
    {ok, Response} = hb_http:get(
        Node,
        <<"/~secret@1.0/generate?persist=client">>,
        #{}
    ),
    % Client persist returns cookie with wallet key
    ?assert(maps:is_key(<<"body">>, Response)).
 
generate_non_volatile_test() ->
    Node = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new()
    }),
    {ok, Response} = hb_http:get(
        Node,
        <<"/~secret@1.0/generate?persist=non-volatile">>,
        #{}
    ),
    ?assert(maps:is_key(<<"body">>, Response)).

2. import/3

-spec import(Base, Request, Opts) -> {ok, Response} | {error, Reason}
    when
        Base :: map(),
        Request :: map(),
        Opts :: map(),
        Response :: map(),
        Reason :: binary().

Description: Import an existing secret for hosting on the node. The secret can be provided directly via the key parameter or extracted from cookies.

Request Parameters:
  • key (optional): JSON-encoded secret key
  • cookie (optional): Cookie containing key in structured fields
  • access-control (optional): Authentication configuration
  • persist (optional): Storage mode
Test Code:
-module(dev_secret_import_test).
-include_lib("eunit/include/eunit.hrl").
 
import_key_test() ->
    Node = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new()
    }),
    TestWallet = ar_wallet:new(),
    WalletKey = hb_escape:encode_quotes(ar_wallet:to_json(TestWallet)),
    WalletAddress = hb_util:human_id(ar_wallet:to_address(TestWallet)),
    % Import via GET with key in URL query parameter
    ImportUrl =
        <<"/~secret@1.0/import?wallet=imported-wallet&persist=in-memory&key=",
            WalletKey/binary>>,
    {ok, ImportResponse} = hb_http:get(Node, ImportUrl, #{}),
    Imported = hb_maps:get(<<"imported">>, ImportResponse, #{}),
    ?assertMatch([WalletAddress], Imported).
 
import_no_key_error_test() ->
    Node = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new()
    }),
    Result = hb_http:post(
        Node,
        #{
            <<"path">> => <<"/~secret@1.0/import">>,
            <<"persist">> => <<"in-memory">>
        },
        #{}
    ),
    ?assertMatch({error, _}, Result).

3. list/3

-spec list(Base, Request, Opts) -> {ok, WalletList}
    when
        Base :: map(),
        Request :: map(),
        Opts :: map(),
        WalletList :: map().

Description: List all hosted secrets on the node by their keyid. Returns a map of wallet identifiers.

Test Code:
-module(dev_secret_list_test).
-include_lib("eunit/include/eunit.hrl").
 
list_wallets_test() ->
    Node = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new()
    }),
    % Generate some wallets first
    {ok, _} = hb_http:get(Node, <<"/~secret@1.0/generate?persist=in-memory">>, #{}),
    {ok, _} = hb_http:get(Node, <<"/~secret@1.0/generate?persist=in-memory">>, #{}),
    % List all
    {ok, List} = hb_http:get(Node, <<"/~secret@1.0/list">>, #{}),
    ?assert(is_map(List)).

4. commit/3

-spec commit(Base, Request, Opts) -> {ok, SignedMessage} | {error, Reason}
    when
        Base :: map(),
        Request :: map(),
        Opts :: map(),
        SignedMessage :: map(),
        Reason :: binary().

Description: Sign (commit) a message using one or more hosted secrets. Authentication is validated against each secret's access-control configuration before signing.

Request Parameters:
  • keyid (optional): Specific secret to use
  • Authentication credentials as required by access-control
Test Code:
-module(dev_secret_commit_test).
-include_lib("eunit/include/eunit.hrl").
 
commit_message_test() ->
    Node = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new()
    }),
    % Generate a client wallet to get a cookie with full wallet key
    {ok, GenResponse} =
        hb_http:get(Node, <<"/~secret@1.0/generate?persist=client">>, #{}),
    WalletName = maps:get(<<"wallet-address">>, GenResponse),
    #{ <<"priv">> := Priv } = GenResponse,
    % Use the cookie to sign a message
    TestMessage = #{
        <<"device">> => <<"secret@1.0">>,
        <<"path">> => <<"commit">>,
        <<"body">> => <<"Test data">>,
        <<"priv">> => Priv
    },
    {ok, SignedMessage} = hb_http:post(Node, TestMessage, #{}),
    % Should return the signed message with signature attached
    ?assertEqual([WalletName], hb_message:signers(SignedMessage, #{})).

5. export/3

-spec export(Base, Request, Opts) -> {ok, ExportedSecrets} | {error, Reason}
    when
        Base :: map(),
        Request :: map(),
        Opts :: map(),
        ExportedSecrets :: map(),
        Reason :: term().

Description: Export one or more secrets. Requires passing either the access-control authentication or controller signature verification.

Request Parameters:
  • keyids (optional): List of keyids to export, or <<"all">>
Export Response Structure:
#{
    KeyID => #{
        <<"key">> => JSONEncodedSecret,
        <<"access-control">> => AccessControlMsg,
        <<"controllers">> => [Address, ...],
        <<"required-controllers">> => Integer,
        <<"persist">> => PersistMode
    }
}
Test Code:
-module(dev_secret_export_test).
-include_lib("eunit/include/eunit.hrl").
 
export_single_wallet_test() ->
    AdminWallet = ar_wallet:new(),
    Node = hb_http_server:start_node(#{
        priv_wallet => AdminWallet
    }),
    AdminOpts = #{ priv_wallet => AdminWallet },
    % Generate a wallet
    {ok, GenResponse} = hb_http:get(
        Node,
        <<"/~secret@1.0/generate?persist=in-memory">>,
        #{}
    ),
    KeyID = maps:get(<<"body">>, GenResponse),
    % Export it
    {ok, ExportResponse} = hb_http:get(
        Node,
        (hb_message:commit(#{
            <<"device">> => <<"secret@1.0">>,
            <<"keyids">> => [KeyID]
        }, AdminOpts))#{ <<"path">> => <<"/~secret@1.0/export">> },
        #{}
    ),
    ?assert(is_map(ExportResponse)).
 
export_all_wallets_test() ->
    AdminWallet = ar_wallet:new(),
    Node = hb_http_server:start_node(#{
        priv_wallet => AdminWallet
    }),
    AdminOpts = #{ priv_wallet => AdminWallet },
    % Generate wallets
    {ok, _} = hb_http:get(Node, <<"/~secret@1.0/generate?persist=in-memory">>, #{}),
    {ok, _} = hb_http:get(Node, <<"/~secret@1.0/generate?persist=in-memory">>, #{}),
    % Export all
    {ok, ExportResponse} = hb_http:get(
        Node,
        (hb_message:commit(#{
            <<"device">> => <<"secret@1.0">>,
            <<"keyids">> => <<"all">>
        }, AdminOpts))#{ <<"path">> => <<"/~secret@1.0/export">> },
        #{}
    ),
    ?assert(is_map(ExportResponse)).

6. sync/3

-spec sync(Base, Request, Opts) -> {ok, Response} | {error, Reason}
    when
        Base :: map(),
        Request :: map(),
        Opts :: map(),
        Response :: map(),
        Reason :: term().

Description: Synchronize secrets from a remote node. Downloads and imports secrets that the requesting identity is authorized to access.

Request Parameters:
  • node: Remote node URL to sync from
  • as (optional): Identity to use for authentication
  • keyids (optional): Specific keyids or <<"all">>
Test Code:
-module(dev_secret_sync_test).
-include_lib("eunit/include/eunit.hrl").
 
sync_wallets_test() ->
    Node1Wallet = ar_wallet:new(),
    Node1 = hb_http_server:start_node(#{
        priv_wallet => Node1Wallet
    }),
    Node2 = hb_http_server:start_node(#{
        priv_wallet => ar_wallet:new(),
        wallet_admin => hb_util:human_id(Node1Wallet)
    }),
    % Generate on Node2
    {ok, GenResponse} = hb_http:get(
        Node2,
        <<"/~secret@1.0/generate?persist=in-memory">>,
        #{}
    ),
    _KeyID = maps:get(<<"body">>, GenResponse),
    % Sync to Node1
    {ok, _} = hb_http:get(
        Node1,
        <<"/~secret@1.0/sync?node=", Node2/binary, "&wallets=all">>,
        #{}
    ),
    % Verify on Node1
    {ok, WalletList} = hb_http:get(Node1, <<"/~secret@1.0/list">>, #{}),
    ?assert(is_map(WalletList)).

Authentication Flow

Secret Generation

1. Device creates secret, determines committer address
2. Invokes access-control message with 'commit' path
3. Access-control sets up authentication (e.g., cookies)
4. Device stores secret with metadata
5. Returns access-control response with keyid

Secret Operations

1. Device retrieves stored access-control for secret
2. Calls access-control with 'verify' path
3. Access-control validates request
4. If verified, operation proceeds
5. If failed, returns 400 error

Persistence Modes

ModeStorageKey ReturnedUse Case
clientNot storedYes (in cookie)User-held keys
in-memoryRAM onlyNoEphemeral secrets
non-volatileDisk (priv_store)NoPersistent secrets

Access Control Configuration

The default access-control device is cookie@1.0. Custom access-control messages must support:

/commit Path:
  • Input: Request with keyid in body
  • Output: Response with authentication setup
/verify Path:
  • Base: Initialized access-control message
  • Request: User's request with credentials
  • Output: true, modified request, or false

Common Patterns

%% Generate and use a secret
{ok, GenResp} = hb_http:get(Node, <<"/~secret@1.0/generate?persist=in-memory">>, #{}),
KeyID = maps:get(<<"body">>, GenResp),
 
%% Sign a message with the secret
SignedMsg = hb_http:post(
    Node,
    #{
        <<"path">> => <<"/~secret@1.0/commit">>,
        <<"keyid">> => KeyID,
        <<"data">> => <<"Important message">>
    },
    Opts
).
 
%% Export for backup
{ok, Exported} = hb_http:get(
    Node,
    (hb_message:commit(#{
        <<"keyids">> => [KeyID]
    }, AdminOpts))#{ <<"path">> => <<"/~secret@1.0/export">> },
    #{}
).
 
%% Sync between nodes
{ok, _} = hb_http:get(
    LocalNode,
    <<"/~secret@1.0/sync?node=", RemoteNode/binary, "&wallets=all">>,
    #{}
).

Wallet Details Structure

#{
    <<"wallet">> => JSONEncodedPrivateKey,
    <<"address">> => HumanReadableAddress,
    <<"persist">> => PersistMode,
    <<"access-control">> => InitializedAuthMsg,
    <<"committer">> => CommitterIdentifier,
    <<"controllers">> => [ControllerAddresses],
    <<"required-controllers">> => MinSignatures
}

References

  • Cookie Device - dev_cookie.erl
  • Message Handling - hb_message.erl
  • HTTP Server - hb_http_server.erl
  • Wallet Functions - ar_wallet.erl
  • Private Storage - hb_private.erl

Notes

  1. Trust Model: Requires trusted execution environment
  2. Default Auth: Uses cookie@1.0 for access control
  3. Controller Override: Controllers can bypass access-control
  4. Multi-Signature: Supports requiring multiple controller signatures
  5. Key Format: Uses JSON-encoded Arweave wallet format
  6. Cookie Storage: Client mode stores key in HTTP cookie
  7. Sync Security: Sync uses requesting identity for authorization
  8. Non-Volatile Store: Uses separate priv_store configuration
  9. Idempotent Generate: Returns existing wallet if keyid matches
  10. Quote Escaping: Handles URL-encoded JSON in parameters