mirror of
https://github.com/openwrt/openwrt.git
synced 2025-03-22 03:55:22 +00:00
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 <nbd@nbd.name>
This commit is contained in:
parent
5f0f8d23a1
commit
5d40123818
33
package/utils/provision/Makefile
Normal file
33
package/utils/provision/Makefile
Normal file
@ -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 <nbd@nbd.name>
|
||||
|
||||
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))
|
107
package/utils/provision/files/usr/sbin/provision
Executable file
107
package/utils/provision/files/usr/sbin/provision
Executable file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env ucode
|
||||
// SPDX-License-Identifier: GPL-2.0+
|
||||
/*
|
||||
* Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
*/
|
||||
'use strict';
|
||||
import { basename } from "fs";
|
||||
import * as provision from "provision";
|
||||
|
||||
const usage_message = `Usage: ${basename(sourcepath())} <command> [<args>]
|
||||
|
||||
Commands:
|
||||
- get [<path>]: Get string value at <path> (or all if no path given)
|
||||
- set <path> <value> Set string value at <path> to <value>
|
||||
- get_json [<path>]: Get JSON data at <path> (or all if no path given)
|
||||
- set_json <path> <value> Set JSON value at <path> to <value>
|
||||
- delete <path> Delete value at <path>
|
||||
- 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();
|
||||
}
|
189
package/utils/provision/files/usr/share/ucode/provision.uc
Normal file
189
package/utils/provision/files/usr/share/ucode/provision.uc
Normal file
@ -0,0 +1,189 @@
|
||||
// SPDX-License-Identifier: GPL-2.0+
|
||||
/*
|
||||
* Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
*/
|
||||
'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());
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user