diff --git a/README.md b/README.md
index f964bc1..2a1e259 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ with.
- `m2pack list`
- `m2pack verify`
- `m2pack extract`
+- `m2pack export-client-config`
## Build
@@ -68,6 +69,15 @@ Extract:
--key keys/master.key
```
+Export a client config header for `m2dev-client-src/src/PackLib/M2PackKeys.h`:
+
+```bash
+./build/m2pack export-client-config \
+ --key keys/master.key \
+ --public-key keys/signing.pub \
+ --output /path/to/m2dev-client-src/src/PackLib/M2PackKeys.h
+```
+
## Format summary
- Single archive file with a fixed header
diff --git a/docs/client-integration.md b/docs/client-integration.md
index b917557..d4f4073 100644
--- a/docs/client-integration.md
+++ b/docs/client-integration.md
@@ -19,6 +19,23 @@ tool.
- machine-bound cache
- or a derived release secret
+## Client key header
+
+For the current implementation, the client expects:
+
+- `src/PackLib/M2PackKeys.h`
+
+Generate it from the release key material with:
+
+```bash
+m2pack export-client-config \
+ --key keys/master.key \
+ --public-key keys/signing.pub \
+ --output /path/to/m2dev-client-src/src/PackLib/M2PackKeys.h
+```
+
+That keeps loader constants aligned with the archive builder.
+
## Runtime validation
Minimum validation:
diff --git a/src/cli.cpp b/src/cli.cpp
index 2182abd..0d1860f 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -102,7 +102,8 @@ void print_usage()
<< " build --input
--output --key --sign-secret-key [--json]\n"
<< " list --archive [--json]\n"
<< " verify --archive [--public-key ] [--key ] [--json]\n"
- << " extract --archive --output --key [--json]\n";
+ << " extract --archive --output --key [--json]\n"
+ << " export-client-config --key --public-key --output [--json]\n";
}
void command_keygen(const ParsedArgs& args)
@@ -261,6 +262,70 @@ void command_extract(const ParsedArgs& args)
std::cout << "Extracted " << archive.entries.size() << " files\n";
}
+void command_export_client_config(const ParsedArgs& args)
+{
+ const auto master = load_hex_file(require_option(args, "key"));
+ const auto public_key = load_hex_file(require_option(args, "public-key"));
+ const auto output_path = require_option(args, "output");
+
+ if (master.size() != kAeadKeySize)
+ {
+ fail("Master key must be 32 bytes");
+ }
+
+ if (public_key.size() != crypto_sign_PUBLICKEYBYTES)
+ {
+ fail("Signing public key has invalid size");
+ }
+
+ auto render_array = [](const std::vector& bytes) {
+ std::ostringstream out;
+ for (std::size_t i = 0; i < bytes.size(); ++i)
+ {
+ if (i % 8 == 0)
+ {
+ out << "\n\t";
+ }
+ out << "0x" << to_hex(&bytes[i], 1);
+ if (i + 1 != bytes.size())
+ {
+ out << ", ";
+ }
+ }
+ out << "\n";
+ return out.str();
+ };
+
+ std::ostringstream header;
+ header
+ << "#pragma once\n\n"
+ << "#include \n"
+ << "#include \n\n"
+ << "// Generated by m2pack export-client-config.\n"
+ << "// Do not edit manually.\n\n"
+ << "constexpr std::array M2PACK_MASTER_KEY = {"
+ << render_array(master)
+ << "};\n\n"
+ << "constexpr std::array M2PACK_SIGN_PUBLIC_KEY = {"
+ << render_array(public_key)
+ << "};\n";
+
+ const auto text = header.str();
+ write_file(output_path, std::vector(text.begin(), text.end()));
+
+ if (args.json)
+ {
+ std::cout
+ << "{"
+ << "\"ok\":true,"
+ << "\"output\":\"" << json_escape(output_path) << "\""
+ << "}\n";
+ return;
+ }
+
+ std::cout << "Wrote client config to " << output_path << "\n";
+}
+
} // namespace
int run_cli(int argc, char** argv)
@@ -300,6 +365,11 @@ int run_cli(int argc, char** argv)
command_extract(args);
return 0;
}
+ if (args.command == "export-client-config")
+ {
+ command_export_client_config(args);
+ return 0;
+ }
if (args.command == "help" || args.command == "--help" || args.command == "-h")
{
print_usage();