Skip to content

Mask license keys in log entries and error messages - #58

Merged
jwrosewell merged 4 commits into
rcfrom
align/mask-keys
Nov 10, 2026
Merged

jwrosewell merged 4 commits into
rcfrom
align/mask-keys

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Keeps license keys out of log entries and error messages, as the License keys section of the logging specification on rc says. specifications#55 added that section and #123 reworded it, and both are on rc. The rule is that a license key must not appear in full in a log entry or an error message at any level. Only its last 4 characters are shown, with each character before them replaced by one *, so ABCDEFGH is written ****EFGH, and a key of 4 characters or fewer is replaced entirely. A URL that carries a key, and the message of an error an HTTP library raises, are masked the same way. There must be a test that a failed request carrying a license key produces log entries and error messages that hold only the masked form. The Cloud Request Engine page says the same of its License Key.

The problem

  1. The data update URL. The URL formatters add licenseKeys to the update URL, and the data update service logs that URL. The open data update draft #52 adds the specification's messages that end the sentence straight after the URL, so the full stop follows the key.
  2. The cloud's own error text. The cloud refuses a license key with an error that names the key in full. A made-up key sent with the public example resource key to GET https://cloud.51degrees.com/api/v4/accessibleproperties?resource=...&license=TESTKEYNOTREALABCDEFGH1234, and to POST https://cloud.51degrees.com/api/v4/id/redeem with the same key in the form, is answered on 10 November with HTTP 401 and { "status":"401", "errors": ["Invalid license key(s) supplied: 'TESTKEYNOTREALABCDEFGH1234'"] }. The Cloud Request Engine made each of those into a CloudRequestError whose message held the key, and the 51Did client put the body in the message and body of its redeem error. From there the key reached the log entries, flowData.errors, the AggregateError raised to the caller and the redeem error.
  3. Objects, arrays and an error's other properties. The cloud engine logs { source, message: errors }, an object, and keeps an array of errors on the flow data. An error also carries properties other than its message, such as CloudRequestError.errorMessage, which repeats the message, and the errors and cause of an AggregateError.

What changed

  1. LicenseKeys.mask in fiftyone.pipeline.core/licenseKeys.js.
    • It masks the value of a license, licence, licenseKey, licenseKeys, licenceKey or licenceKeys parameter in any case. Several keys separated by a comma or a bar, as written or encoded (%2C, %7C), are masked one by one. querystring.stringify in both URL formatters writes a bar as %7C.
    • A parameter's value stops before punctuation that ends a sentence or a clause (., ,, ;, :, ), !, ?), so licenseKeys=ABCDEFGH. gives licenseKeys=****EFGH..
    • A second argument takes the known keys, and each is masked wherever it appears, as written or encoded for a URL with upper or lower case hexadecimal digits. A value that holds several keys separated by a comma or a bar is masked as each of them. A known key of 4 characters or fewer is replaced wherever those characters appear, which the JSDoc says.
    • It masks the text inside arrays and plain objects, and every own property of an error, including the errors and the cause it holds, as well as its message and stack. A value that holds no key is returned as it is, so log entries that never held a key do not change. A value that holds one is returned as a masked copy, and the original is left alone.
  2. Pipeline.log and FlowData.setError call it, as before, so every log entry and every error kept on the flow data is masked by parameter name.
  3. The Cloud Request Engine (getErrorsFromResponse) masks its own license key in the text of every error it makes from the cloud's answer, before the error is logged, rejected or kept on the flow data. This is how .NET draft 51degrees-dotnet#119 and Java draft 51degrees-java#85 mask the engine's key, and like Java it masks each key in a value separated by a bar.
  4. The 51Did client (redeem) masks its licence key in the message and the body of each error it makes from a redeem answer, as Java draft Mask license keys where a key can reach a message, not on the path every detection takes #85 does in DidClient.readRedeem. The body is masked too, because Node prints an error's own properties when it is logged. The 51Did package depends on nothing in the Pipeline, so it masks with a short function of its own that gives the same result.

Tests

  • licenseKeys.test.js covers the mask, including ABCDEFGH followed by a full stop, %7C, known keys, text inside objects and arrays, an object that refers to itself, and an error's other properties, errors and cause.
  • cloudLicenseKeyMasking.test.js gives the Cloud Request Engine the cloud's exact answer, and a failing status with a body that is not JSON, and checks that the log entries, flowData.errors and the rejected AggregateError hold only the masked key, both when processing fails and when the engine cannot get ready. This is the test the page asks for.
  • dataUpdateKeyMasking.test.js also builds the update URL with each URL formatter and checks the message "An error occurred while downloading a data file update for ... from <url>. Update will be attempted again later." gives licenseKeys=****EFGH., and that two keys joined by a bar are masked one by one.
  • didClient.test.js checks a redeem answer naming the licence key, for a 401, a 400 and a 404.

Runs.

macOS was left out of this draft's full build under the 9 November decision. The rc head is built with macOS before it merges into main.

Not done here

  • A key that the package does not know, and that appears in a message without a parameter name before it, cannot be recognised and is not masked. The keys the package holds, being the Cloud Request Engine's and the 51Did client's, are known. The data update service holds no key of its own, only the URL, whose keys are masked by parameter name.
  • Resource keys are not masked, because the License keys section covers license keys only.

The Logging page says a license key must not appear in full in a log entry or
an error message, and that only its last 4 characters are shown, with every
character before them replaced, including in a URL.
…goes

The cloud refuses a license key with an error that names the key in
full, so the tests give the cloud request engine and the 51Did client
that answer and check the log entries, the flow data errors and the
rejected errors. They also check text inside objects, arrays and an
error's other properties, a key followed by a full stop, and keys joined
by a bar that the URL formatters write as %7C.
The cloud refuses a license key with an error that names the key, for
example "Invalid license key(s) supplied: '<key>'". The mask found a key
only after a parameter name, so that key reached the log entries,
flowData.errors and the rejected errors in full.

- LicenseKeys.mask takes the known keys as a second argument and masks
  each wherever it appears, as written or encoded for a URL. A value
  holding several keys separated by a comma or a bar is masked as each.
- The cloud request engine masks its own license key in the text of
  every error it makes from the cloud's answer, before the error is
  logged, rejected or kept on the flow data.
- The 51Did client masks its licence key in the message and body of
  each redeem error. The 51Did package depends on nothing in the
  Pipeline, so it has its own short mask.
- The mask now reaches text inside arrays and plain objects, and every
  own property of an error, including the errors and the cause it holds.
- A parameter's value stops before punctuation that ends a sentence, so
  ABCDEFGH followed by a full stop gives ****EFGH and the full stop.
- %7C, the encoded bar the URL formatters write, separates keys.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant