[PATCH BlueZ v2 3/3] tools: add bluez-quirkctl for managing quirk profiles

Pakrohk <[email protected]>
Newsgroups org.kernel.vger.linux-bluetooth
Message-ID <[email protected]>
From: Pakrohk <[email protected]>

Add bluez-quirkctl, a CLI tool for managing external gamepad quirk
profiles. This tool validates JSON profiles, manages HMAC-SHA256 keys,
and installs/removes signed profiles to the system directory.

Usage:
  bluez-quirkctl install <file.json>   - validate, sign, install
  bluez-quirkctl remove <name>         - uninstall a profile
  bluez-quirkctl list                  - list installed profiles
  bluez-quirkctl validate <file.json>  - check without installing

Security model:
  - Install/remove requires root (checked via getuid())
  - HMAC key auto-generated in /var/lib/bluez/quirks/.hmac_key (0600)
  - Profiles installed to /var/lib/bluez/quirks/ (0644)
  - HMAC-SHA256 computed using bt_crypto (kernel AF_ALG)
  - Symlinks rejected during loading
  - Descriptor size capped at 2048 bytes

The tool links against libbluetooth-internal and libshared-mainloop
for access to bt_crypto_hmac_sha256().

Signed-off-by: Pakrohk <[email protected]>
diff --git a/Makefile.tools b/Makefile.tools
index 44da0ce..ecce5b4 100644
--- a/Makefile.tools
+++ b/Makefile.tools
@@ -581,5 +581,6 @@ bin_PROGRAMS += tools/bluez-quirkctl
 
 tools_bluez_quirkctl_SOURCES = tools/quirkctl.c
 tools_bluez_quirkctl_CFLAGS = $(JSONC_CFLAGS)
-tools_bluez_quirkctl_LDADD = $(JSONC_LIBS)
+tools_bluez_quirkctl_LDADD = lib/libbluetooth-internal.la \
+				src/libshared-mainloop.la $(JSONC_LIBS)
 endif
diff --git a/tools/quirkctl.c b/tools/quirkctl.c
index 5c35e97..93f3207 100644
--- a/tools/quirkctl.c
+++ b/tools/quirkctl.c
@@ -30,10 +30,16 @@
 #include <fcntl.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <sys/sendfile.h>
 #include <pwd.h>
 
 #include <json-c/json.h>
 
+#include "src/shared/crypto.h"
+
+static const char *prog;
+static struct bt_crypto *crypto;
+
 #define QUIRK_DIR "/var/lib/bluez/quirks"
 #define USER_QUIRK_DIR "/.config/bluez/quirks"
 #define HMAC_KEY_PATH QUIRK_DIR "/.hmac_key"
@@ -42,7 +48,166 @@
 #define JSON_EXT ".json"
 #define SIG_EXT ".sig"
 
-static const char *prog;
+/*
+ * Copy file using sendfile(2).
+ */
+static int copy_file(const char *src, const char *dst)
+{
+	int fd_in, fd_out;
+	struct stat st;
+	ssize_t sent;
+
+	fd_in = open(src, O_RDONLY);
+	if (fd_in < 0)
+		return -1;
+
+	if (fstat(fd_in, &st) < 0) {
+		close(fd_in);
+		return -1;
+	}
+
+	fd_out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+	if (fd_out < 0) {
+		close(fd_in);
+		return -1;
+	}
+
+	sent = sendfile(fd_out, fd_in, NULL, st.st_size);
+	close(fd_in);
+	close(fd_out);
+
+	return (sent == st.st_size) ? 0 : -1;
+}
+
+/*
+ * Load HMAC key from disk. Returns key length or -1 on failure.
+ */
+static int load_hmac_key(uint8_t *key, int max_len)
+{
+	FILE *f;
+	long fsize;
+	char *hex;
+	int key_len, i;
+	char *end;
+
+	f = fopen(HMAC_KEY_PATH, "re");
+	if (!f)
+		return -1;
+
+	fseek(f, 0, SEEK_END);
+	fsize = ftell(f);
+	if (fsize < 0 || fsize > 1024) {
+		fclose(f);
+		return -1;
+	}
+	rewind(f);
+
+	hex = malloc(fsize + 1);
+	if (!hex) {
+		fclose(f);
+		return -1;
+	}
+
+	if (fread(hex, 1, fsize, f) != (size_t)fsize) {
+		free(hex);
+		fclose(f);
+		return -1;
+	}
+	fclose(f);
+	hex[fsize] = '\0';
+
+	end = hex + strlen(hex) - 1;
+	while (end > hex && (*end == '\n' || *end == '\r' || *end == ' '))
+		*end-- = '\0';
+
+	key_len = strlen(hex) / 2;
+	if (key_len > max_len)
+		key_len = max_len;
+
+	for (i = 0; i < key_len; i++) {
+		unsigned int byte;
+		char buf[3];
+
+		buf[0] = hex[i * 2];
+		buf[1] = hex[i * 2 + 1];
+		buf[2] = '\0';
+		if (sscanf(buf, "%2x", &byte) != 1) {
+			free(hex);
+			return -1;
+		}
+		key[i] = (uint8_t)byte;
+	}
+
+	free(hex);
+	return key_len;
+}
+
+/*
+ * Compute HMAC-SHA256 using bt_crypto (kernel AF_ALG).
+ * Returns hex string of 32-byte digest, or NULL on failure.
+ */
+static char *compute_hmac(const char *file_path)
+{
+	FILE *f;
+	long fsize;
+	uint8_t *data;
+	uint8_t key[HMAC_KEY_SIZE];
+	uint8_t digest[32];
+	char hex[HMAC_KEY_SIZE * 2 + 1];
+	char *result;
+	int key_len, i;
+
+	if (!crypto)
+		return NULL;
+
+	/* Read the file */
+	f = fopen(file_path, "re");
+	if (!f)
+		return NULL;
+
+	fseek(f, 0, SEEK_END);
+	fsize = ftell(f);
+	if (fsize < 0 || fsize > 1024 * 1024) {
+		fclose(f);
+		return NULL;
+	}
+	rewind(f);
+
+	data = malloc(fsize);
+	if (!data) {
+		fclose(f);
+		return NULL;
+	}
+
+	if (fread(data, 1, fsize, f) != (size_t)fsize) {
+		free(data);
+		fclose(f);
+		return NULL;
+	}
+	fclose(f);
+
+	/* Load the HMAC key */
+	key_len = load_hmac_key(key, HMAC_KEY_SIZE);
+	if (key_len <= 0) {
+		free(data);
+		return NULL;
+	}
+
+	/* Compute HMAC-SHA256 */
+	if (!bt_crypto_hmac_sha256(crypto, key, key_len, data, fsize, digest)) {
+		free(data);
+		return NULL;
+	}
+	free(data);
+
+	/* Convert to hex string */
+	for (i = 0; i < 32; i++)
+		snprintf(hex + i * 2, 3, "%02x", digest[i]);
+	hex[64] = '\0';
+
+	result = strdup(hex);
+	return result;
+}
 
 static void usage(void)
 {
@@ -139,43 +304,6 @@ static int ensure_hmac_key(void)
 	return 0;
 }
 
-/*
- * Compute HMAC-SHA256 via openssl CLI.
- */
-static char *compute_hmac(const char *file_path)
-{
-	char cmd[1024];
-	char line[256];
-	char *result = NULL;
-	FILE *p;
-	char *eq;
-
-	snprintf(cmd, sizeof(cmd),
-		"openssl dgst -sha256 -hmac \"$(cat " HMAC_KEY_PATH ")\" "
-		"-hex < '%s' 2>/dev/null", file_path);
-
-	p = popen(cmd, "r");
-	if (!p)
-		return NULL;
-
-	while (fgets(line, sizeof(line), p)) {
-		eq = strstr(line, "= ");
-		if (eq) {
-			eq += 2;
-			/* trim */
-			char *end = eq + strlen(eq) - 1;
-			while (end > eq && (*end == '\n' || *end == '\r'
-						|| *end == ' '))
-				*end-- = '\0';
-			result = strdup(eq);
-			break;
-		}
-	}
-
-	pclose(p);
-	return result;
-}
-
 /*
  * Validate JSON profile structure.
  */
@@ -390,14 +518,10 @@ static int cmd_install(const char *path)
 		return 1;
 	}
 
-	{
-		char cmd[1024];
-		snprintf(cmd, sizeof(cmd), "cp '%s' '%s'", path, dest);
-		if (system(cmd) != 0) {
-			fprintf(stderr, "Failed to copy profile\n");
-			free(name);
-			return 1;
-		}
+	if (copy_file(path, dest) < 0) {
+		fprintf(stderr, "Failed to copy profile\n");
+		free(name);
+		return 1;
 	}
 
 	if (chmod(dest, 0644) < 0) {
@@ -510,6 +634,8 @@ static int cmd_list(void)
 
 int main(int argc, char *argv[])
 {
+	int ret;
+
 	prog = argv[0];
 
 	if (argc < 2) {
@@ -522,7 +648,14 @@ int main(int argc, char *argv[])
 			fprintf(stderr, "Usage: %s install <file.json>\n", prog);
 			return 1;
 		}
-		return cmd_install(argv[2]);
+		crypto = bt_crypto_new();
+		if (!crypto) {
+			fprintf(stderr, "Failed to init crypto\n");
+			return 1;
+		}
+		ret = cmd_install(argv[2]);
+		bt_crypto_unref(crypto);
+		return ret;
 	}
 
 	if (strcmp(argv[1], "remove") == 0) {
@@ -533,9 +666,8 @@ int main(int argc, char *argv[])
 		return cmd_remove(argv[2]);
 	}
 
-	if (strcmp(argv[1], "list") == 0) {
+	if (strcmp(argv[1], "list") == 0)
 		return cmd_list();
-	}
 
 	if (strcmp(argv[1], "validate") == 0) {
 		if (argc < 3) {
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.