jev-secrets 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jev_secrets-1.0.0/.gitignore +11 -0
- jev_secrets-1.0.0/LICENSE +21 -0
- jev_secrets-1.0.0/PKG-INFO +342 -0
- jev_secrets-1.0.0/README.md +324 -0
- jev_secrets-1.0.0/catalogue/alphabets.json +130 -0
- jev_secrets-1.0.0/catalogue/fixtures.json +1793 -0
- jev_secrets-1.0.0/catalogue/patterns.json +129 -0
- jev_secrets-1.0.0/docs/dates.md +55 -0
- jev_secrets-1.0.0/docs/evidence.md +90 -0
- jev_secrets-1.0.0/docs/generation.md +99 -0
- jev_secrets-1.0.0/docs/performance.md +35 -0
- jev_secrets-1.0.0/pyproject.toml +51 -0
- jev_secrets-1.0.0/python/src/jev_secrets/__init__.py +18 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_catalogue.py +44 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_config.py +290 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_dates.py +333 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_detection.py +345 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_generation.py +366 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_pseudonymiser.py +489 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_secrets.py +51 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_text.py +87 -0
- jev_secrets-1.0.0/python/src/jev_secrets/_vault.py +150 -0
- jev_secrets-1.0.0/python/src/jev_secrets/errors.py +31 -0
- jev_secrets-1.0.0/python/src/jev_secrets/py.typed +0 -0
- jev_secrets-1.0.0/python/src/jev_secrets/typesafe.py +105 -0
- jev_secrets-1.0.0/python/tests/conftest.py +19 -0
- jev_secrets-1.0.0/python/tests/test_config.py +68 -0
- jev_secrets-1.0.0/python/tests/test_dates.py +103 -0
- jev_secrets-1.0.0/python/tests/test_evidence.py +62 -0
- jev_secrets-1.0.0/python/tests/test_fixtures.py +78 -0
- jev_secrets-1.0.0/python/tests/test_generator.py +83 -0
- jev_secrets-1.0.0/python/tests/test_key_and_overrides.py +95 -0
- jev_secrets-1.0.0/python/tests/test_package.py +10 -0
- jev_secrets-1.0.0/python/tests/test_restore.py +65 -0
- jev_secrets-1.0.0/python/tests/test_typesafe.py +78 -0
- jev_secrets-1.0.0/spec/config.schema.json +192 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fox Islam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jev-secrets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Reversible PII redaction for Jev requests
|
|
5
|
+
Author-email: Fox Islam <foxislam@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: jev,pii,pseudonymisation,redaction,system-one,typesafe
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Topic :: Security
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Provides-Extra: sdk
|
|
16
|
+
Requires-Dist: typesafe-sdk<1,>=0.7; extra == 'sdk'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# jev-secrets
|
|
20
|
+
|
|
21
|
+
Replaces names, keys and other secrets in a [Jev](https://docs.typesafe.ai) request with
|
|
22
|
+
stand-ins before it is sent, and puts the originals back in the answers. For the
|
|
23
|
+
[PHP SDK](https://github.com/Fox-Islam/typesafe-sdk-php) and the official
|
|
24
|
+
[JavaScript](https://www.npmjs.com/package/@typesafe-ai/sdk) and
|
|
25
|
+
[Python](https://pypi.org/project/typesafe-sdk/) SDKs, or any code that builds the request body
|
|
26
|
+
itself.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
John Smith <john.smith@acme.com> was charged twice on 2024-03-15. Key sk_live_4eC39HqLyjWD
|
|
30
|
+
Tozd Cpabl <tozd.cpabl@ulco.zuy> was charged twice on 2024-03-15. Key sk_live_3uF21GlNlxHG
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The second line is what Jev sees, with `John Smith` passed as a value and the rest found by the
|
|
34
|
+
default patterns. In each replaced word every letter becomes another letter of the same case,
|
|
35
|
+
vowels stay vowels, digits become digits, and punctuation stays, so a name still reads as a name
|
|
36
|
+
and an email address as an email address. The same word gets the same stand-in everywhere in the
|
|
37
|
+
request, including in the questions. The answers come back with the original question ids and
|
|
38
|
+
choice labels.
|
|
39
|
+
|
|
40
|
+
This is pseudonymisation, not anonymisation: the mapping is held in memory in order to be able
|
|
41
|
+
to reverse the process on the way back to the caller.
|
|
42
|
+
|
|
43
|
+
**Contents:** [Install](#install) · [Use](#use) · [What gets replaced](#what-gets-replaced) ·
|
|
44
|
+
[What a stand-in looks like](#what-a-stand-in-looks-like) · [The key](#the-key) ·
|
|
45
|
+
[What is rewritten and restored](#what-is-rewritten-and-restored) · [Config](#config) ·
|
|
46
|
+
[When it refuses to send](#when-it-refuses-to-send) · [What it does not do](#what-it-does-not-do) ·
|
|
47
|
+
[Does it change the answers?](#does-it-change-the-answers)
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
composer require phox/jev-secrets # PHP 8.3+, with ext-intl and ext-mbstring
|
|
53
|
+
npm install @phox-js/jev-secrets # Node 20.19+
|
|
54
|
+
pip install jev-secrets # Python 3.10+; jev-secrets[sdk] adds the SDK wrapper
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| | Tested on | SDK versions the wrapper is tested with |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| PHP | 8.3, 8.4, 8.5 | `phox/typesafe-sdk-php` 0.3 and 0.4 |
|
|
60
|
+
| JavaScript | Node 20.19, 22, 24 | `@typesafe-ai/sdk` 0.5.7 and 0.6.0 |
|
|
61
|
+
| Python | 3.10 to 3.14 | `typesafe-sdk` 0.7.0 and 0.7.1 |
|
|
62
|
+
|
|
63
|
+
A request of 1000 strings, 500 KB of JSON, takes 0.4-0.5 s in PHP and JavaScript and 1.1-1.4 s
|
|
64
|
+
in Python; [docs/performance.md](docs/performance.md) has the measurements.
|
|
65
|
+
|
|
66
|
+
## Use
|
|
67
|
+
|
|
68
|
+
With the SDK, wrap the client. Each call takes the values to hide in that request, on top of
|
|
69
|
+
whatever the config finds:
|
|
70
|
+
|
|
71
|
+
```php
|
|
72
|
+
use Phox\JevSecrets\Secrets;
|
|
73
|
+
use Phox\JevSecrets\TypeSafe\SecretClient;
|
|
74
|
+
|
|
75
|
+
$client = new SecretClient(new \Phox\TypeSafe\Client(), new Secrets(['terms' => ['Acme Ltd']]));
|
|
76
|
+
|
|
77
|
+
$response = $client->systemOne()
|
|
78
|
+
->state(['ticket' => $ticket])
|
|
79
|
+
->noul('refund', 'Does John Smith ask for a refund?')
|
|
80
|
+
->values(['John Smith'])
|
|
81
|
+
->send();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { TypeSafeClient } from '@typesafe-ai/sdk';
|
|
86
|
+
import { Secrets, withSecrets } from '@phox-js/jev-secrets';
|
|
87
|
+
|
|
88
|
+
const client = withSecrets(new TypeSafeClient(), new Secrets({ terms: ['Acme Ltd'] }));
|
|
89
|
+
const { answers } = await client.systemOne(
|
|
90
|
+
{ state: { ticket }, questions: { refund: { type: 'noul', instructions: 'Does John Smith ask for a refund?' } } },
|
|
91
|
+
{ values: ['John Smith'] },
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from typesafe_sdk import TypeSafeClient
|
|
97
|
+
from jev_secrets import Secrets
|
|
98
|
+
from jev_secrets.typesafe import SecretClient
|
|
99
|
+
|
|
100
|
+
client = SecretClient(TypeSafeClient(), Secrets({'terms': ['Acme Ltd']}))
|
|
101
|
+
result = client.system_one(
|
|
102
|
+
state={'ticket': ticket},
|
|
103
|
+
questions={'refund': {'type': 'noul', 'instructions': 'Does John Smith ask for a refund?'}},
|
|
104
|
+
values=['John Smith'],
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`jev_secrets.typesafe.AsyncSecretClient` wraps the async Python client. The wrappers sit above
|
|
109
|
+
the SDK, not in its transport, because the SDKs log request bodies at debug level before the
|
|
110
|
+
transport sees them.
|
|
111
|
+
|
|
112
|
+
Any config option can be changed at call time. `scope` and `dates` merge entry by entry; any
|
|
113
|
+
other option given replaces the configured one:
|
|
114
|
+
|
|
115
|
+
```php
|
|
116
|
+
$client->systemOne()->state($ticket)->noul('refund', 'Refund?')
|
|
117
|
+
->overrides(['disable' => ['phone'], 'dates' => ['order' => 'mdy']])
|
|
118
|
+
->send();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In JavaScript it is `options.overrides`, in Python `overrides=`, and without an SDK the third
|
|
122
|
+
argument of `pseudo()`.
|
|
123
|
+
|
|
124
|
+
Without an SDK, rewrite the body yourself and restore the decoded response:
|
|
125
|
+
|
|
126
|
+
```php
|
|
127
|
+
$done = (new Secrets())->pseudo($body, ['John Smith']);
|
|
128
|
+
$answers = $done->restore(json_decode(send($done->request), true));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`pseudo()` is also available as `pseudonymise()`, `pseudonymize()` and `mask()`, and `restore()`
|
|
132
|
+
as `unmask()`.
|
|
133
|
+
|
|
134
|
+
`$done->replacements` lists what was replaced, as a path into the sent request and the source
|
|
135
|
+
that found it, without the values themselves.
|
|
136
|
+
|
|
137
|
+
## What gets replaced
|
|
138
|
+
|
|
139
|
+
The sources, all optional:
|
|
140
|
+
|
|
141
|
+
| Source | What it matches |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| Values passed to the call | those strings |
|
|
144
|
+
| Config | `terms` (fixed strings), `fields` (paths whose whole value is replaced), `patterns` (your regexes) |
|
|
145
|
+
| Defaults | the patterns below, and the value under a key such as `password`, `token` or `api_key` |
|
|
146
|
+
|
|
147
|
+
`secretKeys` controls those key names: `false` for none, a list for exactly those, or
|
|
148
|
+
`{"add": [...], "remove": [...]}` to change the default list. Names compare lowercased,
|
|
149
|
+
ignoring spaces, hyphens and underscores, so `member_number` covers `Member-Number`.
|
|
150
|
+
|
|
151
|
+
Values and terms match case-insensitively, at word boundaries. Their words may be joined by
|
|
152
|
+
any run of spaces, underscores, hyphens or dots, so `Ellen Park` is also found in
|
|
153
|
+
`is_from_ellen_park` and `ellen.park@example.com`. A value shorter than 2 characters is refused.
|
|
154
|
+
The value found at a field path is also replaced wherever else it appears in the request.
|
|
155
|
+
|
|
156
|
+
**Names cannot be found by pattern.** Pass them as values, list them as terms, or name the
|
|
157
|
+
fields they're in.
|
|
158
|
+
|
|
159
|
+
Dates, and numbers the default patterns do not cover, are not replaced by default. Pass one as a
|
|
160
|
+
value, or name its field, and a number is replaced digit by digit, a date or time moved
|
|
161
|
+
([below](#what-a-stand-in-looks-like)).
|
|
162
|
+
With `dates.detect: true` every date and time in the request is moved.
|
|
163
|
+
|
|
164
|
+
<!-- patterns -->
|
|
165
|
+
| Default pattern | Finds |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `email` | an email address |
|
|
168
|
+
| `ipv4` | a dotted-quad IPv4 address |
|
|
169
|
+
| `ipv6` | an IPv6 address, full or compressed, with at least one decimal digit |
|
|
170
|
+
| `card` | a card number of 12 to 19 digits that passes the Luhn check |
|
|
171
|
+
| `iban` | an IBAN that passes the mod-97 check |
|
|
172
|
+
| `phone` | a phone number written with a leading `+` and 8 to 15 digits |
|
|
173
|
+
| `us-ssn` | a US Social Security number written with dashes |
|
|
174
|
+
| `uk-nino` | a UK National Insurance number |
|
|
175
|
+
| `aws-access-key` | an AWS access key id, keeping its prefix, such as `AKIA` |
|
|
176
|
+
| `github-token` | a GitHub token, keeping its prefix, such as `ghp_` |
|
|
177
|
+
| `github-pat` | a fine-grained GitHub token, keeping the `github_pat_` prefix |
|
|
178
|
+
| `stripe-key` | a Stripe secret or restricted key, keeping its prefix, such as `sk_live_` |
|
|
179
|
+
| `slack-token` | a Slack token, keeping its prefix, such as `xoxb-` |
|
|
180
|
+
| `anthropic-key` | an Anthropic API key, keeping its prefix, such as `sk-ant-api03-` |
|
|
181
|
+
| `openai-key` | an OpenAI API key, keeping its prefix, such as `sk-proj-` |
|
|
182
|
+
| `google-api-key` | a Google API key, keeping the `AIza` prefix |
|
|
183
|
+
| `jwt` | a JSON Web Token, keeping the leading `eyJ` |
|
|
184
|
+
| `private-key` | the body of a PEM private key, keeping the BEGIN and END lines |
|
|
185
|
+
| `bearer-token` | a bearer token of 16 or more characters, keeping the word `Bearer` |
|
|
186
|
+
| `credential-assignment` | a value after `password=`, `api_key:`, `token=` and the like in text, keeping the name |
|
|
187
|
+
<!-- /patterns -->
|
|
188
|
+
|
|
189
|
+
Where two matches overlap they merge, so a value inside an email address takes the rest of the
|
|
190
|
+
address with it. `catalogue/patterns.json` holds the patterns and the key names; the PHP,
|
|
191
|
+
JavaScript and Python implementations read the same file.
|
|
192
|
+
|
|
193
|
+
## What a stand-in looks like
|
|
194
|
+
|
|
195
|
+
Recorded outputs of the character generator under a fixed key, from `catalogue/fixtures.json`:
|
|
196
|
+
|
|
197
|
+
<!-- stand-ins -->
|
|
198
|
+
| Input | Stand-in |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `John` / `JOHN` | `Tozd` / `TOZD` |
|
|
201
|
+
| `O'Brien-Smythe` | `I'Ghees-Dmjcbu` |
|
|
202
|
+
| `José Müller` | `Taré Hömsif` |
|
|
203
|
+
| `Дмитрий Иванов` | `Ьхэвлаӗ Ычозюв` |
|
|
204
|
+
| `김민준` | `펇쫹떀` |
|
|
205
|
+
| `2024-12-31 23:59` | `1011-11-21 12:33` |
|
|
206
|
+
| `10.0.0.1` | `22.9.9.7` |
|
|
207
|
+
<!-- /stand-ins -->
|
|
208
|
+
|
|
209
|
+
- **Letters:** each becomes another from the same alphabet and case. With `letters: "shape"`,
|
|
210
|
+
the default, vowels stay vowels and consonants stay consonants, so the stand-in is
|
|
211
|
+
pronounceable. Latin, Greek, Cyrillic, Armenian, Georgian, Hebrew, Arabic, Devanagari, Thai,
|
|
212
|
+
kana, Hangul and CJK are drawn from their own scripts; a letter from any other script becomes
|
|
213
|
+
an ASCII letter. Accents stay on.
|
|
214
|
+
- **Digits:** with `digits: "lower"`, the default, 0 and 1 stay and every other digit becomes a
|
|
215
|
+
lower one of at least 1, so an IP address or a version stays valid. A span made only of 0s
|
|
216
|
+
and 1s, like `10.0.0.1`, is drawn under `digits: "any"` instead.
|
|
217
|
+
- **Dates and times** are moved, not rewritten: every date in the request by the same number of
|
|
218
|
+
weeks, every time by the same number of minutes, without crossing midnight. Order, intervals
|
|
219
|
+
and weekdays survive, and the format stays as written, so `Monday, 16 March 2026 at 3:40 pm`
|
|
220
|
+
becomes `Monday, 12 October 2026 at 11:46 am`. `dates.order` decides whether `03/04/2026` is
|
|
221
|
+
3 April (`dmy`, the default) or 4 March (`mdy`); a year written first is always year, month,
|
|
222
|
+
day. With `dates.mode: "digits"` they get the digit rule instead, which is how the
|
|
223
|
+
`2024-12-31 23:59` row above was made. [docs/dates.md](docs/dates.md) lists the formats.
|
|
224
|
+
- **Everything else** stays: spaces, punctuation, `@`, `.`, `-`, `_`.
|
|
225
|
+
|
|
226
|
+
A stand-in is drawn again until it differs from every word already in the request and from
|
|
227
|
+
every other stand-in. Short numbers under the digit rule can run out of stand-ins and share one;
|
|
228
|
+
[docs/generation.md](docs/generation.md) has the exact rules.
|
|
229
|
+
|
|
230
|
+
## The key
|
|
231
|
+
|
|
232
|
+
Stand-ins and date offsets are drawn under a key:
|
|
233
|
+
|
|
234
|
+
| `key` | What each call uses |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| left out | the `JEV_SECRETS_KEY` environment variable, read on every call; without it, a random key |
|
|
237
|
+
| a string | that string |
|
|
238
|
+
| `false` | a random key, whatever the environment holds |
|
|
239
|
+
|
|
240
|
+
With a random key the stand-ins change from call to call. With a fixed one the same request
|
|
241
|
+
gives the same stand-ins every time, in every implementation. Nothing is stored between calls,
|
|
242
|
+
so rotating the key costs nothing: calls after the change get different stand-ins. The library
|
|
243
|
+
reads the process environment, not a `.env` file; load that the way your framework does.
|
|
244
|
+
|
|
245
|
+
## What is rewritten and restored
|
|
246
|
+
|
|
247
|
+
| Part of the request | Rewritten | Restored in the answers |
|
|
248
|
+
| --- | --- | --- |
|
|
249
|
+
| `state` strings and numbers | yes | - |
|
|
250
|
+
| `state` object keys | with `scope.keys` | - |
|
|
251
|
+
| question `instructions` and `criteria` text | yes | a score's `legend`, exactly as sent |
|
|
252
|
+
| question ids | where they contain a match | the answer keys |
|
|
253
|
+
| choice labels | where they contain a match | `choice` and the keys of `probabilities` |
|
|
254
|
+
| noul criteria keys, `type`, `model`, anything else at the top level | never | - |
|
|
255
|
+
|
|
256
|
+
A score's legend keys and probability keys are rubric indices and are never restored, nor is
|
|
257
|
+
any number in the response. A string field in an answer that this library does not know gets
|
|
258
|
+
word-by-word replacement of the stand-ins.
|
|
259
|
+
|
|
260
|
+
## Config
|
|
261
|
+
|
|
262
|
+
The same object in PHP, JavaScript and Python.
|
|
263
|
+
[spec/config.schema.json](spec/config.schema.json) has every option and its default.
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"terms": ["Acme Ltd", "Project Nightingale"],
|
|
268
|
+
"fields": ["state.customer.name", "state.orders.*.email"],
|
|
269
|
+
"patterns": [{ "id": "order-id", "pattern": "ORD-(?<value>[0-9]{6})" }],
|
|
270
|
+
"allow": ["support@acme.com"],
|
|
271
|
+
"disable": ["ipv4"],
|
|
272
|
+
"scope": { "keys": true },
|
|
273
|
+
"secretKeys": { "add": ["member_number"] },
|
|
274
|
+
"dates": { "order": "mdy", "detect": true }
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`allow` protects a string from terms, fields, patterns and the defaults; a value passed to a
|
|
279
|
+
call still replaces it.
|
|
280
|
+
|
|
281
|
+
## When it refuses to send
|
|
282
|
+
|
|
283
|
+
After rewriting, every original value is looked for again across every string and key under
|
|
284
|
+
`state` and `questions`. If one is still there, the call throws `LeakException` (`LeakError` in
|
|
285
|
+
JavaScript and Python) and nothing is sent. The message names the path and the source, never
|
|
286
|
+
the value, and suggests the fix for the usual causes: a value in an object key while
|
|
287
|
+
`scope.keys` is off, or in the questions while `scope.questions` is off.
|
|
288
|
+
|
|
289
|
+
In PHP, a request that cannot be searched is refused too: text that is not valid UTF-8, or a
|
|
290
|
+
regex that stops at PCRE's backtrack limit (`MatchException`). Matching cannot say what it
|
|
291
|
+
missed, so nothing is sent. JavaScript and Python strings are always Unicode, and their regexes
|
|
292
|
+
have no such limit.
|
|
293
|
+
|
|
294
|
+
A config that cannot work is refused when it is made: an unknown option, a pattern that does not
|
|
295
|
+
compile, a term with no letter or digit or shorter than `minLength`. A value passed to a call is
|
|
296
|
+
checked the same way when the call is made.
|
|
297
|
+
|
|
298
|
+
## What it does not do
|
|
299
|
+
|
|
300
|
+
- **Find names by itself.** Only values, terms and fields hide names.
|
|
301
|
+
- **Hide shape.** A stand-in has the same length, capitals, punctuation and word count as the
|
|
302
|
+
original, and the same word always gets the same stand-in, so the number of times a name
|
|
303
|
+
appears is visible.
|
|
304
|
+
- **Hide every digit.** Under the default digit rule 0 and 1 are kept and 2 can only become 1.
|
|
305
|
+
- **Keep order between numbers.** Each number is lowered on its own, so 19 and 22 can come out
|
|
306
|
+
as 17 and 11, and two short numbers can share a stand-in when the rule leaves no other in
|
|
307
|
+
range. Dates and times keep their order unless `dates.mode` is `digits`.
|
|
308
|
+
- **Keep a date's relation to the real world.** A moved date is up to a year away from the
|
|
309
|
+
original, so "is this overdue?" against today, "last Friday" or an age written in words no
|
|
310
|
+
longer line up.
|
|
311
|
+
- **Catch everything the defaults target.** The patterns trade recall for precision: a phone
|
|
312
|
+
number without a leading `+` is missed, a version string like `1.2.3.4` reads as an IPv4
|
|
313
|
+
address, and about one in ten 12 to 19 digit numbers passes the Luhn check by chance.
|
|
314
|
+
- **Unlink calls under a fixed key.** With a fixed key, from the config or `JEV_SECRETS_KEY`, a
|
|
315
|
+
word gets the same stand-in in every call, so whoever receives the requests can tell that two
|
|
316
|
+
calls mention the same person, and anyone holding the key can test a guessed name against a
|
|
317
|
+
stand-in. Use a random key unless you need repeatable output.
|
|
318
|
+
- **Keep an object keyed `"0"`, `"1"`, ... an object in PHP.** PHP decodes it, and `{}`, as a
|
|
319
|
+
list, so its keys are not rewritten and it is sent as a JSON array. The PHP SDK has the same
|
|
320
|
+
limit. For the same reason `secretKeys: {}` means no secret keys in PHP, and no change to the
|
|
321
|
+
default list in JavaScript and Python.
|
|
322
|
+
- **Keep numeric-keyed requests identical in JavaScript.** JavaScript orders integer-like object
|
|
323
|
+
keys ascending, and stand-ins that collide are redrawn in walk order.
|
|
324
|
+
|
|
325
|
+
## Does it change the answers?
|
|
326
|
+
|
|
327
|
+
Over 67 labelled questions on 50 PII-heavy requests ([docs/evidence.md](docs/evidence.md)):
|
|
328
|
+
|
|
329
|
+
- The default config got 64 of 67 right in both runs; tags got 58, the plain request 67. The
|
|
330
|
+
default moved answers least, by 0.05 on average against 0.17 for tags.
|
|
331
|
+
- All 16 questions comparing dates or times kept their answers: every date and time moves by
|
|
332
|
+
the same offset. Names in Chinese, Arabic, Greek and Japanese script made no difference.
|
|
333
|
+
- What replacing removes is lost: whether an email domain is a company's, or `+44` a UK number.
|
|
334
|
+
- Comparing two plain numbers passed as values fails, since the digit rule keeps neither size
|
|
335
|
+
nor order. Leave out of `values` any number a question compares.
|
|
336
|
+
|
|
337
|
+
## Documentation
|
|
338
|
+
|
|
339
|
+
[docs/generation.md](docs/generation.md) and [docs/dates.md](docs/dates.md) are the byte-level
|
|
340
|
+
contract the implementations share. `catalogue/fixtures.json` holds the cases each must
|
|
341
|
+
reproduce: `php scripts/golden.php` records them from the PHP implementation, and the JavaScript
|
|
342
|
+
and Python suites require the same output.
|