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:
- Access Control: Pluggable authentication (default: HTTP cookies)
- Controllers: List of addresses authorized to manage secrets
- 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
| Method | Path | Function | Description |
|---|---|---|---|
| GET/POST | /generate | generate/3 | Generate a new secret |
| POST | /import | import/3 | Import existing secret |
| GET | /list | list/3 | List hosted secrets |
| POST | /commit | commit/3 | Sign message with secret |
| GET | /export | export/3 | Export secret(s) |
| GET | /sync | sync/3 | Sync 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.
access-control(optional): Authentication device configurationkeyid(optional): Identifier for the secretpersist(optional):client,in-memory, ornon-volatilecontrollers(optional): Authorized addressesrequired-controllers(optional): Minimum signatures required
-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.
key(optional): JSON-encoded secret keycookie(optional): Cookie containing key in structured fieldsaccess-control(optional): Authentication configurationpersist(optional): Storage mode
-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.
-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
-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">>
#{
KeyID => #{
<<"key">> => JSONEncodedSecret,
<<"access-control">> => AccessControlMsg,
<<"controllers">> => [Address, ...],
<<"required-controllers">> => Integer,
<<"persist">> => PersistMode
}
}-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 fromas(optional): Identity to use for authenticationkeyids(optional): Specific keyids or<<"all">>
-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 keyidSecret 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 errorPersistence Modes
| Mode | Storage | Key Returned | Use Case |
|---|---|---|---|
client | Not stored | Yes (in cookie) | User-held keys |
in-memory | RAM only | No | Ephemeral secrets |
non-volatile | Disk (priv_store) | No | Persistent secrets |
Access Control Configuration
The default access-control device is cookie@1.0. Custom access-control messages must support:
/commit Path:
- Input: Request with
keyidin body - Output: Response with authentication setup
/verify Path:
- Base: Initialized access-control message
- Request: User's request with credentials
- Output:
true, modified request, orfalse
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
- Trust Model: Requires trusted execution environment
- Default Auth: Uses
cookie@1.0for access control - Controller Override: Controllers can bypass access-control
- Multi-Signature: Supports requiring multiple controller signatures
- Key Format: Uses JSON-encoded Arweave wallet format
- Cookie Storage: Client mode stores key in HTTP cookie
- Sync Security: Sync uses requesting identity for authorization
- Non-Volatile Store: Uses separate
priv_storeconfiguration - Idempotent Generate: Returns existing wallet if keyid matches
- Quote Escaping: Handles URL-encoded JSON in parameters