/* * Copyright (C) 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include #include #include #include "fu-altos-device.h" #include "fu-altos-firmware.h" struct _FuAltosDevice { FuUsbDevice parent_instance; gchar *tty; guint64 addr_base; guint64 addr_bound; struct termios tty_termios; FuIOChannel *io_channel; }; G_DEFINE_TYPE(FuAltosDevice, fu_altos_device, FU_TYPE_USB_DEVICE) static void fu_altos_device_to_string(FuDevice *device, guint idt, GString *str) { FuAltosDevice *self = FU_ALTOS_DEVICE(device); fu_common_string_append_kv(str, idt, "TTY", self->tty); if (self->addr_base != 0x0) fu_common_string_append_kx(str, idt, "AddrBase", self->addr_base); if (self->addr_bound != 0x0) fu_common_string_append_kx(str, idt, "AddrBound", self->addr_bound); } static void fu_altos_device_finalize(GObject *object) { FuAltosDevice *self = FU_ALTOS_DEVICE(object); g_free(self->tty); G_OBJECT_CLASS(fu_altos_device_parent_class)->finalize(object); } static gboolean fu_altos_device_find_tty(FuAltosDevice *self, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self)); g_autoptr(GList) devices = NULL; g_autoptr(GUdevClient) gudev_client = g_udev_client_new(NULL); /* find all tty devices */ devices = g_udev_client_query_by_subsystem(gudev_client, "tty"); for (GList *l = devices; l != NULL; l = l->next) { GUdevDevice *dev = G_UDEV_DEVICE(l->data); /* get the tty device */ const gchar *dev_file = g_udev_device_get_device_file(dev); if (dev_file == NULL) continue; /* get grandparent */ dev = g_udev_device_get_parent(dev); if (dev == NULL) continue; dev = g_udev_device_get_parent(dev); if (dev == NULL) continue; /* check correct device */ if (g_udev_device_get_sysfs_attr_as_int(dev, "busnum") != g_usb_device_get_bus(usb_device)) continue; if (g_udev_device_get_sysfs_attr_as_int(dev, "devnum") != g_usb_device_get_address(usb_device)) continue; /* success */ self->tty = g_strdup(dev_file); return TRUE; } /* failure */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to find tty for %u:%u", g_usb_device_get_bus(usb_device), g_usb_device_get_address(usb_device)); return FALSE; } static gboolean fu_altos_device_tty_write(FuAltosDevice *self, const gchar *data, gssize data_len, GError **error) { /* lets assume this is text */ if (data_len < 0) data_len = strlen(data); if (g_getenv("FWUPD_ALTOS_VERBOSE") != NULL) fu_common_dump_raw(G_LOG_DOMAIN, "write", (const guint8 *)data, (gsize)data_len); return fu_io_channel_write_raw(self->io_channel, (const guint8 *)data, (gsize)data_len, 500, /* ms */ FU_IO_CHANNEL_FLAG_NONE, error); } static GString * fu_altos_device_tty_read(FuAltosDevice *self, guint timeout_ms, gssize max_size, GError **error) { g_autoptr(GBytes) buf = NULL; buf = fu_io_channel_read_bytes(self->io_channel, max_size, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error); if (buf == NULL) return NULL; if (g_getenv("FWUPD_ALTOS_VERBOSE") != NULL) fu_common_dump_bytes(G_LOG_DOMAIN, "read", buf); return g_string_new_len(g_bytes_get_data(buf, NULL), g_bytes_get_size(buf)); } static gboolean fu_altos_device_tty_open(FuAltosDevice *self, GError **error) { struct termios termios; g_autoptr(GString) str = NULL; /* open device */ self->io_channel = fu_io_channel_new_file(self->tty, error); if (self->io_channel == NULL) return FALSE; /* get the old termios settings so we can restore later */ if (tcgetattr(fu_io_channel_unix_get_fd(self->io_channel), &termios) < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get attributes from fd"); return FALSE; } self->tty_termios = termios; cfmakeraw(&termios); /* set speed */ if (cfsetspeed(&termios, B9600) < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to set terminal speed"); return FALSE; } /* one input byte is enough to return * inter-character timer off */ termios.c_cc[VMIN] = 1; termios.c_cc[VTIME] = 0; /* set all new data */ if (tcsetattr(fu_io_channel_unix_get_fd(self->io_channel), TCSAFLUSH, &termios) < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to set attributes on fd"); return FALSE; } /* dump any pending input */ str = fu_altos_device_tty_read(self, 50, -1, NULL); if (str != NULL) g_debug("dumping pending buffer: %s", str->str); return TRUE; } static gboolean fu_altos_device_tty_close(FuAltosDevice *self, GError **error) { if (self->io_channel != NULL) { tcsetattr(fu_io_channel_unix_get_fd(self->io_channel), TCSAFLUSH, &self->tty_termios); if (!fu_io_channel_shutdown(self->io_channel, error)) return FALSE; g_clear_object(&self->io_channel); } return TRUE; } static GString * fu_altos_device_read_page(FuAltosDevice *self, guint address, GError **error) { g_autoptr(GString) str = NULL; g_autofree gchar *cmd = g_strdup_printf("R %x\n", address); if (!fu_altos_device_tty_write(self, cmd, -1, error)) return NULL; str = fu_altos_device_tty_read(self, 1500, 256, error); if (str == NULL) return NULL; return g_steal_pointer(&str); } static gboolean fu_altos_device_write_page(FuAltosDevice *self, guint address, const guint8 *data, guint data_len, GError **error) { g_autofree gchar *cmd = g_strdup_printf("W %x\n", address); if (!fu_altos_device_tty_write(self, cmd, -1, error)) return FALSE; if (!fu_altos_device_tty_write(self, (const gchar *)data, data_len, error)) return FALSE; return TRUE; } static FuFirmware * fu_altos_device_prepare_firmware(FuDevice *device, GBytes *fw, FwupdInstallFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_altos_firmware_new(); if (!fu_firmware_parse(firmware, fw, flags, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_altos_device_write_firmware(FuDevice *device, FuFirmware *firmware, FwupdInstallFlags flags, GError **error) { FuAltosDevice *self = FU_ALTOS_DEVICE(device); const gchar *data; const gsize data_len; guint flash_len; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GString) buf = g_string_new(NULL); /* check kind */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "verification only supported in bootloader"); return FALSE; } /* check sizes */ if (self->addr_base == 0x0 || self->addr_bound == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "address base and bound are unset"); return FALSE; } /* read in blocks of 256 bytes */ flash_len = self->addr_bound - self->addr_base; if (flash_len == 0x0 || flash_len > 0x100000) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "address range was icorrect"); return FALSE; } /* check the start address */ if (fu_firmware_get_addr(firmware) != self->addr_base) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "start address not correct %" G_GUINT64_FORMAT ":" "%" G_GUINT64_FORMAT, fu_firmware_get_addr(firmware), self->addr_base); return FALSE; } /* check firmware will fit */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; data = g_bytes_get_data(fw, (gsize *)&data_len); if (data_len > flash_len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware too large for device %" G_GSIZE_FORMAT ":%u", data_len, flash_len); return FALSE; } /* open tty for download */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_altos_device_tty_open, (FuDeviceLockerFunc)fu_altos_device_tty_close, error); if (locker == NULL) return FALSE; fu_device_set_status(device, FWUPD_STATUS_DEVICE_WRITE); for (guint i = 0; i < flash_len; i += 0x100) { g_autoptr(GString) str = NULL; guint8 buf_tmp[0x100]; /* copy remaining data into buf if required */ memset(buf_tmp, 0xff, sizeof(buf)); if (i < data_len) { gsize chunk_len = 0x100; if (i + 0x100 > data_len) chunk_len = data_len - i; if (!fu_memcpy_safe(buf_tmp, sizeof(buf_tmp), 0, /* dst */ (const guint8 *)data, data_len, i, /* src */ chunk_len, error)) return FALSE; } /* verify data from device */ if (!fu_altos_device_write_page(self, self->addr_base + i, buf_tmp, 0x100, error)) return FALSE; /* verify data written on device */ str = fu_altos_device_read_page(self, self->addr_base + i, error); if (str == NULL) return FALSE; if (str->len != 0x100) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to verify @%x, " "not enough data returned", (guint)(self->addr_base + i)); return FALSE; } if (memcmp(str->str, buf_tmp, 0x100) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to verify @%x", (guint)(self->addr_base + i)); return FALSE; } /* progress */ fu_device_set_progress_full(device, i, flash_len); g_string_append_len(buf, str->str, str->len); } /* go to application mode */ if (!fu_altos_device_tty_write(self, "a\n", -1, error)) return FALSE; /* progress complete */ fu_device_set_progress_full(device, flash_len, flash_len); /* success */ return TRUE; } static GBytes * fu_altos_device_dump_firmware(FuDevice *device, GError **error) { FuAltosDevice *self = FU_ALTOS_DEVICE(device); guint flash_len; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GString) buf = g_string_new(NULL); /* check kind */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "verification only supported in bootloader"); return NULL; } /* check sizes */ if (self->addr_base == 0x0 || self->addr_bound == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "address base and bound are unset"); return NULL; } /* read in blocks of 256 bytes */ flash_len = self->addr_bound - self->addr_base; if (flash_len == 0x0 || flash_len > 0x100000) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "address range was icorrect"); return NULL; } /* open tty for download */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_altos_device_tty_open, (FuDeviceLockerFunc)fu_altos_device_tty_close, error); if (locker == NULL) return NULL; for (guint i = self->addr_base; i < self->addr_bound; i += 0x100) { g_autoptr(GString) str = NULL; /* request data from device */ str = fu_altos_device_read_page(self, i, error); if (str == NULL) return NULL; /* progress */ fu_device_set_progress_full(device, i - self->addr_base, self->addr_bound - self->addr_base); g_string_append_len(buf, str->str, str->len); } /* success */ return g_bytes_new(buf->str, buf->len); } static gboolean fu_altos_device_probe_bootloader(FuAltosDevice *self, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_auto(GStrv) lines = NULL; g_autoptr(GString) str = NULL; /* get tty for upload */ if (!fu_altos_device_find_tty(self, error)) return FALSE; locker = fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_altos_device_tty_open, (FuDeviceLockerFunc)fu_altos_device_tty_close, error); if (locker == NULL) return FALSE; /* get the version information */ if (!fu_altos_device_tty_write(self, "v\n", -1, error)) return FALSE; str = fu_altos_device_tty_read(self, 100, -1, error); if (str == NULL) { g_prefix_error(error, "failed to get version information: "); return FALSE; } /* parse each line */ lines = g_strsplit_set(str->str, "\n\r", -1); for (guint i = 0; lines[i] != NULL; i++) { /* ignore */ if (lines[i][0] == '\0') continue; if (g_str_has_prefix(lines[i], "manufacturer ")) continue; if (g_str_has_prefix(lines[i], "product ")) continue; /* we can flash firmware */ if (g_strcmp0(lines[i], "altos-loader") == 0) { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER); continue; } /* version number */ if (g_str_has_prefix(lines[i], "software-version ")) { fu_device_set_version(FU_DEVICE(self), lines[i] + 17); continue; } /* address base and bound */ if (g_str_has_prefix(lines[i], "flash-range ")) { g_auto(GStrv) addrs = g_strsplit(lines[i] + 17, " ", -1); self->addr_base = g_ascii_strtoull(addrs[0], NULL, 16); self->addr_bound = g_ascii_strtoull(addrs[1], NULL, 16); g_debug("base: %x, bound: %x", (guint)self->addr_base, (guint)self->addr_bound); continue; } /* unknown line */ g_debug("unknown data: '%s'", lines[i]); } return TRUE; } static gboolean fu_altos_device_probe(FuDevice *device, GError **error) { FuAltosDevice *self = FU_ALTOS_DEVICE(device); GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self)); const gchar *version_prefix = "ChaosKey-hw-1.0-sw-"; guint8 version_idx; g_autofree gchar *version = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* bootloader uses tty */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return fu_altos_device_probe_bootloader(self, error); /* open */ locker = fu_device_locker_new(usb_device, error); if (locker == NULL) return FALSE; /* get string */ version_idx = g_usb_device_get_product_index(usb_device); version = g_usb_device_get_string_descriptor(usb_device, version_idx, error); if (version == NULL) return FALSE; if (!g_str_has_prefix(version, version_prefix)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a ChaosKey v1.0 device: %s", version); return FALSE; } fu_device_set_version(FU_DEVICE(self), version + 19); /* success */ return TRUE; } static void fu_altos_device_init(FuAltosDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_vendor(FU_DEVICE(self), "altusmetrum.org"); fu_device_set_summary(FU_DEVICE(self), "USB hardware random number generator"); fu_device_add_protocol(FU_DEVICE(self), "org.altusmetrum.altos"); /* requires manual step */ if (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER); } static void fu_altos_device_class_init(FuAltosDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); klass_device->to_string = fu_altos_device_to_string; klass_device->probe = fu_altos_device_probe; klass_device->prepare_firmware = fu_altos_device_prepare_firmware; klass_device->write_firmware = fu_altos_device_write_firmware; klass_device->dump_firmware = fu_altos_device_dump_firmware; object_class->finalize = fu_altos_device_finalize; }