| Title: | Client for the 'KeyNub' USB License Dongle |
|---|---|
| Description: | Verifies that a 'KeyNub' USB license dongle is genuine, reads and writes the license records it holds, reads and increments its monotonic counters, and encrypts data so that only a dongle can decrypt it. The work is done by the vendor's native library, which the package loads at run time; the package installs without it and reports where it looked. Runs on Windows, Linux and macOS. |
| Authors: | Andreas Breitschopp [aut, cre], KeyNub [cph] |
| Maintainer: | Andreas Breitschopp <[email protected]> |
| License: | Apache License (== 2.0) |
| Version: | 1.1.1 |
| Built: | 2026-10-09 13:58:42 UTC |
| Source: | https://github.com/ab-keynub/keynub-sdk |
The pair to build a licence check on. licd_app_encrypt() seals data so
that only a dongle can open it; licd_app_decrypt() opens it again. Put
something the program genuinely needs through this, such as the parameters
of your model, and ship only the sealed form: then removing the check
removes the data. Bulk encryption runs on the host; the dongle wraps only a
small key.
licd_app_encrypt(dongle, data, scope = c("device", "developer")) licd_app_decrypt(dongle, packed)licd_app_encrypt(dongle, data, scope = c("device", "developer")) licd_app_decrypt(dongle, packed)
dongle |
A |
data |
The plaintext: a raw vector, or a single string for its bytes. |
scope |
|
packed |
The sealed data from |
scope = "developer" lets any dongle you have issued decrypt the data, so
one sealed file serves every customer; scope = "device" locks it to the
one dongle that sealed it.
A raw vector: the sealed data, or the recovered plaintext.
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { sealed <- licd_with_session(dongle, licd_app_encrypt(dongle, "the data", "developer")) file <- tempfile(fileext = ".sealed") saveRDS(sealed, file) # In the shipped program: plain <- licd_with_session(dongle, licd_app_decrypt(dongle, readRDS(file))) print(rawToChar(plain)) unlink(file) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { sealed <- licd_with_session(dongle, licd_app_encrypt(dongle, "the data", "developer")) file <- tempfile(fileext = ".sealed") saveRDS(sealed, file) # In the shipped program: plain <- licd_with_session(dongle, licd_app_decrypt(dongle, readRDS(file))) print(rawToChar(plain)) unlink(file) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well
Releases the handle. Safe to call more than once. Closing a context closes every dongle still open on it. An object that is not closed is released when the garbage collector reclaims it, so a forgotten handle leaks nothing, but closing explicitly frees the dongle for other programs at once.
licd_close(x)licd_close(x)
x |
A |
NULL, invisibly.
# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { dongle <- tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) licd_close(dongle) licd_close(ctx) }# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { dongle <- tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) licd_close(dongle) licd_close(ctx) }
A context owns the connection to the operating system's USB layer and the
dongles opened on it. One per program is usual. Close it with
licd_close() when done; a context the garbage collector reclaims is closed
as well, together with any dongle still open on it.
licd_context()licd_context()
A licd_context object.
# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { print(licd_enumerate(ctx)) licd_close(ctx) }# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { print(licd_enumerate(ctx)) licd_close(ctx) }
The dongle holds hardware counters that only ever go up. Reading needs a session; incrementing needs the write role and is irreversible.
licd_counter_read(dongle, id) licd_counter_increment(dongle, id)licd_counter_read(dongle, id) licd_counter_increment(dongle, id)
dongle |
A |
id |
The counter, an integer from 0 upwards; the dongle reports
|
The counter's value (after the increment, for
licd_counter_increment()) as a number. Counters are unsigned 32-bit, so
the value is returned as a double.
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_with_session(dongle, licd_counter_read(dongle, 0))) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_with_session(dongle, licd_counter_read(dongle, 0))) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well
Enumerates without opening anything.
licd_enumerate(ctx)licd_enumerate(ctx)
ctx |
A |
A data frame with one row per dongle and the columns serial (hex),
path (the operating system's device path, accepted by
licd_open_path()), vendor_id and product_id (USB ids). Zero rows when
none is attached.
# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { dongles <- licd_enumerate(ctx) print(nrow(dongles)) licd_close(ctx) }# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { dongles <- licd_enumerate(ctx) print(nrow(dongles)) licd_close(ctx) }
Every failure the native library reports is signalled as a condition of
class licd_error (and error), with these fields:
status: the code, one of licd_status_codes;
operation: the SDK function that failed, such as "licd_open";
detail: the library's diagnostic text for this failure, or "". Log it;
do not parse it.
The failures a program is likely to branch on carry a more specific class in
front of licd_error: licd_no_device, licd_not_genuine,
licd_certificate_invalid, licd_session_expired, licd_not_found,
licd_auth_required and licd_cancelled.
# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { dongle <- tryCatch(licd_open(ctx), licd_no_device = function(e) { message("no dongle attached") NULL }) licd_close(ctx) }# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { dongle <- tryCatch(licd_open(ctx), licd_no_device = function(e) { message("no dongle attached") NULL }) licd_close(ctx) }
The native library's diagnostic text for the last call that failed on the
current thread. The same text travels in the detail field of every
licd_error, so this is for code that inspects a failure after the fact.
licd_error_detail(ctx)licd_error_detail(ctx)
ctx |
A |
A string, "" when the last call succeeded.
Reads what the dongle reports without a session.
licd_info(dongle)licd_info(dongle)
dongle |
A |
A list: protocol_version (two integers), firmware_version (three
integers), se_ready (the secure element responded), provisioned
(factory provisioning complete), data_capacity and data_free (bytes),
watchdog_reboot, isolated and writeauth_rotated (logicals).
watchdog_reboot means the dongle's previous boot ended in a watchdog
timeout: the firmware hung and reset itself. It is the only trace a field
hang leaves behind and a power cycle clears it, so it is worth logging.
writeauth_rotated is FALSE while the dongle still accepts the write key
it left the factory with. That key is public, so a dongle in that state
takes writes from anyone holding it; rotate on receipt with
licd_write_auth_rotate().
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { info <- licd_info(dongle) print(info$firmware_version) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { info <- licd_info(dongle) print(info$firmware_version) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well
Is a context or dongle still open?
licd_is_open(x)licd_is_open(x)
x |
A |
TRUE while the handle can be used, FALSE once it has been closed,
explicitly or through its context.
# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { print(licd_is_open(ctx)) licd_close(ctx) print(licd_is_open(ctx)) }# Does nothing where the native library is not installed. ctx <- tryCatch(licd_context(), error = function(e) NULL) if (!is.null(ctx)) { print(licd_is_open(ctx)) licd_close(ctx) print(licd_is_open(ctx)) }
The package does its work through the KeyNub native library
(keynub_licdongle.dll, libkeynub_licdongle.so or
libkeynub_licdongle.dylib), which a process loads once, on the first call
that needs it. licd_library(path) names the file to load, and must come
before that first call. Without it the package takes, in this order, the
KEYNUB_LICDONGLE_LIBRARY environment variable, the natives/<platform>/
folder of a clone of the SDK repository found from the working directory
upwards, and finally the bare file name, which the operating system resolves
along its usual search path (PATH, LD_LIBRARY_PATH,
DYLD_LIBRARY_PATH).
licd_library(path = NULL)licd_library(path = NULL)
path |
A file path to the library, or |
The path in use, or the one that would be used: the loaded library
once there is one, otherwise the choice the next call would make.
Invisibly when path is given.
# The file the package has loaded, or would load on its first call. licd_library()# The file the package has loaded, or would load on its first call. licd_library()
licd_open() opens the dongle with the given serial, or the first one found
when serial is NULL. licd_open_path() opens the dongle at a device path
from licd_enumerate(). Both signal a condition of class licd_no_device
when there is no such dongle.
licd_open(ctx, serial = NULL) licd_open_path(ctx, path)licd_open(ctx, serial = NULL) licd_open_path(ctx, path)
ctx |
A |
serial |
The serial as hex, or |
path |
A device path from |
A licd_dongle object. Close it with licd_close().
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_serial(dongle)) licd_close(dongle) } if (!is.null(ctx)) licd_close(ctx)# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_serial(dongle)) licd_close(dongle) } if (!is.null(ctx)) licd_close(ctx)
The records on the dongle
licd_record_list(dongle)licd_record_list(dongle)
dongle |
A |
A data frame with the columns name and size (bytes), one row per
record; zero rows when the dongle holds none.
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_with_session(dongle, licd_record_list(dongle))) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_with_session(dongle, licd_record_list(dongle))) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well
Records are named blobs in the dongle's storage; a licence typically is one.
Reading needs a session, writing and erasing need the write role
(licd_write_auth()). licd_record_write() replaces the record atomically.
A record that does not exist signals licd_not_found.
licd_record_read(dongle, name, progress = NULL) licd_record_write(dongle, name, data, progress = NULL) licd_record_erase(dongle, name) licd_record_erase_all(dongle)licd_record_read(dongle, name, progress = NULL) licd_record_write(dongle, name, data, progress = NULL) licd_record_erase(dongle, name) licd_record_erase_all(dongle)
dongle |
A |
name |
The record name, a non-empty string. |
progress |
|
data |
The bytes to store: a raw vector, or a single string for its bytes. |
licd_record_erase_all() is a separate function on purpose: to the C
library a missing name means "erase every record", and an accidentally empty
variable must not do that.
licd_record_read(): the record as a raw vector (rawToChar() gives
text back). The others: NULL, invisibly.
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { licd_with_session(dongle, { records <- licd_record_list(dongle) if (nrow(records) > 0) print(licd_record_read(dongle, records$name[1])) }) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { licd_with_session(dongle, { records <- licd_record_list(dongle) if (nrow(records) > 0) print(licd_record_read(dongle, records$name[1])) }) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well
The dongle's serial
licd_serial(dongle)licd_serial(dongle)
dongle |
A |
The serial as a hex string.
Records, counters and app-data encryption need a session: an encrypted,
authenticated channel to the dongle (P-256 ECDH, HKDF-SHA256, AES-256-GCM).
licd_with_session() opens one, evaluates expr and closes the session
afterwards whatever happens.
licd_session_open(dongle) licd_session_close(dongle) licd_with_session(dongle, expr)licd_session_open(dongle) licd_session_close(dongle) licd_with_session(dongle, expr)
dongle |
A |
expr |
An expression to evaluate while the session is open. |
licd_session_open() and licd_session_close(): NULL, invisibly.
licd_with_session(): the value of expr.
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { licd_session_open(dongle) print(licd_record_list(dongle)) licd_session_close(dongle) # The same, closing the session whatever happens: print(licd_with_session(dongle, licd_record_list(dongle))) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { licd_session_open(dongle) print(licd_record_list(dongle)) licd_session_close(dongle) # The same, closing the session whatever happens: print(licd_with_session(dongle, licd_record_list(dongle))) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well
Replaces the CA root certificate that licd_verify_genuine() checks the
dongle's certificate chain against. Applications do not need this: a release
build of the library embeds the KeyNub production root. It exists for dongles
provisioned against a different CA, and for vendor tooling.
licd_set_trust_root(ctx, der)licd_set_trust_root(ctx, der)
ctx |
A |
der |
The root certificate, DER-encoded, as a raw vector. |
NULL, invisibly.
A named integer vector of the status codes the native library reports; ok
is zero and every failure is negative. A failed call signals a condition of
class licd_error whose status field holds one of these.
licd_status_codeslicd_status_codes
A named integer vector with 21 entries.
A named integer vector: each name is a status (such as no_device)
and each value the code the native library returns for it, 0 for ok
and negative for a failure. Compare it with the status field of a
licd_error.
licd_status_codes[["no_device"]] names(licd_status_codes)[licd_status_codes == -14L]licd_status_codes[["no_device"]] names(licd_status_codes)[licd_status_codes == -14L]
Text for a status code
licd_strerror(status)licd_strerror(status)
status |
One or more status codes, see licd_status_codes. |
A character vector with the native library's short text for each code.
# NULL where the native library is not installed. tryCatch(licd_strerror(licd_status_codes[["no_device"]]), error = function(e) NULL)# NULL where the native library is not installed. tryCatch(licd_strerror(licd_status_codes[["no_device"]]), error = function(e) NULL)
Verifies the dongle's certificate chain against the trusted root and runs a
live challenge-response against the key inside the dongle.
licd_verify_genuine() signals a condition of class licd_not_genuine (or
licd_certificate_invalid) when the proof fails; licd_is_genuine() is the
non-signalling form for a gate and fails closed: a missing dongle, an I/O
error and an invalid certificate all give FALSE.
licd_verify_genuine(dongle) licd_is_genuine(dongle)licd_verify_genuine(dongle) licd_is_genuine(dongle)
dongle |
A |
Read the SDK's integration security guide before building a check on the
answer. if (!licd_is_genuine(dongle)) stop() is one line to delete from a
program that ships as source; what cannot be deleted is data the program
needs and only the dongle can decrypt, see licd_app_encrypt().
licd_verify_genuine(): a list with genuine (TRUE), serial
(from the verified certificate) and provisioned_date ("YYYY-MM-DD", or
""). licd_is_genuine(): TRUE or FALSE.
# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_is_genuine(dongle)) result <- tryCatch(licd_verify_genuine(dongle), licd_error = function(e) e) print(if (inherits(result, "licd_error")) conditionMessage(result) else result$serial) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well# Needs the native library and an attached dongle; does nothing without them. ctx <- tryCatch(licd_context(), error = function(e) NULL) dongle <- if (!is.null(ctx)) tryCatch(licd_open(ctx), licd_error = function(e) NULL) if (!is.null(dongle)) { print(licd_is_genuine(dongle)) result <- tryCatch(licd_verify_genuine(dongle), licd_error = function(e) e) print(if (inherits(result, "licd_error")) conditionMessage(result) else result$serial) } if (!is.null(ctx)) licd_close(ctx) # closes the dongle as well
Version of the native library
licd_version()licd_version()
An integer vector of length three: major, minor and patch version of the loaded library, which is the SDK version it was built from.
# NULL where the native library is not installed. tryCatch(licd_version(), error = function(e) NULL)# NULL where the native library is not installed. tryCatch(licd_version(), error = function(e) NULL)
licd_write_auth() unlocks writing, erasing and counter increments for the
rest of the session with the dongle's write key, a P-256 private key in
PKCS#8 DER. This belongs in your licence-issuing tooling; never ship that key
in the application your users run. A key the dongle does not accept signals
licd_not_genuine.
licd_write_auth(dongle, key) licd_write_auth_rotate(dongle, key)licd_write_auth(dongle, key) licd_write_auth_rotate(dongle, key)
dongle |
A |
key |
The key as a raw vector, for example
|
licd_write_auth_rotate() replaces the dongle's write key with key, a key
you hold. It needs the write role, so call licd_write_auth() with the
current key first. That session keeps the write role; from the next session
on only the new key elevates, and the old one no longer works on this
dongle. Do this once per dongle, when it arrives: the factory key is public.
NULL, invisibly.
# Needs your write key, and replaces a record on the dongle. ## Not run: key <- readBin("my-write-key.der", "raw", file.size("my-write-key.der")) licd_with_session(dongle, { licd_write_auth(dongle, key) licd_record_write(dongle, "license", "expires 2027-12-31") }) ## End(Not run)# Needs your write key, and replaces a record on the dongle. ## Not run: key <- readBin("my-write-key.der", "raw", file.size("my-write-key.der")) licd_with_session(dongle, { licd_write_auth(dongle, key) licd_record_write(dongle, "license", "expires 2027-12-31") }) ## End(Not run)