| Type: | Package |
| Title: | Client for the 'KeyNub' USB License Dongle |
| Version: | 1.1.1 |
| 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. |
| License: | Apache License (== 2.0) |
| URL: | https://www.keynub.com/developers/r/, https://github.com/AB-KeyNub/KeyNub-SDK |
| BugReports: | https://github.com/AB-KeyNub/KeyNub-SDK/issues |
| SystemRequirements: | the KeyNub License Dongle native library (keynub_licdongle) for the platform, see https://github.com/AB-KeyNub/KeyNub-SDK/blob/master/NATIVES.md |
| Encoding: | UTF-8 |
| Depends: | R (≥ 4.0) |
| NeedsCompilation: | yes |
| Config/roxygen2/version: | 8.1.0 |
| Packaged: | 2026-09-28 08:58:49 UTC; a-bre |
| Author: | Andreas Breitschopp [aut, cre], KeyNub [cph] |
| Maintainer: | Andreas Breitschopp <info@keynub.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-08 10:10:06 UTC |
KeyNubLicDongle: 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.
Author(s)
Maintainer: Andreas Breitschopp info@keynub.com
Authors:
Andreas Breitschopp info@keynub.com
Other contributors:
KeyNub [copyright holder]
See Also
Useful links:
Report bugs at https://github.com/AB-KeyNub/KeyNub-SDK/issues
Encrypt data that only a dongle can decrypt
Description
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.
Usage
licd_app_encrypt(dongle, data, scope = c("device", "developer"))
licd_app_decrypt(dongle, packed)
Arguments
dongle |
A |
data |
The plaintext: a raw vector, or a single string for its bytes. |
scope |
|
packed |
The sealed data from |
Details
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.
Value
A raw vector: the sealed data, or the recovered plaintext.
Examples
# 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
Close a context or a dongle
Description
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.
Usage
licd_close(x)
Arguments
x |
A |
Value
NULL, invisibly.
Examples
# 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)
}
Create a library context
Description
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.
Usage
licd_context()
Value
A licd_context object.
Examples
# 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)
}
Monotonic counters
Description
The dongle holds hardware counters that only ever go up. Reading needs a session; incrementing needs the write role and is irreversible.
Usage
licd_counter_read(dongle, id)
licd_counter_increment(dongle, id)
Arguments
dongle |
A |
id |
The counter, an integer from 0 upwards; the dongle reports
|
Value
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.
Examples
# 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
List the attached dongles
Description
Enumerates without opening anything.
Usage
licd_enumerate(ctx)
Arguments
ctx |
A |
Value
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.
Examples
# 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)
}
Errors signalled by this package
Description
Every failure the native library reports is signalled as a condition of
class licd_error (and error), with these fields:
Details
-
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.
Examples
# 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)
}
Diagnostic detail of the most recent failure
Description
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.
Usage
licd_error_detail(ctx)
Arguments
ctx |
A |
Value
A string, "" when the last call succeeded.
Plaintext device information
Description
Reads what the dongle reports without a session.
Usage
licd_info(dongle)
Arguments
dongle |
A |
Value
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().
Examples
# 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?
Description
Is a context or dongle still open?
Usage
licd_is_open(x)
Arguments
x |
A |
Value
TRUE while the handle can be used, FALSE once it has been closed,
explicitly or through its context.
Examples
# 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 native library in use
Description
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).
Usage
licd_library(path = NULL)
Arguments
path |
A file path to the library, or |
Value
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.
Examples
# The file the package has loaded, or would load on its first call.
licd_library()
Open a dongle
Description
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.
Usage
licd_open(ctx, serial = NULL)
licd_open_path(ctx, path)
Arguments
ctx |
A |
serial |
The serial as hex, or |
path |
A device path from |
Value
A licd_dongle object. Close it with licd_close().
Examples
# 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
Description
The records on the dongle
Usage
licd_record_list(dongle)
Arguments
dongle |
A |
Value
A data frame with the columns name and size (bytes), one row per
record; zero rows when the dongle holds none.
Examples
# 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
Read, write and erase records
Description
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.
Usage
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)
Arguments
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. |
Details
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.
Value
licd_record_read(): the record as a raw vector (rawToChar() gives
text back). The others: NULL, invisibly.
Examples
# 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
Description
The dongle's serial
Usage
licd_serial(dongle)
Arguments
dongle |
A |
Value
The serial as a hex string.
Open and close the encrypted session
Description
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.
Usage
licd_session_open(dongle)
licd_session_close(dongle)
licd_with_session(dongle, expr)
Arguments
dongle |
A |
expr |
An expression to evaluate while the session is open. |
Value
licd_session_open() and licd_session_close(): NULL, invisibly.
licd_with_session(): the value of expr.
Examples
# 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
Override the trust root
Description
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.
Usage
licd_set_trust_root(ctx, der)
Arguments
ctx |
A |
der |
The root certificate, DER-encoded, as a raw vector. |
Value
NULL, invisibly.
Status codes of the SDK
Description
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.
Usage
licd_status_codes
Format
A named integer vector with 21 entries.
Value
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.
See Also
Examples
licd_status_codes[["no_device"]]
names(licd_status_codes)[licd_status_codes == -14L]
Text for a status code
Description
Text for a status code
Usage
licd_strerror(status)
Arguments
status |
One or more status codes, see licd_status_codes. |
Value
A character vector with the native library's short text for each code.
Examples
# NULL where the native library is not installed.
tryCatch(licd_strerror(licd_status_codes[["no_device"]]), error = function(e) NULL)
Prove that a dongle is genuine
Description
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.
Usage
licd_verify_genuine(dongle)
licd_is_genuine(dongle)
Arguments
dongle |
A |
Details
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().
Value
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.
Examples
# 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
Description
Version of the native library
Usage
licd_version()
Value
An integer vector of length three: major, minor and patch version of the loaded library, which is the SDK version it was built from.
Examples
# NULL where the native library is not installed.
tryCatch(licd_version(), error = function(e) NULL)
Elevate to the write role
Description
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.
Usage
licd_write_auth(dongle, key)
licd_write_auth_rotate(dongle, key)
Arguments
dongle |
A |
key |
The key as a raw vector, for example
|
Details
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.
Value
NULL, invisibly.
Examples
# 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)