From 5550fa28a7578ce6dbc50d1f94bb4740c1f3ec32 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 24 Jan 2022 12:45:23 +0100 Subject: [PATCH] Stabilise configuration and documentation of UUID factory. --- .../api/uuid/AbstractAsyncUuidFactory.java | 9 +- .../argeo/api/uuid/AbstractUuidFactory.java | 41 ++------- .../argeo/api/uuid/ConcurrentUuidFactory.java | 86 +++++++++++++++--- .../argeo/api/uuid/MacAddressUuidFactory.java | 41 +++++++-- .../src/org/argeo/api/uuid/TimeUuidState.java | 19 +++- .../src/org/argeo/api/uuid/UuidFactory.java | 87 ++++++++++++++++--- 6 files changed, 207 insertions(+), 76 deletions(-) diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractAsyncUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractAsyncUuidFactory.java index f57462388..2b4c27f3f 100644 --- a/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractAsyncUuidFactory.java +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractAsyncUuidFactory.java @@ -64,14 +64,11 @@ public abstract class AbstractAsyncUuidFactory extends AbstractUuidFactory imple if (nodeIdSupplier == null) throw new IllegalStateException("No node id supplier available"); UUID uuid = new UUID(timeUuidState.getMostSignificantBits(), timeUuidState.getLeastSignificantBits()); - long clockSequence = timeUuidState.getClockSequence(); - long timestamp = timeUuidState.getLastTimestamp(); - // assert uuid.node() == longFromBytes(node); - assert uuid.timestamp() == timestamp; - assert uuid.clockSequence() == clockSequence - : "uuid.clockSequence()=" + uuid.clockSequence() + " clockSequence=" + clockSequence; + assert uuid.version() == 1; assert uuid.variant() == 2; + assert uuid.timestamp() == timeUuidState.getLastTimestamp(); + assert uuid.clockSequence() == timeUuidState.getClockSequence(); return uuid; } diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractUuidFactory.java index 9d829987b..d348504ad 100644 --- a/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractUuidFactory.java +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractUuidFactory.java @@ -1,13 +1,10 @@ package org.argeo.api.uuid; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.net.UnknownHostException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.time.temporal.Temporal; +import java.util.BitSet; import java.util.Objects; import java.util.Random; import java.util.UUID; @@ -48,17 +45,15 @@ public abstract class AbstractUuidFactory implements UuidFactory { UUID uuid = new UUID(mostSig, leastSig); // tests -// assert uuid.node() == BitSet.valueOf(node).toLongArray()[0]; - // assert uuid.node() == longFromBytes(node); - assert uuid.timestamp() == timestamp; - assert uuid.clockSequence() == clockSequence - : "uuid.clockSequence()=" + uuid.clockSequence() + " clockSequence=" + clockSequence; assert uuid.version() == 1; assert uuid.variant() == 2; + assert uuid.node() == BitSet.valueOf(node).toLongArray()[0]; + assert uuid.timestamp() == timestamp; + assert uuid.clockSequence() == clockSequence; return uuid; } - public UUID timeUUID(Temporal time, long clockSequence, byte[] node, int offset) { + protected UUID timeUUID(Temporal time, long clockSequence, byte[] node, int offset) { // TODO add checks Duration duration = Duration.between(TimeUuidState.GREGORIAN_START, time); // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000 @@ -66,21 +61,6 @@ public abstract class AbstractUuidFactory implements UuidFactory { return newTimeUUID(timestamp, clockSequence, node, offset); } - protected byte[] getHardwareAddress() { - InetAddress localHost; - try { - localHost = InetAddress.getLocalHost(); - try { - NetworkInterface nic = NetworkInterface.getByInetAddress(localHost); - return nic.getHardwareAddress(); - } catch (SocketException e) { - return null; - } - } catch (UnknownHostException e) { - return null; - } - - } /* * NAME BASED (version 3 and 5) */ @@ -282,17 +262,6 @@ public abstract class AbstractUuidFactory implements UuidFactory { return nodeId; } -// protected long toNodeIdBase(byte[] node) { -// assert node.length == 6; -// return LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID -// | (node[0] & 0xFFL) // -// | ((node[1] & 0xFFL) << 8) // -// | ((node[2] & 0xFFL) << 16) // -// | ((node[3] & 0xFFL) << 24) // -// | ((node[4] & 0xFFL) << 32) // -// | ((node[5] & 0xFFL) << 40); // -// } - /* * STATIC UTILITIES */ diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/ConcurrentUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/ConcurrentUuidFactory.java index 05b2f05c6..4ad2290b9 100644 --- a/org.argeo.api.uuid/src/org/argeo/api/uuid/ConcurrentUuidFactory.java +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/ConcurrentUuidFactory.java @@ -1,12 +1,21 @@ package org.argeo.api.uuid; import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.INFO; import static java.lang.System.Logger.Level.WARNING; import java.lang.System.Logger; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; import java.security.DrbgParameters; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.BitSet; +import java.util.Enumeration; import java.util.Objects; /** @@ -18,8 +27,6 @@ import java.util.Objects; public class ConcurrentUuidFactory extends AbstractAsyncUuidFactory { private final static Logger logger = System.getLogger(ConcurrentUuidFactory.class.getName()); -// private byte[] defaultNodeId; - private Long nodeIdBase; public ConcurrentUuidFactory(byte[] nodeId, int offset) { @@ -29,10 +36,14 @@ public class ConcurrentUuidFactory extends AbstractAsyncUuidFactory { byte[] defaultNodeId = toNodeIdBytes(nodeId, offset); nodeIdBase = NodeIdSupplier.toNodeIdBase(defaultNodeId); setNodeIdSupplier(() -> nodeIdBase); + assert newTimeUUID().node() == BitSet.valueOf(defaultNodeId).toLongArray()[0]; } - protected ConcurrentUuidFactory() { - + public ConcurrentUuidFactory() { + byte[] defaultNodeId = getIpBytes(); + nodeIdBase = NodeIdSupplier.toNodeIdBase(defaultNodeId); + setNodeIdSupplier(() -> nodeIdBase); + assert newTimeUUID().node() == BitSet.valueOf(defaultNodeId).toLongArray()[0]; } @Override @@ -53,12 +64,61 @@ public class ConcurrentUuidFactory extends AbstractAsyncUuidFactory { return secureRandom; } - /* - * TIME-BASED (version 1) - */ -// -// @Override -// public UUID newTimeUUID() { -// return newTimeUUID(timeUuidState.useTimestamp(), timeUuidState.getClockSequence(), defaultNodeId, 0); -// } -} + /** Returns an SHA1 digest of one of the IP addresses. */ + protected byte[] getIpBytes() { + Enumeration netInterfaces = null; + try { + netInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + throw new IllegalStateException(e); + } + if (netInterfaces == null) + throw new IllegalStateException("No interfaces"); + + InetAddress selectedIpv6 = null; + InetAddress selectedIpv4 = null; + netInterfaces: while (netInterfaces.hasMoreElements()) { + NetworkInterface netInterface = netInterfaces.nextElement(); + byte[] hardwareAddress = null; + try { + hardwareAddress = netInterface.getHardwareAddress(); + if (hardwareAddress != null) { + // first IPv6 + addr: for (InterfaceAddress addr : netInterface.getInterfaceAddresses()) { + InetAddress ip = addr.getAddress(); + if (ip instanceof Inet6Address) { + Inet6Address ipv6 = (Inet6Address) ip; + if (ipv6.isAnyLocalAddress() || ipv6.isLinkLocalAddress() || ipv6.isLoopbackAddress()) + continue addr; + selectedIpv6 = ipv6; + break netInterfaces; + } + + } + // then IPv4 + addr: for (InterfaceAddress addr : netInterface.getInterfaceAddresses()) { + InetAddress ip = addr.getAddress(); + if (ip instanceof Inet4Address) { + Inet4Address ipv4 = (Inet4Address) ip; + if (ipv4.isAnyLocalAddress() || ipv4.isLinkLocalAddress() || ipv4.isLoopbackAddress()) + continue addr; + selectedIpv4 = ipv4; + } + + } + } + } catch (SocketException e) { + throw new IllegalStateException(e); + } + } + InetAddress selectedIp = selectedIpv6 != null ? selectedIpv6 : selectedIpv4; + if (selectedIp == null) + throw new IllegalStateException("No IP address found"); + byte[] digest = sha1(selectedIp.getAddress()); + logger.log(INFO, "Use IP " + selectedIp + " hashed as " + toHexString(digest) + " as node id"); + byte[] nodeId = toNodeIdBytes(digest, 0); + // marks that this is not based on MAC address + forceToNoMacAddress(nodeId, 0); + return nodeId; + } +} \ No newline at end of file diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java index d1ce6ded8..39c919fa6 100644 --- a/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java @@ -1,15 +1,44 @@ package org.argeo.api.uuid; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.UUID; + +/** + * An {@link UUID} factory whose node id (for time based UUIDs) is the hardware + * MAC address as specified in RFC4122. + * + * @see https://datatracker.ietf.org/doc/html/rfc4122.html#section-4.1.6 + */ public class MacAddressUuidFactory extends ConcurrentUuidFactory { public final static UuidFactory DEFAULT = new MacAddressUuidFactory(); public MacAddressUuidFactory() { - setNodeIdSupplier(() -> { - byte[] hardwareAddress = getHardwareAddress(); - byte[] macAddressNodeId = toNodeIdBytes(hardwareAddress, 0); - long nodeIdBase = NodeIdSupplier.toNodeIdBase(macAddressNodeId); - return nodeIdBase; - }); + super(localHardwareAddressAsNodeId(), 0); + } + + public static byte[] localHardwareAddressAsNodeId() { + InetAddress localHost; + try { + localHost = InetAddress.getLocalHost(); + NetworkInterface nic = NetworkInterface.getByInetAddress(localHost); + return hardwareAddressToNodeId(nic); + } catch (UnknownHostException | SocketException e) { + throw new IllegalStateException(e); + } + + } + + public static byte[] hardwareAddressToNodeId(NetworkInterface nic) throws SocketException { + byte[] hardwareAddress = nic.getHardwareAddress(); + final int length = 6; + byte[] arr = new byte[length]; + for (int i = 0; i < length; i++) { + arr[i] = hardwareAddress[length - 1 - i]; + } + return arr; } } diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuidState.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuidState.java index 780e8ad33..9c8b6dd69 100644 --- a/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuidState.java +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuidState.java @@ -3,6 +3,7 @@ package org.argeo.api.uuid; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.UUID; /** * The state of a time based UUID generator, as described and discussed in @@ -17,14 +18,24 @@ public interface TimeUuidState { /** Start of the Gregorian time, used by time-based UUID (v1). */ final static Instant GREGORIAN_START = ZonedDateTime.of(1582, 10, 15, 0, 0, 0, 0, ZoneOffset.UTC).toInstant(); - long getLastTimestamp(); - - long getClockSequence(); - + /** Current node id and clock sequence for this thread. */ long getLeastSignificantBits(); + /** A new current timestamp for this thread. */ long getMostSignificantBits(); + /** + * The last timestamp which was produced by this thread, as returned by + * {@link UUID#timestamp()}. + */ + long getLastTimestamp(); + + /** + * The current clock sequence for this thread, as returned by + * {@link UUID#clockSequence()}. + */ + long getClockSequence(); + static boolean isNoMacAddressNodeId(byte[] nodeId) { return (nodeId[0] & 1) != 0; } diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidFactory.java index 78bb402cf..91191dae2 100644 --- a/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidFactory.java +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidFactory.java @@ -2,6 +2,7 @@ package org.argeo.api.uuid; import static java.nio.charset.StandardCharsets.UTF_8; +import java.nio.charset.Charset; import java.util.Objects; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; @@ -19,29 +20,86 @@ public interface UuidFactory extends Supplier { /* * TIME-BASED (version 1) */ + /** + * A new time based {@link UUID} (v1) with efforts to make it unique on this + * node. + */ UUID timeUUID(); /* * NAME BASED (version 3 and 5) */ + /** + * A new {@link UUID} v5, which an SHA1 digest of namespace and the provided + * bytes. This use to build names and implementation MAY restrict the maximal + * size of the byte array. + * + * @see UuidFactory#NAMESPACE_UUID_DNS + * @see UuidFactory#NAMESPACE_UUID_URL + * @see UuidFactory#NAMESPACE_UUID_OID + * @see UuidFactory#NAMESPACE_UUID_X500 + */ UUID nameUUIDv5(UUID namespace, byte[] data); + /** + * A new {@link UUID} v3, which a MD5 digest of namespace and the provided + * bytes. This use to build names and implementation MAY restrict the maximal + * size of the byte array. + * + * @see UuidFactory#NAMESPACE_UUID_DNS + * @see UuidFactory#NAMESPACE_UUID_URL + * @see UuidFactory#NAMESPACE_UUID_OID + * @see UuidFactory#NAMESPACE_UUID_X500 + */ UUID nameUUIDv3(UUID namespace, byte[] data); + /** + * A convenience method to generate a name based UUID v5 based on a string, + * using the UTF-8 charset. + * + * @see UuidFactory#nameUUIDv5(UUID, byte[]) + */ default UUID nameUUIDv5(UUID namespace, String name) { + return nameUUIDv5(namespace, name, UTF_8); + } + + /** + * A convenience method to generate a name based UUID v5 based on a string. + * + * @see UuidFactory#nameUUIDv5(UUID, byte[]) + */ + default UUID nameUUIDv5(UUID namespace, String name, Charset charset) { Objects.requireNonNull(name, "Name cannot be null"); - return nameUUIDv5(namespace, name.getBytes(UTF_8)); + return nameUUIDv5(namespace, name.getBytes(charset)); } + /** + * A convenience method to generate a name based UUID v3 based on a string, + * using the UTF-8 charset. + * + * @see UuidFactory#nameUUIDv3(UUID, byte[]) + */ default UUID nameUUIDv3(UUID namespace, String name) { + return nameUUIDv3(namespace, name, UTF_8); + } + + /** + * A convenience method to generate a name based UUID v3 based on a string. + * + * @see UuidFactory#nameUUIDv3(UUID, byte[]) + */ + default UUID nameUUIDv3(UUID namespace, String name, Charset charset) { Objects.requireNonNull(name, "Name cannot be null"); - return nameUUIDv3(namespace, name.getBytes(UTF_8)); + return nameUUIDv3(namespace, name.getBytes(charset)); } /* * RANDOM (version 4) */ - /** A random UUID at least as good as {@link UUID#randomUUID()}. */ + /** + * A random UUID at least as good as {@link UUID#randomUUID()}, but with efforts + * to make it even more random, using more secure algorithms and resseeding. + */ UUID randomUUIDStrong(); /** @@ -52,7 +110,11 @@ public interface UuidFactory extends Supplier { /** * The default random {@link UUID} (v4) generator to use. This default - * implementation returns {@link #randomUUIDStrong()}. + * implementation returns {@link #randomUUIDStrong()}. In general, one should + * use {@link UUID#randomUUID()} to generate random UUID, as it is certainly the + * best balanced and to avoid unnecessary dependencies with an API. The + * implementations provided here are either when is looking for something + * "stronger" ({@link #randomUUIDStrong()} or faster {@link #randomUUIDWeak()}. */ default UUID randomUUID() { return randomUUIDStrong(); @@ -60,11 +122,12 @@ public interface UuidFactory extends Supplier { /** * The default {@link UUID} to provide, either random (v4) or time based (v1). - * This default implementations returns {@link #randomUUID()}. + * This default implementations returns {@link #timeUUID()} because it is + * supposed to be fast and use few resources. */ @Override default UUID get() { - return randomUUID(); + return timeUUID(); } /* @@ -77,31 +140,32 @@ public interface UuidFactory extends Supplier { * Standard DNS namespace ID for type 3 or 5 UUID (as defined in Appendix C of * RFC4122). */ - final static UUID NS_DNS = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); + final static UUID NAMESPACE_UUID_DNS = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); /** * Standard URL namespace ID for type 3 or 5 UUID (as defined in Appendix C of * RFC4122). */ - final static UUID NS_URL = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + final static UUID NAMESPACE_UUID_URL = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); /** * Standard OID namespace ID (typically an LDAP type) for type 3 or 5 UUID (as * defined in Appendix C of RFC4122). */ - final static UUID NS_OID = UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8"); + final static UUID NAMESPACE_UUID_OID = UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8"); /** * Standard X500 namespace ID (typically an LDAP DN) for type 3 or 5 UUID (as * defined in Appendix C of RFC4122). */ - final static UUID NS_X500 = UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"); + final static UUID NAMESPACE_UUID_X500 = UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"); /* * UTILITIES */ - + /** Whether this {@link UUID} is random (v4). */ static boolean isRandom(UUID uuid) { return uuid.version() == 4; } + /** Whether this {@link UUID} is time based (v1). */ static boolean isTimeBased(UUID uuid) { return uuid.version() == 1; } @@ -119,6 +183,7 @@ public interface UuidFactory extends Supplier { return false; } + /** Whether this {@link UUID} is name based (v3 or v5). */ static boolean isNameBased(UUID uuid) { return uuid.version() == 3 || uuid.version() == 5; } -- 2.30.2