{"id":21513241,"url":"https://github.com/patrickfav/id-mask","last_synced_at":"2025-04-09T18:53:06.273Z","repository":{"id":57714976,"uuid":"178486098","full_name":"patrickfav/id-mask","owner":"patrickfav","description":"IDMask is a Java library for masking internal ids (e.g. from your DB) when they need to be published to hide their actual value and to prevent forging. It has support optional randomisation has a wide support for various Java types including long, UUID and BigInteger.","archived":false,"fork":false,"pushed_at":"2024-01-15T19:43:34.000Z","size":892,"stargazers_count":69,"open_issues_count":4,"forks_count":3,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-03-23T20:51:18.681Z","etag":null,"topics":["64bit","aes-encryption","biginteger","byte-array","database-ids","hashid","hashids","hkdf","hmac","id","integer","jackson","jax-rs","long","obfuscation","paramconverter","serializer","uuid"],"latest_commit_sha":null,"homepage":"https://favr.dev/opensource/id-mask","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patrickfav.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-03-29T23:05:59.000Z","updated_at":"2025-03-19T03:46:11.000Z","dependencies_parsed_at":"2022-09-02T22:11:15.098Z","dependency_job_id":null,"html_url":"https://github.com/patrickfav/id-mask","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickfav%2Fid-mask","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickfav%2Fid-mask/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickfav%2Fid-mask/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickfav%2Fid-mask/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patrickfav","download_url":"https://codeload.github.com/patrickfav/id-mask/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248093378,"owners_count":21046674,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["64bit","aes-encryption","biginteger","byte-array","database-ids","hashid","hashids","hkdf","hmac","id","integer","jackson","jax-rs","long","obfuscation","paramconverter","serializer","uuid"],"created_at":"2024-11-23T22:55:07.734Z","updated_at":"2025-04-09T18:53:06.242Z","avatar_url":"https://github.com/patrickfav.png","language":"Java","readme":"\n\n# IDMask - Encryption and Obfuscation of IDs\n\n\u003cimg src=\"https://raw.githubusercontent.com/patrickfav/id-mask/main/misc/icon_sm.png\" align=\"right\"\n     alt=\"IDMask Logo\" width=\"128\" height=\"128\" style=\"padding: 0px 8px 0 8px;\"\u003e\n\nIDMask is a Java library for masking **internal IDs** (e.g. from your DB) when they need to be publicly published to **hide their actual value and to prevent forging**. This should make it very hard for an attacker to **understand** provided IDs (e.g. by witnessing a sequence, deducting how many orders you had, etc.) and **prevent guessing** of possible valid ones. Masking is **fully reversible** and also supports optional **randomization** for e.g. **shareable links** or **one-time tokens**. It has a wide support for various **Java types** including `long`, `UUID` and `BigInteger`. This library bases its security on **strong cryptographic primitives** ([AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard), [HMAC](https://en.wikipedia.org/wiki/HMAC), [HKDF](https://en.wikipedia.org/wiki/HKDF)) to create a secure encryption schema. It was inspired by [HashIds](https://hashids.org/) but tries to tackle most of its shortcomings.\n\n[![Maven Central](https://img.shields.io/maven-central/v/at.favre.lib/id-mask)](https://mvnrepository.com/artifact/at.favre.lib/id-mask)\n[![Github Actions](https://github.com/patrickfav/id-mask/actions/workflows/build_deploy.yml/badge.svg)](https://github.com/patrickfav/id-mask/actions)\n[![Javadocs](https://www.javadoc.io/badge/at.favre.lib/id-mask.svg)](https://www.javadoc.io/doc/at.favre.lib/id-mask)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=patrickfav_id-mask\u0026metric=coverage)](https://sonarcloud.io/summary/new_code?id=patrickfav_id-mask)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=patrickfav_id-mask\u0026metric=security_rating)](https://sonarcloud.io/summary/new_code?id=patrickfav_id-mask)\n\n## Feature Overview\n\n* **Secure**: Creates encrypted IDs with **no-nonsense cryptography** ([AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard), [HKDF](https://en.wikipedia.org/wiki/HKDF)) including **forgery protection** ([HMAC](https://en.wikipedia.org/wiki/HMAC))\n* **Wide range of Java type support**: mask IDs from `long`, `UUID`, `BigInteger`, `LongTuple` and `byte[]`\n* **Full support of types**: no arbitrary restrictions like \"only positive longs\", etc.\n* **ID randomization**: if enabled, will create IDs which appear uncorrelated with the same underlying value.\n* **No collisions possible**: because the IDs are not hashed or otherwise compressed, collisions are impossible\n* **Built-in caching support**: to increase performance a simple caching framework can be facilitated.\n* **Lightweight \u0026 Easy-to-use**: the library has only minimal dependencies and a straight forward API\n* **Fast**: 8 byte IDs take about `2µs` and 16 byte IDs `7µs` to mask on a fast desktop machine (see [_JMH Benchmarks_](https://github.com/patrickfav/id-mask/tree/master/misc/jmh-reports))\n* **Supports multiple encodings**: Depending on your requirement (short IDs vs. readability vs. should not contain words) multiple encodings are available including [Base64](https://en.wikipedia.org/wiki/Base64), [Base32](https://en.wikipedia.org/wiki/Base32) and [Hex](https://en.wikipedia.org/wiki/Hexadecimal) with the option of providing a custom one.\n* **Includes default implementations for various serializer**: Jackson serializer, JAX-RS `ParamConverter`\n\nThe code is compiled with target [Java 7](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_7) to keep backwards compatibility with *Android* and older *Java* applications.\n\n## Quickstart\n\nAdd the dependency to your `pom.xml` ([check latest release](https://github.com/patrickfav/id-mask/releases)):\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eat.favre.lib\u003c/groupId\u003e\n    \u003cartifactId\u003eid-mask\u003c/artifactId\u003e\n    \u003cversion\u003e{latest-version}\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nA very simple example using 64-bit integers ([`long`](https://docs.oracle.com/javase/7/docs/api/java/lang/Long.html)):\n\n```java\nbyte[] key = Bytes.random(16).array();\nlong id = ...\n\nIdMask\u003cLong\u003e idMask = IdMasks.forLongIds(Config.builder(key).build());\n\nString maskedId = idMask.mask(id);\n//example: NPSBolhMyabUBdTyanrbqT8\nlong originalId = idMask.unmask(maskedId);\n```\n\nalternatively using [`UUIDs`](https://docs.oracle.com/javase/7/docs/api/java/util/UUID.html):\n\n```java\nUUID id = UUID.fromString(\"eb1c6999-5fc1-4d5f-b98a-792949c38c45\");\n\nIdMask\u003cUUID\u003e idMask = IdMasks.forUuids(Config.builder(key).build());\n\nString maskedId = idMask.mask(id);\n//example: rK0wpnG1lwvG0xiZn5swxOYmAvxhA4A7yg\nUUID originalId = idMask.unmask(maskedId);\n```\n\nExamples for other java types (e.g. [`BigInteger`](https://docs.oracle.com/javase/7/docs/api/java/math/BigInteger.html), [`byte[]`](https://docs.oracle.com/javase/7/docs/api/java/lang/Byte.html) and `LongTuple`), see below.\n\n## How-To\n\nThe following section explains in detail how to use and configure IDMask:\n\n* **Step 1:** How to create your secret key\n    * Option A: Random Number Generator CLI\n    * Option B: Generate a Random Key within a Java Runtime\n* **Step 2:** Select the Java type to use\n    * Option A: `long`\n    * Option B: `UUID` \n    * Option C: `BigInteger`\n    * Option D: `LongTuple`\n    * Option E: `byte[]`\n    * Option F: multiple `int`\n* **Step 3:** Adjust the configuration to your needs\n    * Q1: Deterministic or Random?\n    * Q2: Which encoding?\n    * Q3: Caching?\n    * Q4: Advanced Security Features?\n* **Additional Features**: key migration, error-handling, converter for Jackson and JAX-RS\n\n### Step 1: Create a Secret Key\n\nIDMask's security relies on the strength of the used cryptographic key. In its rawest from, a secret key is basically just a random byte array. A provided key should be at least 16 bytes long (longer _usually_ doesn't translate to better security). IDMask requires it to be between 12 and 64. There are multiple ways to manage secret keys, if your project already has a managed [`KeyStore`](https://docs.oracle.com/javase/7/docs/api/java/security/KeyStore.html) or similar, use it. Otherwise, you could just hardcode the key in your code. This, of course, only makes sense where the client doesn't have access to the source or binary (i.e. in a backend scenario). Here are some suggestion on how to create your secret key:\n\n#### Option A: Use Random Number Generator CLI\n\nOne of the easiest ways to create a high quality key is to use this java cli: [Dice](https://github.com/patrickfav/dice/releases). Just download the `.jar` (or `.exe`) and run:\n\n    java -jar dice.jar 16 -e \"java\"\n\nThis will generate multiple 16 byte long syntactically correct java byte arrays:\n\n    new byte[]{(byte) 0xE4, (byte) 0x8A, ...};\n\nYou could just hard code this value:\n\n    private static final byte[] ID_MASK_KEY = new byte[]{(byte) 0xE4, (byte) 0x8A, ...};\n    \n#### Option B: Generate a Random Key within a Java Runtime\n\nEither in the [debugger](https://www.jetbrains.com/help/idea/debugging-your-first-java-application.html), simple application or any other [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) execute the following code (IDMask must be in classpath):\n\n    Bytes.random(16).encodeHex();\n\nwhich will create a random byte array using `SecureRandom` and encodes it as [hex string](https://en.wikipedia.org/wiki/Hexadecimal). In your code use this hex parser and the previously generated string:\n\n    private static final byte[] ID_MASK_KEY = Bytes.parseHex(\"e48a....\").array();\n\nEither way, don't worry too much as the library supports changing the secret key while still supporting unmasking of older IDs.\n\n### Step 2: Choosing the correct Type\n\nIDMask basically supports 2 data types:\n\n* 64 bit long words (8 byte): `long`\n* 128 bit long words  (16 byte): `UUID`, `BigInteger`, `byte[]`, `LongTuple`\n\nData types with these byte lengths can be represented as various Java types often used as identifiers:\n\n#### Option A: 64-bit integers (long)\n\nThe most common case and the only one fitting in the '8 byte' category is an id with the type [`long`](https://docs.oracle.com/javase/7/docs/api/java/lang/Long.html). \nIn Java a `long` is a signed integer and can represent `-2^63` to `2^63 -1`. IDMask can mask any valid `long` value.\nInternally it will be represented as 8 byte, two's complement.\n\nCreate a new instance by calling:\n\n```java\nIdMask\u003cLong\u003e idMask = IdMasks.forLongIds(Config.builder(key).build());\nString masked = idMask.mask(-588461182736122L);\n```\n\nIt is of course possible to also pass `int` types by casting them:\n\n```java\nString masked = idMask.mask((long) 1780);\n```\n\nNote that the generated masked id will not be shorter when using 32-bit integers.\n\n#### Option B: Universally unique identifier (UUIDs)\n\nA [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) is a 128 bit long identifier (with 122 bit entropy) and\noften used in databases because one does not have to worry about sequences or duplicates, but can just generate\nunique IDs by choosing one randomly. Java has first level support for [UUIDs](https://docs.oracle.com/javase/7/docs/api/java/util/UUID.html) for generation, parsing and serialization.\n\nCreate a new instance by calling:\n\n```java\nIdMask\u003cUUID\u003e idMask = IdMasks.forUuids(Config.builder(key).build());\nString masked = idMask.mask(UUID.fromString(\"eb1c6999-5fc1-4d5f-b98a-792949c38c45\"));\n```\n\n#### Option C: Arbitrary-Precision Integers (BigInteger)\n\nIf your IDs are typed as [BigInteger](https://docs.oracle.com/javase/7/docs/api/java/math/BigInteger.html) you can either convert them to long (if they are bound to 64 bit) or use the specific IDMask implementation. Note that the big integer will be converted to a [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) byte representation and are supported *up to 15 byte length* (i.e. up to `2^120`).\n\n```java\nIdMask\u003cBigInteger\u003e idMask = IdMasks.forBigInteger(Config.builder(key).build());\nString masked = idMask.mask(BigInteger.ONE);\n```\n\n#### Option D: Tuple of 64-bit integers\n\nSometimes it makes sense to encode multiple IDs to a single serialized version. Use this if you want to combine two `long` IDs, making it even harder to understand individual ones within.\n\n```java\nIdMask\u003cLongTuple\u003e idMask = IdMasks.forLongTuples(Config.builder(key).build());\nString masked = idMask.mask(new LongTuple(182736128L, 33516718189976L));\n```\n\n#### Option E: 16 byte (128 bit) byte array\n\n**Only for advanced use cases.** The most generic way to represent a 128-bit id is as a byte array. Basically you may provide any data as long as it fits in 16 bytes. *Note, that this is not a general purpose encryption schema and your data might not be secure!* \n\n```java\nIdMask\u003cbyte[]\u003e idMask = IdMasks.for128bitNumbers(Config.builder(key).build());\nString masked = idMask.mask(new byte[] {0xE3, ....});\n```\n\n#### Option F: Encoding multiple 32-bit integers\n\nNot supported directly, but still quite easy to implement - if you use only 4 byte (32-bit) integers (`int`), you can encoded multiple numbers. \n\nUsing the `long` schema you can encode up to two of those:\n\n```java\nint intId1 = 1;\nint intId2 = 2;\n\nIdMask\u003cLong\u003e idMask = IdMasks.forLongIds(Config.builder(key).build());\n\nlong encodedInts = Bytes.from(intId1, intId2).toLong();\nString masked = idMask.mask(encodedInts);\nlong raw = idMask.unmask(masked);\nint[] originalIds = Bytes.from(raw).toIntArray(); // originalIds[0] == intId1; originalIds[1] == intId2\n```\n\nor using four 32-bit integers with the `byte[]` schema:\n\n```java\nint intId1 = 1;\nint intId2 = 2;\nint intId3 = 3;\nint intId4 = 4;\n\nIdMask\u003cbyte[]\u003e idMask = IdMasks.for128bitNumbers(Config.builder(key).build());\n\nbyte[] ids = Bytes.from(intId1, intId2, intId3, intId4).array();\nString masked = idMask.mask(ids);\nbyte[] raw = idMask.unmask(masked);\nint[] originalIds = Bytes.from(raw).toIntArray(); // originalIds[0] == intId1; originalIds[1] == intId2,...\n```\n\n#### Option G: Strings?\n\nPer design this library lacks the feature to mask string based IDs. This is to discourage using it as general purpose encryption library. In most cases strings are encoded data: e.g. `UUIDs` string representation, `hex`, `base64`, etc. Best practice would be to decode these to a byte array (or `UUID` if possible) and use any of the other options provided above. (Note: *technically* it would be possible to convert the string to e.g. [ASCII](https://en.wikipedia.org/wiki/ASCII) bytes and just feed it `IdMask\u003cbyte[]\u003e` if it's length is equal or under 16; **but this is highly discouraged**).\n\n### Step 3: IDMask Configuration\n\nUsually the default settings are fine for most use cases, however it may make sense to adapt to different usage scenarios with the following settings.\n\n#### Q1: Should Ids be deterministic or random?\n\nBy default, off, the masking algorithm supports randomization of generated IDs. This is achieved by creating a random number and using it as part of the encrypt scheme as well as appending it to the output of the masked id. Therefore, randomized IDs are longer than their deterministic counterpart. Randomization increases the obfuscation effectiveness but makes it impossible for a client to check equality. This usually makes sense with shareable links, random access tokens, or other one-time identifiers. Randomized IDs within models are probably a bad idea. \n\nFor instance these masked IDs all represent the same original id `70366123987523049`:\n\n```\nSUkHScdj3j9sE3B3K8KGzgc\nHx8KpcbNQb7MAAnKPW-H3D4\nwsKW652TCEDBjim8JfOmLbg\n```\n\nEnable with:\n\n```java\nConfig.builder(key)\n    .randomizedIds(true)\n    ...\n```        \n\n#### Q2: What encoding should I choose?\n\nThe library internally converts everything to bytes, encrypts it and then requires an encoding schema to make the output printable. Per default the url-safe version of Base64 ([RFC4648](https://tools.ietf.org/html/rfc4648)) is used. This is a well-supported, fast and reasonable space efficient (needs ~25% more storage than the raw bytes) encoding. Note that the output size is constant using the same settings a type and does _not_ grow or shrink depending on e.g. how big the number is.\n\nHowever, depending on your use case, you may want Ids that are easy to type, do not contain possible problematic words\nor require some maximum length. The library includes some built-in encodings which satisfy different requirements:\n\n\n| Encoding               | may contain words | easy to type                       | url safe | Length for 64 bit id (deterministic/randomized) | Length for 128 bit id (deterministic/randomized) | Example                              |\n|------------------------|-------------------|------------------------------------|----------|-------------------------------------------------|--------------------------------------------------|--------------------------------------|\n| Hex                    | no                | yes                                | yes      | 34/50                                           | 50/82                                            | `e5e53e09bbd37f8d8b9afdfbed776de6fe` |\n| Base32                 | yes               | yes                  | yes      | 28/40                                           | 40/66                                            | `XS6GLNDNQ2NSBWJRMWM3U72FTLLA`       |\n| Base32 (Safe Alphabet) | no curse words    | contains upper and lowercase       | yes      | 28/40                                           | 40/66                                            | `jKVJx8yQPP8wkBNQhZBkJr6vKhwH`       |\n| Base64                 | yes               | no                                 | yes      | 23/34                                           | 34/55                                            | `SkqktDj1MVEkiPMrwg1blfA`            |\n\nIf IDs should be as short as possible, you may look into using [Ascii85/Base85](https://en.wikipedia.org/wiki/Ascii85) with a Java implementation [here](https://github.com/fzakaria/ascii85); expect around 8% better space efficiency compared to Base64. \n\nChoose a different encoding by setting the following in the config builder:\n\n```java\nConfig.builder(key)\n    .encoding(new ByteToTextEncoding.Base32Rfc4648())\n    ...\n```\nImplement your own encoding by using the `ByteToTextEncoding` interface.\n\n#### Formatted IDs\n\nFor IDs that are better readable for humans you can use the `ByteToTextEncoding.Formatter` and use it to wrap any other encoding instance with: \n\n    ByteToTextEncoding.Formatter.wrap(myEncoding);\n\nFor example with Base32 this could look like this\n\n    SE4RT-7LNHU7X-X3TMJ-OJYNMDS-ETVQ\n\n#### Q3: Do you need Caching?\n\n By default, a simple in-memory [lru cache](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)) is enabled. This cache improves performance if recurring IDs are encoded/decoded - if this is not the case the cache should be disabled to safe memory.\n\nThis setting is responsible for disabling the cache:\n\n```java\nConfig.builder(key)\n    .enableCache(true)\n    ...\n```\n\nif you want to wire your own cache framework to the id mask library you may do so by implementing the `Cache` interface and setting:\n\n```java\nConfig.builder(key)\n    .cacheImpl(new MyHazelcastCache())\n    ...\n```\n\n#### Q4: Any other Advanced Security Features required?\n\nYou may provide your own [JCA provider](https://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html) (like [BouncyCastle](https://www.bouncycastle.org/)) or your own cryptographically secure pseudo-random number generator\n(i.e. a [SecureRandom](https://docs.oracle.com/javase/8/docs/api/java/security/SecureRandom.html) implementation). The provider is used to encrypt/decrypt with [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) and to calculate [HMACs](https://en.wikipedia.org/wiki/HMAC)\n\nExample:\n```java\nConfig.builder(key)\n    .secureRandom(new SecureRandom())\n    .securityProvider(Security.getProvider(\"BC\"))\n    ...\n```\n\n#### High Security Mode\n\nOnly applicable with 16 byte IDs (e.g. `UUID`, `byte[]`, `BigInteger`, ...) it is optionally possible to increase the security\nstrength of the masked id in expense for increased id lengths. By default, an 8-byte [MAC](https://en.wikipedia.org/wiki/Message_authentication_code)\n is appended to the ID and, if randomization is enabled, an 8-byte random nonce is prepended. In high security mode these \n numbers double to 16 byte, therefore high security IDs are 16 bytes longer. If you generate a massive amount of IDs (more than 2^32) or don't \n mind the longer output length, high security mode is recommended.\n\nIssue with smaller MAC is increased chance of not recognizing a forgery and issue with smaller randomization nonce is higher\nchance of finding duplicated randomization values and recognizing equal IDs (chance of duplicate after 5,000,000,000 randomized IDs\nwith 8 byte nonce is 50%). Increasing these numbers to 16 bytes make both those issue negligible.\n\n### A Full Example\n\nHere is a fully wired example using the generic byte array IDMask:\n\n```java\nIdMask\u003cbyte[]\u003e idMask = IdMasks.for128bitNumbers(\n        Config.builder(KeyManager.Factory.with(key))\n                .randomizedIds(true) //non-deterministic output\n                .enableCache(true)\n                .cacheImpl(new Cache.SimpleLruMemCache(64))\n                .encoding(new ByteToTextEncoding.Base32Rfc4648())\n                .secureRandom(new SecureRandom())\n                .securityProvider(Security.getProvider(\"BC\"))\n                .build());\n\nString maskedId = idMask.mask(id128bit);\n//example: RAKESQ32V37ORV5JX7FAWCFVV2PITRN5KMOKYBJBLNS42WCEA3FI2FIBXLKJGMTSZY\nbyte[] originalId = idMask.unmask(maskedId);\n```\n\n## Additional Features\n\n### Upgrade of used Secret Key\n\nIf you want to change the secret key, because e.g. it became compromised, but still want to be able to unmask IDs created\nwith the previous key, you can use the built-in key migration scheme. Since every created id encodes the id of the used key, it\nis possible to choose a different key decryption.\n\nHere is a full example: First a new instance with `key1` will be created. If no `key-id` is passed, the library uses\n`KeyManager.Factory.DEFAULT_KEY_ID`.\n\n```java\nlong id = 123456789L;\nbyte[] key1 = Bytes.random(16).array();\n\n// create new instance with your key\nIdMask\u003cLong\u003e idMask1 = IdMasks.forLongIds(Config.builder(key1).build());\nString maskKey1 = idMask1.mask(id);\n// e.g.: kpKOdqrNdmyx34-VxjTg6B4\n```\n\nIf you want to switch the active key, just generate a new one and also set the old one as legacy key. Mind to choose\na different `key-id`:\n\n```java\n// if key1 is compromised create a new key\nbyte[] key2 = Bytes.random(16).array();\n\n// set the new key as active key and add the old key as legacy key - us the DEFAULT_KEY_ID, is it is used if no key id is set\nIdMask\u003cLong\u003e idMask2 = IdMasks.forLongIds(\n        Config.builder(KeyManager.Factory.withKeyAndLegacyKeys(\n                new KeyManager.IdSecretKey(KeyManager.Factory.DEFAULT_KEY_ID+1, key2), //new key with a new key id\n                new KeyManager.IdSecretKey(KeyManager.Factory.DEFAULT_KEY_ID, key1))) //old key with the DEFAULT_KEY_ID\n                .build());\n```\n\nMasking the same id, with the new key will generate different output:\n\n```java\n// same id will create different output\nString maskKey2 = idMask2.mask(id);\n// e.g.: 3c1UMVvVK5SvNiOaT4btpiQ\n```\n\nUnmasking however will reveal the same underlying id, no matter if it was masked with `key1` or `key2`.\n\n```java\n// the new instance can unmask the old a new key\nassert idMask2.unmask(maskKey1).equals(idMask2.unmask(maskKey2));\n```\n\n_Be aware that changing the secret key, will destroy equality of masked IDs cached with clients or elsewhere._\n\n### Error Handling\n\nAn `IdMask` instance will basically throw 2 types of _unchecked exceptions_:\n\n1) `IllegalArgumentException`\n2) `IdMaskSecurityException` (`extends SecurityException`)\n\nThe first will be thrown on basic parameter validation errors which usually stems from incorrect use of the library (e.g. \nmasked id too long or short, null reference, etc.). The second are errors thrown from the id masking decryption logic which may be security\nrelevant. It would make sense to at least catch and log them. The `IdMaskSecurityException.getReason()` can be used to group\ndetailed causes.\n\n### Using in your Application\n\nVarious default implementation for value converter exist in the `ext.*` package. All dependencies for these converters\nare set to `provided` which means the caller needs to provide their own. This will prevent unnecessary dependencies for\nproject that use different frameworks.\n\n#### Jackson JSON Serializer/Deserializer\n\nThe [object serialization framework Jackson](https://github.com/FasterXML/jackson) (most often used with JSON serialization) has \na serializer/deserializer feature. A way to use it, is to extend any of the pre-implemented serializers found in `IdMaskJackson`\nand provide it with a IdMask instance:\n\n```java\npublic final class MyIdMaskLongSerializers {\n     private MyIdMaskLongSerializers() {\n     }\n \n     @Inject\n     private IdMask\u003cLong\u003e idMask;\n \n     public static final class Serializer extends IdMaskJackson.LongSerializer {\n         public Serializer() {\n             super(idMask);\n         }\n     }\n \n     public static final class Deserializer extends IdMaskJackson.LongDeserializer {\n         public Deserializer() {\n             super(idMask);\n         }\n     }\n}\n```\n\nand then add the annotations to the id you want to mask:\n\n```java\npublic class LongIdUser {\n     @JsonSerialize(using = MyIdMaskLongSerializers.Serializer.class)\n     @JsonDeserialize(using = MyIdMaskLongSerializers.Deserializer.class)\n     private final long id;\n...\n}\n```\n\n#### JAX-RS 2 Parameter Converter\n\nThe [Java API for RESTful Web Services ](https://en.wikipedia.org/wiki/Java_API_for_RESTful_Web_Services), JAX-RS 2 provides \nthe possibility to define so called [`ParamConverter\u003cT\u003e`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/ParamConverter.html) which\ncan be used to transparently convert between java types and string representations for query, path, header, cookie and header parameter.\n\nThe `IdMaskParamConverters` class contains various default implementations for the most used types. To make it work, you have\nto define a `ParamConverterProvider` and return the appropriate converter like so:\n\n```java\n@Provider\npublic static class MyParamConverterProvider implements ParamConverterProvider {\n\n    @Inject\n    private IdMask\u003cLong\u003e idMask;\n\n    @Override\n    public \u003cT\u003e ParamConverter\u003cT\u003e getConverter(Class\u003cT\u003e aClass, Type type, Annotation[] annotations) {\n\n        if (aClass.equals(MaskedLongId.class)) {\n            return (ParamConverter\u003cT\u003e) new IdMaskMaskedLongIdParamConverter(idMask);\n        } else if (aClass.equals(Long.class)) {\n            return (ParamConverter\u003cT\u003e) new IdMaskLongIdParamConverter(idMask);\n        }\n        ...\n    }\n}\n```\n\nNote that maybe you don't want to convert ALL long type values, so there is a simple wrapper class `MaskedLongId` which can\nbe used for easier type mapping instead of just `Long`.\n\n## Download\n\nThe artifacts are deployed to [jcenter](https://bintray.com/bintray/jcenter) and [Maven Central](https://search.maven.org/).\n\n### Maven\n\nAdd dependency to your `pom.xml`:\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eat.favre.lib\u003c/groupId\u003e\n    \u003cartifactId\u003eid-mask\u003c/artifactId\u003e\n    \u003cversion\u003e{latest-version}\u003c/version\u003e\n\u003c/dependency\u003e\n```\n### Gradle\n\nAdd to your `build.gradle` module dependencies:\n\n    implementation group: 'at.favre.lib', name: 'id-mask', version: '{latest-version}'\n\n### Local Jar\n\n[Grab jar from the latest release.](https://github.com/patrickfav/id-mask/releases/latest)\n\n## Description\n\n### Why?\n\nIDMask can be used in an environment, where you want to protect the knowledge of the value of your IDs. Usually a very\neasy workaround would be to add another column in your database and randomly create UUIDs and use this instead of your\ne.g. numeric IDs. However sometimes this is not feasible (e.g. having millions of rows) or you cannot change the DB schema.\nAdditionally, IDMask can make IDs appear random, a feature which cannot be satisfied with the above approach.\n\n#### When to use IDMask\n\n* If IDs are used which are easily guessable (i.e. simple sequence) and knowledge of this ID might reveal confidential information\n* If IDs expose row count in a database table, which in turn reveals business intelligence (e.g. how many orders per day, etc.)\n* For creating ad-hoc shareable links which should appear random to the public\n* For creating single-use tokens for various use cases\n\n#### When not to use IDMask\n\n* If it is feasible to create a new column with random UUIDs\n* If maximum performance is required\n\n### Performance\n\nIDMask requires a non-trivial amount of work to encrypt IDs. The 8-byte-schema only needs to encrypt a single AES block (which should be hardware accelerated with most CPUs). The 16-byte schema is more expensive, since it requires encryption of an AES block, one HKDF expand and a HMAC calculation. According to the JMH benchmark, you can expect multiple hundreds' encryption/decryption per ms. Compared to the performance HashIds, which is faster by a factor of about 1000, IDMask seems extremely slow, but in the grant scheme of things it probably doesn't make a difference if masking of a single id costs 2µs or 0.002µs - there will be no performance bottleneck either way.\n\n#### JMH Benchmark\n\nHere is an benchmark done on a [i7-7700k](https://ark.intel.com/content/www/us/en/ark/products/97129/intel-core-i7-7700k-processor-8m-cache-up-to-4-50-ghz.html):\n\n```\nBenchmark                                               Mode  Cnt      Score      Error  Units\nIdMaskAndHashIdsBenchmark.benchmarkHashIdEncode         avgt    3      2,372 ±    0,282  ns/op\nIdMaskAndHashIdsBenchmark.benchmarkHashIdEncodeDecode   avgt    3      3,396 ±    0,965  ns/op\nIdMaskAndHashIdsBenchmark.benchmarkIdMask16Byte         avgt    3   7530,858 ±  761,871  ns/op\nIdMaskAndHashIdsBenchmark.benchmarkIdMask8Byte          avgt    3   2863,481 ±  183,189  ns/op\nIdMaskAndHashIdsBenchmark.benchmarkMaskAndUnmask16Byte  avgt    3  15054,198 ± 2030,344  ns/op\nIdMaskAndHashIdsBenchmark.benchmarkMaskAndUnmask8Byte   avgt    3   4707,021 ±  164,998  ns/op\n```\n\n### Encryption Schema\n\nTwo slightly different encryption schemes are used to optimize for performance and output size for the specific case. \nFor each of these schemes two variation exist for deterministic and randomized encryption. As reference here are some [test-vectors](https://github.com/patrickfav/id-mask/wiki/test-vectors).\n\n#### 8 Byte Encryption Schema\n\nThis schema uses the following cryptographic primitives:\n\n* [AES-128](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) + [ECB](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_(ECB)) + [No Padding](https://en.wikipedia.org/wiki/Padding_(cryptography))\n\nUsing the full 16 byte AES block, we create a message containing of the 8 byte id (i.e. the plaintext) and an 8 byte\nreference value. Then we encrypt it with AES/ECB (since we encrypt only a single block, a block mode using an IV like CBC\nwouldn't make a difference):\n\n    message_d = ( refValue_1a | id )\n    maskedId_d = ciphertext_d = AES_ECB( message_d )\n\nWhen decrypting, we compare the reference value, and if it has changed we discard the id, since either the key is incorrect,\nor this was a forgery attempt:\n\n    AES_ECB( maskedId_d ) = refValue_1b | id \n    refValue_1a == refValue_1b\n\n##### Deterministic\n\nIn the deterministic mode the reference value is just an 8 byte long array of zeros.\n\n##### Randomized\n\nIn the randomized mode the reference value is a random 8 byte long array. Because the decryption requires knowledge\nof this value it will be prepended to the cipher text:\n\n    ciphertext_r = AES_ECB( refValue_rnd | id )\n    maskedId_r = refValue_rnd | ciphertext_r\n\n##### Version Byte\n\nBoth modes have a version byte prepended which will be xor-ed with the first byte of the cipher text for simple obfuscation:\n\n    obfuscated_version_byte = version_byte ^ ciphertext[0]\n    \nFinally, the message looks like this:\n\n    maskeId_msg_d = obfuscated_version_byte | maskedId_d\n    \nand     \n\n    maskeId_msg_r = obfuscated_version_byte | maskedId_r\n\nfor randomized encryption.\n\n##### Summary\n\nThis schema has the advantage of having forgery protection without the need for a dedicated MAC generation and also keeps\nthe output reasonable small with 16 + 1 byte.\n\n#### 16 Byte Encryption Schema\n\nThis schema uses the following cryptographic primitives:\n\n* [AES-128](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) + [CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_(CBC)) + [No Padding](https://en.wikipedia.org/wiki/Padding_(cryptography))\n* [HMAC-SHA256](https://en.wikipedia.org/wiki/HMAC)\n* [HKDF-HMAC-SHA512](https://en.wikipedia.org/wiki/HKDF)\n\nThe basic scheme works as follows:\n\nFirst create the required keys and nonce:\n\n```\nokm = hkdf.expand(key, entropy, 64);\nkey_s = okm[0-16];\niv_s = okm[16-32];\nmac_key_s = okm[32-64];\n\nkey ......... provided secret key\nentropy ..... 8 byte value. For randomized-ids it is a random value, otherwise zero bytes \n```       \n\nThen encrypt the id:\n\n```\nciphertext = AES_CBC( iv_s , id ^ entropy)\nmac = HMAC(ciphertext)\nmaskedId_msg= ciphertext | mac[0-8]\n\nid .......... id to mask (aka plaintext)\n```\n\noptionally if randomized IDs are enabled, also append `entropy` to the output:\n\n```\nmaskedId_msg_r = entropy | maskedId_msg\n```\n\nFinally, append the version byte (see explanation in 8 byte schema). Use either the randomized or deterministic version:\n\n```\nmaskeId_msg_r = obfuscated_version_byte | maskedId_msg_r\nmaskeId_msg_d = obfuscated_version_byte | maskedId_msg\n```\n\n### IDMask vs HashIds\n\nOne of the reasons this library was created, was that the author was not happy how HashIds solved the issue of\nobfuscating/encryption of IDs. These are the main criticism:\n\n#### Weak, home-brew cryptography\n\nHashIds encryption schema is basically: Have an alphabet. Shuffle it with Yates Algorithm and a salt provided by the user. Use this simple encoding schema to encode 53 bit integers. There is a well known [cryptanalysis](https://carnage.github.io/2015/08/cryptanalysis-of-hashids) and to be fair, the author of HashIds does not directly claim any security - BUT a library called HashId which is used to obfuscate IDs for security purpose which does not claim any actual security does not make sense to me.\n\n#### Arbitrary limitation inherited rom the original javascript implementation\n\nThe integer values in HashId must be positive and have an upper bound of 53-bit (instead of 64-bit). If IDs are created in a sequence in the DB, this limitation is probably irrelevant, but if you work with random 64-bit values, this might break your code. IDMask does not apply any restriction on Java's `long`; the value may be positive as well as negative in the full range of 2^64. Only `BigInteger` is restricted to 15 byte (or 2^120).\n\n\nIn summary, here is simple comparison table of the main points:\n\n|                    | IDMask                                    | HashId                       |\n|--------------------|-------------------------------------------|------------------------------|\n| Supported Types    | long, UUID, BigInteger, byte[], LongTuple | long, long[]                 |\n| Type Limitations   | long: none, BigInteger: max 15 byte, byte[]: max 16 byte       | only positive and max 2^53   |\n| Randomized IDs     | optional                                  | no                           |\n| Output Length      | fixed length                              | variable length              |\n| Encryption         | AES + HMAC                                | encoding + shuffled alphabet |\n| Performance        | 2-7 µs/op                                 | 0.003 µs/op                  |\n| Collision possible | no                                        | no                           |\n| Caching            | Built-In                                  | no                           |\n| Encodings          | Hex, Base32, Base64, Custom...            | customizable alphabet        |\n| Forgery Protection | HMAC (8-16 bytes)                         | no                           |\n\n## Security Relevant Information\n\n### OWASP Dependency Check\n\nThis project uses the [OWASP Dependency-Check](https://www.owasp.org/index.php/OWASP_Dependency_Check) which is a utility that identifies project dependencies and checks if there are any known, publicly disclosed, vulnerabilities against a [NIST database](https://nvd.nist.gov/vuln/data-feeds).\nThe build will fail if any issue is found.\n\n### Digital Signatures\n\n#### Signed Jar\n\nThe provided JARs in the GitHub release page are signed with my private key:\n\n    CN=Patrick Favre-Bulle, OU=Private, O=PF Github Open Source, L=Vienna, ST=Vienna, C=AT\n    Validity: Thu Sep 07 16:40:57 SGT 2017 to: Fri Feb 10 16:40:57 SGT 2034\n    SHA1: 06:DE:F2:C5:F7:BC:0C:11:ED:35:E2:0F:B1:9F:78:99:0F:BE:43:C4\n    SHA256: 2B:65:33:B0:1C:0D:2A:69:4E:2D:53:8F:29:D5:6C:D6:87:AF:06:42:1F:1A:EE:B3:3C:E0:6D:0B:65:A1:AA:88\n\nUse the jarsigner tool (found in your `$JAVA_HOME/bin` folder) folder to verify.\n\n#### Signed Commits\n\nAll tags and commits by me are signed with git with my private key:\n\n    GPG key ID: 4FDF85343912A3AB\n    Fingerprint: 2FB392FB05158589B767960C4FDF85343912A3AB\n\n## Build\n\n### Jar Sign\n\nIf you want to jar sign you need to provide a file `keystore.jks` in the\nroot folder with the correct credentials set in environment variables (\n`OPENSOURCE_PROJECTS_KS_PW` and `OPENSOURCE_PROJECTS_KEY_PW`); alias is\nset as `pfopensource`.\n\nIf you want to skip jar signing just change the skip configuration in the\n`pom.xml` jar sign plugin to true:\n\n    \u003cskip\u003etrue\u003c/skip\u003e\n\n### Build with Maven\n\nUse the Maven wrapper to create a jar including all dependencies\n\n    mvnw clean install\n\n### Checkstyle Config File\n\nThis project uses my [`common-parent`](https://github.com/patrickfav/mvn-common-parent) which centralized a lot of\nthe plugin versions as well as providing the checkstyle config rules. Specifically they are maintained in [`checkstyle-config`](https://github.com/patrickfav/checkstyle-config). Locally the files will be copied after you `mvnw install` into your `target` folder and is called\n`target/checkstyle-checker.xml`. So if you use a plugin for your IDE, use this file as your local configuration.\n\n## Tech-Stack\n\n* Java 7 (+ [errorprone](https://github.com/google/error-prone) static analyzer)\n* Maven\n\n## Further Reading\n\n### Main Article\n\n* [A Better Way to Protect Your IDs](https://medium.com/@patrickfav/a-better-way-to-protect-your-database-ids-a33fa9867552)\n\n### Discussions\n\n* [Exposing database IDs - security risk?](https://stackoverflow.com/questions/396164/exposing-database-ids-security-risk)\n* [Prevent Business Intelligence Leaks by Using UUIDs Instead of Database IDs on URLs and in APIs](https://medium.com/lightrail/prevent-business-intelligence-leaks-by-using-uuids-instead-of-database-ids-on-urls-and-in-apis-17f15669fd2e)\n* [Why not expose a primary key](https://softwareengineering.stackexchange.com/questions/218306/why-not-expose-a-primary-key)\n* [Sharding \u0026 IDs at Instagram](https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c)\n* [HashId Cryptanalysis](https://carnage.github.io/2015/08/cryptanalysis-of-hashids)\n* [Discussion about IDMask encryption schema](https://crypto.stackexchange.com/q/68415/44838)\n\n### Similar Libraries\n\n* [HashIds](https://github.com/10cella/hashids-java)\n* [NanoId](https://github.com/ai/nanoid)\n\n### Credits\n\n* [Icon made by mynamepong (CC 3.0 BY)](https://www.flaticon.com/authors/mynamepong)\n\n# License\n\nCopyright 2019 Patrick Favre-Bulle\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n","funding_links":[],"categories":["安全"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickfav%2Fid-mask","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatrickfav%2Fid-mask","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickfav%2Fid-mask/lists"}