com.authlete.sd.SDObjectEncoder Maven / Gradle / Ivy
Show all versions of sd-jwt Show documentation
/*
* Copyright (C) 2023 Authlete, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.authlete.sd;
import static com.authlete.sd.SDConstants.DEFAULT_HASH_ALGORITHM;
import static com.authlete.sd.SDConstants.RETAINED_CLAIMS;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
/**
* A utility to make elements in a map or a list selectively-disclosable
* recursively.
*
*
* Decoy digests are automatically added unless decoy magnification ratio
* is set to 0.0 through constructors or the {@link #setDecoyMagnification(double, double)}
* method.
*
*
*
* Some claims such as "{@code iss}" and "{@code iat}" are retained without
* being made selectively-disclosable. See the description of the {@link
* #getRetainedClaims()} method for details.
*
*
*
*
* // Original dataset
* //
* // {
* // "key-1": "value-1",
* // "key-2": [
* // "element-1",
* // "element-2"
* // ],
* // "key-3": {
* // "sub-key-1": "sub-value-1",
* // "sub-key-2": "sub-value-2",
* // },
* // }
* //
*
* List<String> sublist = List.of(
* "element-1",
* "element-2"
* );
*
* Map<String, String> submap = Map.of(
* "sub-key-1", "sub-value-1",
* "sub-key-2", "sub-value-2"
* );
*
* Map<String, Object> originalMap = Map.of(
* "key-1", "value-1",
* "key-2", sublist,
* "key-3", submap
* );
*
* // Encoder
* SDObjectEncoder encoder = new SDObjectEncoder();
*
* // Encode
* Map<String, Object> encodedMap = encoder.encode(originalMap);
*
* // Disclosures yielded as a result of the encoding process.
* List<Disclosure> disclosures = encoder.getDisclosures();
*
* // Encoded dataset
* //
* // {
* // "key-2": [
* // { "...": "Z_JV6E3FColTFBqUrvfa366V27BFy8cf8fa59NQdavg" },
* // { "...": "p-vA6nPBgbL-Zgzd5MkVV7RrFPvMCV_f0N9p3CKOLVo" },
* // { "...": "NVpowlkRQq9aC8aJAS3tz7Gzs3PolUJ7bZLYZiUg5pw" },
* // { "...": "TfdBAy9CRDAhoyB2O3tcGUWOKnfSzQ1wKDTwJQyuFVU" },
* // { "...": "Ujg9QqkNQ0tKN_DiPoCQOmHAWGThokrjA5ceve6Xxik" }
* // ],
* // "key-3": {
* // "_sd": [
* // "JVWbh08VUtBXLWOH16OgPMFZu7qGmKIc7Gt0dxwJin0",
* // "YJ_T7R1qZsfdDIKoHFQ1ubOToI-DHZHBvZwBU6S1svE",
* // "Z14X_kICU8SpDGpfDQ2mP1LfWAMtdPPRPJ_434cdKe4",
* // "r67vz8Rq22eoCw_D-xDVa1bRucngVuRAExSQvWdbrXo"
* // ]
* // },
* // "_sd": [
* // "--3D5V1QiCzfs7gt4hxlaiFh02bBcUKH6VKCxAcPuGk",
* // "ON2rSnqtfmLcJTrKKP5_l6swD3AkMbcmjb80hge2eMs",
* // "wf2OtpIlIqG58GfXN6-jiDX-k1Wt4eJX-nPWbTdfonM"
* // ],
* // "_sd_alg": "sha-256"
* // }
* //
* // Disclosures
* //
* // | digest | claim name | claim value |
* // |---------------------------------------------|------------|--------------|
* // | ON2rSnqtfmLcJTrKKP5_l6swD3AkMbcmjb80hge2eMs | key-1 | value-1 |
* // | NVpowlkRQq9aC8aJAS3tz7Gzs3PolUJ7bZLYZiUg5pw | (null) | element-1 |
* // | TfdBAy9CRDAhoyB2O3tcGUWOKnfSzQ1wKDTwJQyuFVU | (null) | element-2 |
* // | r67vz8Rq22eoCw_D-xDVa1bRucngVuRAExSQvWdbrXo | sub-key-1 | sub-value-1 |
* // | Z14X_kICU8SpDGpfDQ2mP1LfWAMtdPPRPJ_434cdKe4 | sub-key-2 | sub-value-2 |
* //
*
*
*
* @since 1.3
*/
public class SDObjectEncoder
{
private static final double DECOY_MAGNIFICATION_MIN_LIMIT = 0.0;
private static final double DECOY_MAGNIFICATION_MAX_LIMIT = 10.0;
private static final double DECOY_MAGNIFICATION_MIN_DEFAULT = 0.5;
private static final double DECOY_MAGNIFICATION_MAX_DEFAULT = 1.5;
private final Random random = new SecureRandom();
private String hashAlgorithm;
private double decoyMagnificationMin;
private double decoyMagnificationMax;
private boolean hashAlgorithmIncluded;
private final Set retainedClaims;
private List disclosures;
/**
* The default constructor with the default hash algorithm
* ("{@code sha-256}") and the default decoy magnification ratio
* (min = 0.5, max = 1.5).
*/
public SDObjectEncoder()
{
this(DEFAULT_HASH_ALGORITHM, DECOY_MAGNIFICATION_MIN_DEFAULT, DECOY_MAGNIFICATION_MAX_DEFAULT);
}
/**
* A constructor with the specified hash algorithm and the default decoy
* magnification ratio (min = 0.5, max = 1.5).
*
* @param hashAlgorithm
* The hash algorithm for digests. If {@code null} is given, the
* default hash algorithm ("{@code sha-256}") is used.
*/
public SDObjectEncoder(String hashAlgorithm)
{
this(hashAlgorithm, DECOY_MAGNIFICATION_MIN_DEFAULT, DECOY_MAGNIFICATION_MAX_DEFAULT);
}
/**
* A constructor with the default hash algorithm ("{@code sha-256}") and
* the specified decoy magnification ratio.
*
*
* The pair of the decoy magnification arguments specifies the range of decoy
* magnification ratio. The actual ratio is determined randomly between the
* range for each JSON object and JSON array. The number of inserted decoys is
* computed by multiplying the ratio to the size of the original JSON object
* or the length of the original JSON array.
*
*
*
* If 0.0 is set to both the decoy magnification arguments, no decoy is inserted.
*
*
*
* // Create an encoder that yields no decoy digests.
* SDObjectEncoder encoder = new SDObjectEncoder(0.0, 0.0);
*
*
* @param decoyMagnificationMin
* The minimum decoy magnification ratio. If a negative value is
* given, 0.0 is used instead. If a value greater than 10.0 is
* given, 10.0 is used instead.
*
* @param decoyMagnificationMax
* The maximum decoy magnification ratio. If a negative value is
* given, 0.0 is used instead. If a value greater than 10.0 is
* given, 10.0 is used instead.
*/
public SDObjectEncoder(double decoyMagnificationMin, double decoyMagnificationMax)
{
this(DEFAULT_HASH_ALGORITHM, decoyMagnificationMin, decoyMagnificationMax);
}
/**
* A constructor with the specified hash algorithm and decoy magnification ratio.
*
*
* The pair of the decoy magnification arguments specifies the range of decoy
* magnification ratio. The actual ratio is determined randomly between the
* range for each JSON object and JSON array. The number of inserted decoys is
* computed by multiplying the ratio to the size of the original JSON object
* or the length of the original JSON array.
*
*
*
* If 0.0 is set to both the decoy magnification arguments, no decoy is inserted.
*
*
*
* // Create an encoder that yields no decoy digests.
* SDObjectEncoder encoder = new SDObjectEncoder(null, 0.0, 0.0);
*
*
* @param hashAlgorithm
* The hash algorithm for digests. If {@code null} is given, the
* default hash algorithm ("{@code sha-256}") is used.
*
* @param decoyMagnificationMin
* The minimum decoy magnification ratio. If a negative value is
* given, 0.0 is used instead. If a value greater than 10.0 is
* given, 10.0 is used instead.
*
* @param decoyMagnificationMax
* The maximum decoy magnification ratio. If a negative value is
* given, 0.0 is used instead. If a value greater than 10.0 is
* given, 10.0 is used instead.
*/
public SDObjectEncoder(String hashAlgorithm, double decoyMagnificationMin, double decoyMagnificationMax)
{
if (decoyMagnificationMin > decoyMagnificationMax)
{
throw new IllegalArgumentException("decoyMagnificationMin > decoyMagnificationMax");
}
this.hashAlgorithm = normalizeHashAlgorithm(hashAlgorithm);
this.decoyMagnificationMin = normalizeDecoyMagnification(decoyMagnificationMin);
this.decoyMagnificationMax = normalizeDecoyMagnification(decoyMagnificationMax);
this.hashAlgorithmIncluded = true;
this.retainedClaims = new TreeSet<>(RETAINED_CLAIMS);
}
private static String normalizeHashAlgorithm(String hashAlgorithm)
{
return (hashAlgorithm != null) ? hashAlgorithm : DEFAULT_HASH_ALGORITHM;
}
private static double normalizeDecoyMagnification(double magnification)
{
return between(DECOY_MAGNIFICATION_MIN_LIMIT, magnification,
DECOY_MAGNIFICATION_MAX_LIMIT);
}
private static double between(double min, double value, double max)
{
return Math.max(min, Math.min(value, max));
}
/**
* Get the hash algorithm for digests.
*
* @return
* The hash algorithm.
*/
public String getHashAlgorithm()
{
return hashAlgorithm;
}
/**
* Set the hash algorithm for digests.
*
* @param hashAlgorithm
* The hash algorithm. If {@code null} is given, the default hash
* algorithm ("{@code sha-256}") is used.
*
* @return
* {@code this} object.
*/
public SDObjectEncoder setHashAlgorithm(String hashAlgorithm)
{
this.hashAlgorithm = normalizeHashAlgorithm(hashAlgorithm);
return this;
}
/**
* Set the decoy magnification ratio.
*
*
* The pair of the arguments specifies the range of decoy magnification
* ratio. The actual ratio is determined randomly between the range for
* each JSON object and SON array. The number of inserted decoys is
* computed by multiplying the ratio to the size of the original JSON
* object or the length of the original JSON array.
*
*
*
* If 0.0 is set to both the arguments, no decoy is inserted.
*
*
*
* // Yield no decoy digests.
* encoder.{@link #setDecoyMagnification(double, double) setDecoyMagnification}(0.0, 0.0);
*
*
* @param min
* The minimum decoy magnification ratio. If a negative value is
* given, 0.0 is used instead. If a value greater than 10.0 is
* given, 10.0 is used instead.
*
* @param max
* The maximum decoy magnification ratio. If a negative value is
* given, 0.0 is used instead. If a value greater than 10.0 is
* given, 10.0 is used instead.
*
* @return
* {@code this} object.
*/
public SDObjectEncoder setDecoyMagnification(double min, double max)
{
if (min > max)
{
throw new IllegalArgumentException("min > max");
}
this.decoyMagnificationMin = normalizeDecoyMagnification(min);
this.decoyMagnificationMax = normalizeDecoyMagnification(max);
return this;
}
/**
* Get the flag indicating whether the "{@code _sd_alg}" key (that denotes
* the hash algorithm for digests) will be included in the encoded map.
*
* @return
* {@code true} if the "{@code _sd_alg}" key will be included in
* the encoded map.
*/
public boolean isHashAlgorithmIncluded()
{
return hashAlgorithmIncluded;
}
/**
* Set the flag indicating whether the "{@code _sd_alg}" key (that denotes
* the hash algorithm for digests) will be included in the encoded map.
*
* @param included
* {@code true} to include the "{@code _sd_alg}" key in the encoded
* map. {@code false} not to include the key.
*
* @return
* {@code this} object.
*/
public SDObjectEncoder setHashAlgorithmIncluded(boolean included)
{
this.hashAlgorithmIncluded = included;
return this;
}
/**
* Get the set of claims that are retained without being made
* selectively-disclosable when they appear in the top-level map.
*
*
* By default, the following claims are registered as ones to retain.
*
*
*
* - {@code iss}
*
- {@code iat}
*
- {@code nbf}
*
- {@code exp}
*
- {@code cnf}
*
- {@code vct}
*
- {@code status}
*
*
*
* By modifying the {@code Set} object returned from this method, the
* behavior of this encoder can be changed. For instance, the example
* below makes the encoder retain the "{@code sub}" claim.
*
*
*
* encoder.{@link #getRetainedClaims()}.add("sub");
*
*
* @return
* The set of claims to retain.
*/
public Set getRetainedClaims()
{
return retainedClaims;
}
/**
* Get the list of {@link Disclosure}s yielded as a result of the encoding
* process.
*
*
* On every call of either the {@link #encode(Map)} method or the
* {@link #encode(List)} method, the disclosure list is reset. The "reset"
* here means that a new {@code List} instance is created and assigned,
* and the previous one (if any) is detached.
*
*
* @return
* The list of {@link Disclosure}s.
*/
public List getDisclosures()
{
return disclosures;
}
/**
* Encode the content of the given map.
*
*
* On the entry of this method, the disclosure list returned from the
* {@link #getDisclosures()} method is reset. The "reset" here means that
* a new {@code List} instance is created and assigned, and the previous
* one (if any) is detached.
*
*
*
* Some claims such as "{@code iss}" and "{@code iat}" are retained without
* being made selectively-disclosable when they appear in the top-level map.
* See the description of the {@link #getRetainedClaims()} method for details.
*
*
*
* The encoded map will contain the "{@code _sd_alg}" key that denotes the
* hash algorithm for digests. If the key should not be included, call
* {@link #setHashAlgorithmIncluded(boolean) setHashAlgorithmIncluded}{@code
* (false)} before calling this method.
*
*
* @param input
* The input map. If {@code null} is given, {@code null} is returned.
*
* @return
* The encoded map.
*/
public Map encode(Map input)
{
reset();
if (input == null)
{
return null;
}
// Encode the given map.
return encodeMap(input, /* top */ true);
}
/**
* Encode the content of the given list.
*
*
* On the entry of this method, the disclosure list returned from the
* {@link #getDisclosures()} method is reset. The "reset" here means that
* a new {@code List} instance is created and assigned, and the previous
* one (if any) is detached.
*
*
* @param input
* The input list. If {@code null} is given, {@code null} is returned.
*
* @return
* The encoded list.
*/
public List