From 5d401238185f56d7791d0850e4f83d6b1cf2ec7d Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Fri, 14 Mar 2025 14:05:06 +0100 Subject: [PATCH] provision: add script for managing device provisioning data This is useful for keeping specific data on a device across factory reset. It uses a separate partition (only UBI supported at the moment) to store the data. The primary use case is storing sensitive data like cryptographic keys for maintaining a device as part of a network. Signed-off-by: Felix Fietkau --- package/utils/provision/Makefile | 33 +++ .../utils/provision/files/usr/sbin/provision | 107 ++++++++++ .../files/usr/share/ucode/provision.uc | 189 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 package/utils/provision/Makefile create mode 100755 package/utils/provision/files/usr/sbin/provision create mode 100644 package/utils/provision/files/usr/share/ucode/provision.uc diff --git a/package/utils/provision/Makefile b/package/utils/provision/Makefile new file mode 100644 index 00000000000..49c57379b62 --- /dev/null +++ b/package/utils/provision/Makefile @@ -0,0 +1,33 @@ +# +# Copyright (C) 2025 OpenWrt.org +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=provision +PKG_RELEASE:=$(AUTORELEASE) + +PKG_LICENSE:=GPL-2.0 +PKG_MAINTAINER:=Felix Fietkau + +include $(INCLUDE_DIR)/package.mk + +define Package/provision + SECTION:=utils + CATEGORY:=Utilities + TITLE:=Utility for managing device provisioning data + DEPENDS:=+ucode +ucode-mod-fs +ucode-mod-struct +endef + +define Build/Compile + : +endef + +define Package/provision/install + $(CP) ./files/* $(1)/ +endef + +$(eval $(call BuildPackage,provision)) diff --git a/package/utils/provision/files/usr/sbin/provision b/package/utils/provision/files/usr/sbin/provision new file mode 100755 index 00000000000..49542e5b8d2 --- /dev/null +++ b/package/utils/provision/files/usr/sbin/provision @@ -0,0 +1,107 @@ +#!/usr/bin/env ucode +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2025 Felix Fietkau + */ +'use strict'; +import { basename } from "fs"; +import * as provision from "provision"; + +const usage_message = `Usage: ${basename(sourcepath())} [] + + Commands: + - get []: Get string value at (or all if no path given) + - set Set string value at to + - get_json []: Get JSON data at (or all if no path given) + - set_json Set JSON value at to + - delete Delete value at + - reset Clear provision data + - create Create provisioning partition + - destroy Destroy provisioning partition + +`; + + +function usage() +{ + warn(usage_message); + exit(1); +} + +if (!length(ARGV)) + usage(); + +const create_error_msg = `Provisioning partition could not be created. +This is only supported on devices with UBI for now. +If there was not enough space, please reflash using the sysugrade -P parameter +`; + +let ctx; +if (ARGV[0] == "create") { + ctx = provision.create(); + if (!ctx) { + warn(create_error_msg); + exit(1); + } + ctx.reset(); + ctx.commit(); + exit(0); +} else { + ctx = provision.open(); + if (!ctx) { + warn(`Provisioning partition not found. Try ${basename(sourcepath())} enable\n`); + exit(1); + } +} +ctx.init(); + +let cmd = shift(ARGV); +switch (cmd) { +case "get": + let val = ctx.get(ARGV[0]); + val ??= ""; + print(val + "\n"); + break; +case "get_json": + printf("%.J\n", ctx.get(ARGV[0])); + break; +case "set_json": + if (length(ARGV) != 2) + usage(); + ARGV[1] = json(ARGV[1]); + if (ARGV[1] == null) { + warn('Invalid JSON argument\n'); + exit(1); + } + // fallthrough +case "set": + if (length(ARGV) != 2) + usage(); + + if (!ctx.set(ARGV[0], ARGV[1])) { + warn('Set failed\n'); + exit(1); + } + + ctx.commit(); + break; +case "delete": + if (length(ARGV) != 1) + usage(); + + if (!ctx.set(ARGV[0])) { + warn('Delete failed\n'); + exit(1); + } + ctx.commit(); + break; +case "reset": + ctx.reset(); + ctx.commit(); + break; +case "destroy": + ctx.destroy(); + break; +default: + usage(); +} diff --git a/package/utils/provision/files/usr/share/ucode/provision.uc b/package/utils/provision/files/usr/share/ucode/provision.uc new file mode 100644 index 00000000000..3f43331d00e --- /dev/null +++ b/package/utils/provision/files/usr/share/ucode/provision.uc @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2025 Felix Fietkau + */ +'use strict'; +import * as struct from "struct"; +import * as fs from "fs"; + +const MAGIC = 0xf09f8697; +const HDR_LEN = 9; + +let hdr = struct.new(">LLc"); + +const ubi_proto = { + read: function() { + let file = fs.open(this.dev); + if (!file) + return; + + let hdr_data = file.read(HDR_LEN); + if (!hdr_data) + return; + + hdr_data = hdr.unpack(hdr_data); + if (!hdr_data) + return; + + if (hdr_data[0] != MAGIC) + return; + + if (hdr_data[1] > 131072 || hdr_data[2] != 0) + return; + + let data = file.read(hdr_data[1]); + if (length(data) != hdr_data[1]) + return; + + return data; + }, + commit: function(data) { + let len = HDR_LEN + length(data); + + let file = fs.popen(`ubiupdatevol ${this.dev} -s ${len} -`, "w"); + file.write(hdr.pack(MAGIC, length(data), 0)); + file.write(data); + + return file.close() == 0; + }, + destroy: function() { + let dev = replace(this.dev, /_\d+$/, ""); + return system(`ubirmvol ${dev} -N provisioning`) == 0; + } +}; + +function open_ubi() +{ + let found = fs.glob("/sys/class/ubi/*/name"); + found = filter(found, (v) => trim(fs.readfile(v)) == "provisioning"); + if (!length(found)) + return; + + let dev_name = fs.basename(fs.dirname(found[0])); + + return proto({ + dev: "/dev/" + dev_name, + }, ubi_proto); +} + +function create_ubi() +{ + let ctx = open_ubi(); + if (ctx) + return ctx; + + let found = fs.glob("/sys/class/ubi/*/name"); + found = filter(found, (v) => substr(fs.readfile(v), 0, 6) == "rootfs"); + if (!length(found)) + return; + + let dev = fs.basename(fs.dirname(found[0])); + dev = "/dev/" + replace(dev, /_\d+$/, ""); + if (system(`ubimkvol ${dev} -N provisioning -s 131072`) != 0) + return; + + return open_ubi(); +} + +function data_path_get(data, path, create) +{ + if (!data) + return; + + if (!length(path)) + return data; + + if (type(path) == "string") + path = split(path, "."); + + let last = data; + let last_name; + for (let name in path) { + switch (type(data)) { + case "object": + last = data; + last_name = name; + data = data[name]; + break; + case "array": + last = data; + last_name = name; + data = data[+name]; + break; + default: + return; + } + + if (data == null && create) + data = last[last_name] = {}; + } + + return data; +} + +const provision_proto = { + init: function() { + this.data = this.backend.read(); + try { + this.data = json(this.data); + } catch(e) { + this.data = null; + } + if (!this.data) + this.reset(); + return true; + }, + get: function(path) { + return data_path_get(this.data, path); + }, + set: function(path, value) { + if (!length(path)) + return; + + if (type(path) == "string") + path = split(path, "."); + let name = pop(path); + let data = data_path_get(this.data, path, true); + if (type(data) != "object") + return; + + if (value == null) + delete data[name]; + else + data[name] = value; + return true; + }, + reset: function() { + this.data = {}; + return true; + }, + commit: function() { + if (!this.data) + return; + + return this.backend.commit("" + this.data); + }, + destroy: function() { + return this.backend.destroy(); + } +}; + +function __open(backend) +{ + if (!backend) + return; + + return proto({ + backend, + }, provision_proto); +} + +export function create() +{ + return __open(create_ubi()); +}; + +export function open() +{ + return __open(open_ubi()); +};