From eb6c2bef298b125478984be170f30f5a33801c0a Mon Sep 17 00:00:00 2001 From: Quantum Date: Wed, 6 Dec 2023 00:19:27 -0500 Subject: [PATCH 1/2] Add fingerprint functionality via fprintd DBus service Co-authored-by: Alexander Lutsai --- completions/bash/swaylock | 2 + completions/fish/swaylock.fish | 1 + completions/zsh/_swaylock | 1 + fingerprint/fingerprint.c | 249 +++++++++++++++++++++++++++++++++ fingerprint/fingerprint.h | 43 ++++++ fingerprint/meson.build | 33 +++++ include/swaylock.h | 3 + main.c | 30 +++- meson.build | 4 +- render.c | 3 + 10 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 fingerprint/fingerprint.c create mode 100644 fingerprint/fingerprint.h create mode 100644 fingerprint/meson.build diff --git a/completions/bash/swaylock b/completions/bash/swaylock index 87fc617f..b0bd84e1 100644 --- a/completions/bash/swaylock +++ b/completions/bash/swaylock @@ -14,6 +14,7 @@ _swaylock() -F -h -i + -p -k -K -L @@ -41,6 +42,7 @@ _swaylock() --hide-keyboard-layout --ignore-empty-password --image + --fingerprint --indicator-caps-lock --indicator-idle-visible --indicator-radius diff --git a/completions/fish/swaylock.fish b/completions/fish/swaylock.fish index bb25d709..52ccdea5 100644 --- a/completions/fish/swaylock.fish +++ b/completions/fish/swaylock.fish @@ -14,6 +14,7 @@ complete -c swaylock -l help -s h --description "Show help mes complete -c swaylock -l hide-keyboard-layout -s K --description "Hide the current xkb layout while typing." complete -c swaylock -l ignore-empty-password -s e --description "When an empty password is provided, do not validate it." complete -c swaylock -l image -s i --description "Display the given image, optionally only on the given output." +complete -c swaylock -l fingerprint p --description "Enable fingerprint scanning. Fprint is required." complete -c swaylock -l indicator-caps-lock -s l --description "Show the current Caps Lock state also on the indicator." complete -c swaylock -l indicator-idle-visible --description "Sets the indicator to show even if idle." complete -c swaylock -l indicator-radius --description "Sets the indicator radius." diff --git a/completions/zsh/_swaylock b/completions/zsh/_swaylock index 6fc2126a..8e4abcdb 100644 --- a/completions/zsh/_swaylock +++ b/completions/zsh/_swaylock @@ -18,6 +18,7 @@ _arguments -s \ '(--hide-keyboard-layout -K)'{--hide-keyboard-layout,-K}'[Hide the current xkb layout while typing]' \ '(--ignore-empty-password -e)'{--ignore-empty-password,-e}'[When an empty password is provided, do not validate it]' \ '(--image -i)'{--image,-i}'[Display the given image, optionally only on the given output]:filename:_files' \ + '(--fingerprint -p)'{--fingerprint,-p}'[Enable fingerprint scanning. Fprint is required]' \ '(--indicator-caps-lock -l)'{--indicator-caps-lock,-l}'[Show the current Caps Lock state also on the indicator]' \ '(--indicator-idle-visible)'--indicator-idle-visible'[Sets the indicator to show even if idle]' \ '(--indicator-radius)'--indicator-radius'[Sets the indicator radius]:radius:' \ diff --git a/fingerprint/fingerprint.c b/fingerprint/fingerprint.c new file mode 100644 index 00000000..df2ea81d --- /dev/null +++ b/fingerprint/fingerprint.c @@ -0,0 +1,249 @@ +/* + * Based on fprintd util to verify a fingerprint + * Copyright (C) 2008 Daniel Drake + * Copyright (C) 2020 Marco Trevisan + * Copyright (C) 2023 Alexandr Lutsai + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include +#include +#include +#include + +#include "fingerprint.h" +#include "log.h" + +static void display_message(struct FingerprintState *state, const char *fmt, ...) { + va_list(args); + va_start(args, fmt); + vsnprintf(state->status, sizeof(state->status), fmt, args); + va_end(args); + + state->sw_state->auth_state = AUTH_STATE_FINGERPRINT; + state->sw_state->fingerprint_msg = state->status; + damage_state(state->sw_state); + schedule_indicator_clear(state->sw_state); +} + +static void create_manager(struct FingerprintState *state) { + g_autoptr(GError) error = NULL; + state->connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + if (state->connection == NULL) { + swaylock_log(LOG_ERROR, "Failed to connect to session bus: %s", error->message); + display_message(state, "Fingerprint error"); + return; + } + + state->manager = fprint_dbus_manager_proxy_new_sync( + state->connection, + G_DBUS_PROXY_FLAGS_NONE, + "net.reactivated.Fprint", + "/net/reactivated/Fprint/Manager", + NULL, &error); + if (state->manager == NULL) { + swaylock_log(LOG_ERROR, "Failed to get Fprintd manager: %s", error->message); + display_message(state, "Fingerprint error"); + return; + } + + swaylock_log(LOG_DEBUG, "FPrint manager created"); +} + +static void open_device(struct FingerprintState *state) { + state->device = NULL; + g_autoptr(FprintDBusDevice) dev = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *path = NULL; + if (!fprint_dbus_manager_call_get_default_device_sync(state->manager, &path, NULL, &error)) { + swaylock_log(LOG_ERROR, "Impossible to verify: %s", error->message); + display_message(state, "Fingerprint error"); + return; + } + + swaylock_log(LOG_DEBUG, "Fingerprint: using device %s", path); + dev = fprint_dbus_device_proxy_new_sync( + state->connection, + G_DBUS_PROXY_FLAGS_NONE, + "net.reactivated.Fprint", + path, NULL, &error); + if (error) { + swaylock_log(LOG_ERROR, "failed to connect to device: %s", error->message); + display_message(state, "Fingerprint error"); + return; + } + + if (!fprint_dbus_device_call_claim_sync(dev, "", NULL, &error)) { + // we need to wait while device can be claimed + swaylock_log(LOG_DEBUG, "failed to claim the device: %s(%d)", error->message, error->code); + return; + } + + swaylock_log(LOG_DEBUG, "FPrint device opened %s", path); + state->device = g_steal_pointer (&dev); +} + +static void verify_result(GObject *object, const char *result, gboolean done, void *user_data) { + struct FingerprintState *state = user_data; + swaylock_log(LOG_INFO, "Verify result: %s (%s)", result, done ? "done" : "not done"); + state->match = g_str_equal(result, "verify-match"); + if (g_str_equal(result, "verify-retry-scan")) { + display_message(state, "Retry"); + return; + } else if (g_str_equal(result, "verify-swipe-too-short")) { + display_message(state, "Retry, too short"); + return; + } else if (g_str_equal(result, "verify-finger-not-centered")) { + display_message(state, "Retry, not centered"); + return; + } else if (g_str_equal(result, "verify-remove-and-retry")) { + display_message(state, "Remove and retry"); + return; + } + + if(state->match) { + display_message(state, "Fingerprint OK"); + } else { + display_message(state, "Retry"); + } + + state->completed = TRUE; + g_autoptr(GError) error = NULL; + if (!fprint_dbus_device_call_verify_stop_sync(state->device, NULL, &error)) { + swaylock_log(LOG_ERROR, "VerifyStop failed: %s", error->message); + display_message(state, "Fingerprint error"); + return; + } +} + +static void verify_started_cb(GObject *obj, GAsyncResult *res, gpointer user_data) { + struct FingerprintState *state = user_data; + if (!fprint_dbus_device_call_verify_start_finish(FPRINT_DBUS_DEVICE(obj), res, &state->error)) { + return; + } + + swaylock_log(LOG_DEBUG, "Verify started!"); + state->started = TRUE; +} + +static void proxy_signal_cb(GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + struct FingerprintState *state = user_data; + if (!state->started) { + return; + } + + if (!g_str_equal(signal_name, "VerifyStatus")) { + return; + } + + const gchar *result; + gboolean done; + g_variant_get(parameters, "(&sb)", &result, &done); + verify_result(G_OBJECT (proxy), result, done, user_data); +} + +static void start_verify(struct FingerprintState *state) { + /* This one is funny. We connect to the signal immediately to avoid + * race conditions. However, we must ignore any authentication results + * that happen before our start call returns. + * This is because the verify call itself may internally try to verify + * against fprintd (possibly using a separate account). + * + * To do so, we *must* use the async version of the verify call, as the + * sync version would cause the signals to be queued and only processed + * after it returns. + */ + fprint_dbus_device_call_verify_start(state->device, "any", NULL, + verify_started_cb, + state); + + /* Wait for verify start while discarding any VerifyStatus signals */ + while (!state->started && !state->error) { + g_main_context_iteration(NULL, TRUE); + } + + if (state->error) { + swaylock_log(LOG_ERROR, "VerifyStart failed: %s", state->error->message); + display_message(state, "Fingerprint error"); + g_clear_error(&state->error); + return; + } +} + +static void release_callback(GObject *source_object, GAsyncResult *res, + gpointer user_data) { +} + +void fingerprint_init(struct FingerprintState *fingerprint_state, + struct swaylock_state *swaylock_state) { + memset(fingerprint_state, 0, sizeof(struct FingerprintState)); + fingerprint_state->sw_state = swaylock_state; + create_manager(fingerprint_state); + if (fingerprint_state->manager == NULL || fingerprint_state->connection == NULL) { + return; + } +} + +int fingerprint_verify(struct FingerprintState *fingerprint_state) { + /* VerifyStatus signals are processing, do not wait for completion. */ + g_main_context_iteration (NULL, FALSE); + if (fingerprint_state->manager == NULL || + fingerprint_state->connection == NULL) { + return false; + } + + if (fingerprint_state->device == NULL) { + open_device(fingerprint_state); + if (fingerprint_state->device == NULL) { + return false; + } + + g_signal_connect(fingerprint_state->device, "g-signal", G_CALLBACK (proxy_signal_cb), + fingerprint_state); + start_verify(fingerprint_state); + } + + if (!fingerprint_state->completed) { + return false; + } + + if (!fingerprint_state->match) { + fingerprint_state->completed = 0; + fingerprint_state->match = 0; + start_verify(fingerprint_state); + return false; + } + + return true; +} + +void fingerprint_deinit(struct FingerprintState *fingerprint_state) { + if (!fingerprint_state->device) { + return; + } + + g_signal_handlers_disconnect_by_func(fingerprint_state->device, proxy_signal_cb, + fingerprint_state); + fprint_dbus_device_call_release(fingerprint_state->device, NULL, release_callback, NULL); + fingerprint_state->device = NULL; +} diff --git a/fingerprint/fingerprint.h b/fingerprint/fingerprint.h new file mode 100644 index 00000000..d030a43c --- /dev/null +++ b/fingerprint/fingerprint.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Alexandr Lutsai + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _FINGERPRINT_H +#define _FINGERPRINT_H + +#include "swaylock.h" +#include "fingerprint/fprintd-dbus.h" + +struct FingerprintState { + GError *error; + gboolean started; + gboolean completed; + gboolean match; + + char status[128]; + + FprintDBusManager *manager; + GDBusConnection *connection; + FprintDBusDevice *device; + struct swaylock_state *sw_state; +}; + +void fingerprint_init(struct FingerprintState *fingerprint_state, struct swaylock_state *state); +int fingerprint_verify(struct FingerprintState *fingerprint_state); +void fingerprint_deinit(struct FingerprintState *fingerprint_state); + +#endif \ No newline at end of file diff --git a/fingerprint/meson.build b/fingerprint/meson.build new file mode 100644 index 00000000..1bcd667f --- /dev/null +++ b/fingerprint/meson.build @@ -0,0 +1,33 @@ +gnome = import('gnome') + +dbus = dependency('dbus-1') +glib = dependency('glib-2.0', version: '>=2.64.0') +gio_dep = dependency('gio-2.0') + +fprintd_dbus_interfaces = files( + '/usr/share/dbus-1/interfaces/net.reactivated.Fprint.Manager.xml', + '/usr/share/dbus-1/interfaces/net.reactivated.Fprint.Device.xml', +) + +fprintd_dbus_sources = gnome.gdbus_codegen('fprintd-dbus', + sources: fprintd_dbus_interfaces, + autocleanup: 'all', + interface_prefix: 'net.reactivated.Fprint.', + namespace: 'FprintDBus', + object_manager: true, +) + +fingerprint = declare_dependency( + include_directories: [ + include_directories('..'), + ], + sources: [ + fprintd_dbus_sources, + 'fingerprint.c', + ], + dependencies: [ + gio_dep, + glib, + dbus, + ], +) diff --git a/include/swaylock.h b/include/swaylock.h index de09ce85..4d2e86bb 100644 --- a/include/swaylock.h +++ b/include/swaylock.h @@ -20,6 +20,7 @@ enum auth_state { AUTH_STATE_VALIDATING, AUTH_STATE_INVALID, AUTH_STATE_GRACE, + AUTH_STATE_FINGERPRINT, }; struct swaylock_colorset { @@ -81,6 +82,7 @@ struct swaylock_args { uint32_t password_grace_period; bool password_grace_no_mouse; bool password_grace_no_touch; + bool fingerprint; char *text_cleared; char *text_caps_lock; @@ -119,6 +121,7 @@ struct swaylock_state { bool run_display; struct ext_session_lock_manager_v1 *ext_session_lock_manager_v1; struct ext_session_lock_v1 *ext_session_lock_v1; + char *fingerprint_msg; }; struct swaylock_surface { diff --git a/main.c b/main.c index 9e055176..19d0f27b 100644 --- a/main.c +++ b/main.c @@ -29,6 +29,7 @@ #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "wlr-screencopy-unstable-v1-client-protocol.h" #include "ext-session-lock-v1-client-protocol.h" +#include "fingerprint/fingerprint.h" // returns a positive integer in milliseconds static uint32_t parse_seconds(const char *seconds) { @@ -997,6 +998,7 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state, {"disable-caps-lock-text", no_argument, NULL, 'L'}, {"indicator-caps-lock", no_argument, NULL, 'l'}, {"line-uses-inside", no_argument, NULL, 'n'}, + {"fingerprint", no_argument, NULL, 'p'}, {"line-uses-ring", no_argument, NULL, 'r'}, {"scaling", required_argument, NULL, 's'}, {"tiling", no_argument, NULL, 'T'}, @@ -1106,6 +1108,8 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state, "Disable the Caps Lock text.\n" " -l, --indicator-caps-lock " "Show the current Caps Lock state also on the indicator.\n" + " -p, --fingerprint " + "Enable fingerprint scanning. Fprint is required.\n" " -s, --scaling " "Image scaling mode: stretch, fill, fit, center, tile, solid_color.\n" " -T, --tiling " @@ -1227,7 +1231,7 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state, optind = 1; while (1) { int opt_idx = 0; - c = getopt_long(argc, argv, "c:deFfhi:SkKLlnrs:tuvC:", long_options, + c = getopt_long(argc, argv, "c:deFfhi:SkKLlnprs:tuvC:", long_options, &opt_idx); if (c == -1) { break; @@ -1274,6 +1278,11 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state, state->args.screenshots = true; } break; + case 'p': + if (state) { + state->args.fingerprint = true; + } + break; case 'k': if (state) { state->args.show_keyboard_layout = true; @@ -1785,6 +1794,15 @@ static void term_in(int fd, short mask, void *data) { state.run_display = false; } +static void check_fingerprint(void *d) { + struct FingerprintState *fingerprint_state = d; + if (fingerprint_verify(fingerprint_state)) { + state.run_display = false; + } + + loop_add_timer(state.eventloop, 300, check_fingerprint, fingerprint_state); +} + // Check for --debug 'early' we also apply the correct loglevel // to the forked child, without having to first proces all of the // configuration (including from file) before forking and (in the @@ -1847,6 +1865,7 @@ int main(int argc, char **argv) { .datestr = strdup("%a, %x"), .allow_fade = true, .password_grace_period = 0, + .fingerprint = false, .text_cleared = strdup("Cleared"), .text_caps_lock = strdup("Caps Lock"), @@ -2011,6 +2030,12 @@ int main(int argc, char **argv) { loop_add_timer(state.eventloop, 1000, timer_render, &state); + struct FingerprintState fingerprint_state; + if (state.args.fingerprint) { + fingerprint_init(&fingerprint_state, &state); + loop_add_timer(state.eventloop, 100, check_fingerprint, &fingerprint_state); + } + if (state.args.fade_in) { loop_add_timer(state.eventloop, state.args.fade_in, end_allow_fade_period, &state); } @@ -2045,6 +2070,9 @@ int main(int argc, char **argv) { wl_display_roundtrip(state.display); } + if(state.args.fingerprint) { + fingerprint_deinit(&fingerprint_state); + } free(state.args.font); return 0; } diff --git a/meson.build b/meson.build index e5e20cfd..5407b463 100644 --- a/meson.build +++ b/meson.build @@ -93,6 +93,7 @@ conf_data.set_quoted('SWAYLOCK_VERSION', version) conf_data.set10('HAVE_GDK_PIXBUF', gdk_pixbuf.found()) subdir('include') +subdir('fingerprint') dependencies = [ cairo, @@ -102,7 +103,8 @@ dependencies = [ dl, xkbcommon, wayland_client, - omp + omp, + fingerprint ] sources = [ diff --git a/render.c b/render.c index 368a4bca..4d41119f 100644 --- a/render.c +++ b/render.c @@ -256,6 +256,9 @@ void render_frame(struct swaylock_surface *surface) { case AUTH_STATE_VALIDATING: text = state->args.text_verifying; break; + case AUTH_STATE_FINGERPRINT: + text = state->fingerprint_msg; + break; case AUTH_STATE_INVALID: text = state->args.text_wrong; break; From a124d1adc717fba32887828e8ae675695dd8eeda Mon Sep 17 00:00:00 2001 From: Quantum Date: Wed, 6 Dec 2023 00:44:49 -0500 Subject: [PATCH 2/2] Make fingerprint feature optional --- main.c | 20 ++++++++++++++++++-- meson.build | 10 +++++++--- meson_options.txt | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/main.c b/main.c index 19d0f27b..baa4ab26 100644 --- a/main.c +++ b/main.c @@ -29,7 +29,10 @@ #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "wlr-screencopy-unstable-v1-client-protocol.h" #include "ext-session-lock-v1-client-protocol.h" -#include "fingerprint/fingerprint.h" + +#if HAVE_FINGERPRINT +# include "fingerprint/fingerprint.h" +#endif // returns a positive integer in milliseconds static uint32_t parse_seconds(const char *seconds) { @@ -998,7 +1001,9 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state, {"disable-caps-lock-text", no_argument, NULL, 'L'}, {"indicator-caps-lock", no_argument, NULL, 'l'}, {"line-uses-inside", no_argument, NULL, 'n'}, +#if HAVE_FINGERPRINT {"fingerprint", no_argument, NULL, 'p'}, +#endif {"line-uses-ring", no_argument, NULL, 'r'}, {"scaling", required_argument, NULL, 's'}, {"tiling", no_argument, NULL, 'T'}, @@ -1108,8 +1113,10 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state, "Disable the Caps Lock text.\n" " -l, --indicator-caps-lock " "Show the current Caps Lock state also on the indicator.\n" +#if HAVE_FINGERPRINT " -p, --fingerprint " "Enable fingerprint scanning. Fprint is required.\n" +#endif " -s, --scaling " "Image scaling mode: stretch, fill, fit, center, tile, solid_color.\n" " -T, --tiling " @@ -1278,11 +1285,13 @@ static int parse_options(int argc, char **argv, struct swaylock_state *state, state->args.screenshots = true; } break; +#if HAVE_FINGERPRINT case 'p': if (state) { state->args.fingerprint = true; } break; +#endif case 'k': if (state) { state->args.show_keyboard_layout = true; @@ -1794,6 +1803,7 @@ static void term_in(int fd, short mask, void *data) { state.run_display = false; } +#if HAVE_FINGERPRINT static void check_fingerprint(void *d) { struct FingerprintState *fingerprint_state = d; if (fingerprint_verify(fingerprint_state)) { @@ -1802,6 +1812,7 @@ static void check_fingerprint(void *d) { loop_add_timer(state.eventloop, 300, check_fingerprint, fingerprint_state); } +#endif // Check for --debug 'early' we also apply the correct loglevel // to the forked child, without having to first proces all of the @@ -2030,11 +2041,13 @@ int main(int argc, char **argv) { loop_add_timer(state.eventloop, 1000, timer_render, &state); +#if HAVE_FINGERPRINT struct FingerprintState fingerprint_state; if (state.args.fingerprint) { fingerprint_init(&fingerprint_state, &state); loop_add_timer(state.eventloop, 100, check_fingerprint, &fingerprint_state); } +#endif if (state.args.fade_in) { loop_add_timer(state.eventloop, state.args.fade_in, end_allow_fade_period, &state); @@ -2070,9 +2083,12 @@ int main(int argc, char **argv) { wl_display_roundtrip(state.display); } - if(state.args.fingerprint) { +#if HAVE_FINGERPRINT + if (state.args.fingerprint) { fingerprint_deinit(&fingerprint_state); } +#endif + free(state.args.font); return 0; } diff --git a/meson.build b/meson.build index 5407b463..44c98868 100644 --- a/meson.build +++ b/meson.build @@ -91,9 +91,9 @@ conf_data = configuration_data() conf_data.set_quoted('SYSCONFDIR', get_option('prefix') / get_option('sysconfdir')) conf_data.set_quoted('SWAYLOCK_VERSION', version) conf_data.set10('HAVE_GDK_PIXBUF', gdk_pixbuf.found()) +conf_data.set10('HAVE_FINGERPRINT', get_option('fingerprint').enabled()) subdir('include') -subdir('fingerprint') dependencies = [ cairo, @@ -103,8 +103,7 @@ dependencies = [ dl, xkbcommon, wayland_client, - omp, - fingerprint + omp ] sources = [ @@ -124,6 +123,11 @@ sources = [ 'fade.c', ] +if get_option('fingerprint').enabled() + subdir('fingerprint') + dependencies += [fingerprint] +endif + if libpam.found() sources += ['pam.c'] dependencies += [libpam] diff --git a/meson_options.txt b/meson_options.txt index d6a90bc2..0547ad29 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,3 +5,4 @@ option('zsh-completions', type: 'boolean', value: true, description: 'Install zs option('bash-completions', type: 'boolean', value: true, description: 'Install bash shell completions') option('fish-completions', type: 'boolean', value: true, description: 'Install fish shell completions') option('sse', type: 'boolean', value: true, description: 'Use SSE instructions where possible') +option('fingerprint', type: 'feature', value: 'auto', description: 'Enable fingerprint support')