Cette classe gère les informations de l'appareil, y compris la clé de chiffrement symétrique privée (AES) de l'appareil ("devicesalt"), le numéro de série de l'appareil, l'empreinte numérique de l'appareil et d'autres informations d'identification de l'appareil ("device.xml").
This commit is contained in:
519
src/main/java/common/Device.java
Normal file
519
src/main/java/common/Device.java
Normal file
@@ -0,0 +1,519 @@
|
||||
package common;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Custom exception class for common.Device operations.
|
||||
*/
|
||||
class DeviceException extends GourouException
|
||||
{
|
||||
public static final int DEV_INVALID_DEVICE_FILE = 1;
|
||||
public static final int DEV_INVALID_DEVICE_KEY_FILE = 2;
|
||||
public static final int DEV_INVALID_DEV_PROPERTY = 3;
|
||||
public static final int DEV_MAC_ERROR = 4;
|
||||
public static final int DEV_MKPATH = 5;
|
||||
public static final int DEV_FILE_ERROR = 6;
|
||||
|
||||
private int errorCode;
|
||||
|
||||
public DeviceException( String message )
|
||||
{
|
||||
super( message );
|
||||
this.errorCode = -1; // Default or unknown error code
|
||||
}
|
||||
|
||||
public DeviceException( int errorCode, String message )
|
||||
{
|
||||
this( message );
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public DeviceException( String message, Throwable cause )
|
||||
{
|
||||
super( message, cause );
|
||||
this.errorCode = -1;
|
||||
}
|
||||
|
||||
public DeviceException( int errorCode, String message, Throwable cause )
|
||||
{
|
||||
super( message, cause );
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public int getErrorCode()
|
||||
{
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The core common.Device class, responsible for managing device information,
|
||||
* serial numbers, fingerprints, and associated files.
|
||||
*/
|
||||
public class Device
|
||||
{
|
||||
private static final String HEXES = "0123456789abcdef";
|
||||
// public static final int DEVICE_SERIAL_LEN = CryptoUtils.SHA1_LEN;
|
||||
public static final int DEVICE_KEY_SIZE = 16;
|
||||
|
||||
private Path deviceFile;
|
||||
private Path deviceKeyFile;
|
||||
|
||||
protected byte[] deviceKey = new byte[DEVICE_KEY_SIZE];
|
||||
protected String deviceClass;
|
||||
protected String deviceSerial;
|
||||
protected String deviceName;
|
||||
protected String deviceType = "standalone";
|
||||
protected String fingerprint;
|
||||
|
||||
private Map<String, String> versions = new HashMap<>(); // Stores device properties
|
||||
protected final Logger LOGGER = Logger.getLogger( Device.class.getName() );
|
||||
|
||||
protected Device() {}
|
||||
|
||||
/**
|
||||
* Main Device constructor.
|
||||
*
|
||||
* @param deviceFile Path of device.xml
|
||||
* @param deviceKeyFile Path of devicesalt
|
||||
* @param hobbesVersion Used to create the Device file if it doesn't exist
|
||||
* @param randomSerial If the Device file is being created, use a random serial number instead of a derived one.
|
||||
*/
|
||||
public Device( Path deviceFile, Path deviceKeyFile, String hobbesVersion, boolean randomSerial, Level logLevel )
|
||||
throws NoSuchAlgorithmException //, UnknownHostException
|
||||
{
|
||||
LOGGER.setLevel( logLevel );
|
||||
this.deviceFile = deviceFile;
|
||||
this.deviceKeyFile = deviceKeyFile;
|
||||
try
|
||||
{
|
||||
readDeviceKeyFile();
|
||||
}
|
||||
catch (DeviceException deviceException)
|
||||
{
|
||||
createDeviceKeyFile( deviceKeyFile );
|
||||
readDeviceKeyFile();
|
||||
}
|
||||
try
|
||||
{
|
||||
parseDeviceFile();
|
||||
}
|
||||
catch (DeviceException deviceException)
|
||||
{
|
||||
createDeviceFile( hobbesVersion, randomSerial );
|
||||
parseDeviceFile();
|
||||
}
|
||||
}
|
||||
|
||||
public void setLogLevel( Level logLevel )
|
||||
{
|
||||
LOGGER.setLevel( logLevel );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device serial number.
|
||||
* SHA1(uid ":" username ":" macaddress ":" )
|
||||
*
|
||||
* @param random If true, generates a random serial; otherwise, uses system info.
|
||||
*
|
||||
* @return The generated serial number as a hexadecimal string.
|
||||
*/
|
||||
private String makeSerial( boolean random )
|
||||
throws NoSuchAlgorithmException
|
||||
{
|
||||
byte[] shaOut;
|
||||
|
||||
if (!random)
|
||||
{
|
||||
String username = System.getProperty( "user.name" );
|
||||
String mockUid = // String.valueOf( username.hashCode() ); // Simple mock for uid
|
||||
"1000";
|
||||
byte[] macAddress = getMacAddress();
|
||||
|
||||
String dataToHashStr = String.format( "%s:%s:%02x:%02x:%02x:%02x:%02x:%02x:", mockUid, username,
|
||||
macAddress[0], macAddress[1], macAddress[2], macAddress[3],
|
||||
macAddress[4], macAddress[5] );
|
||||
|
||||
byte[] dataToHash = dataToHashStr.getBytes( StandardCharsets.UTF_8 ); // Use UTF-8 for consistency
|
||||
|
||||
MessageDigest digestor = MessageDigest.getInstance( "SHA1" );
|
||||
digestor.update( dataToHash );
|
||||
shaOut = digestor.digest();
|
||||
}
|
||||
else
|
||||
{
|
||||
shaOut = new byte[ CryptoUtils.SHA1_LEN ]; // 20
|
||||
new SecureRandom().nextBytes( shaOut );
|
||||
}
|
||||
|
||||
final StringBuilder hex = new StringBuilder( 2 * shaOut.length );
|
||||
for (final byte b : shaOut)
|
||||
{
|
||||
hex.append( HEXES.charAt( (b & 0xF0) >> 4 ) ).append( HEXES.charAt( (b & 0x0F) ) );
|
||||
}
|
||||
// SHA1 is 20 bytes. Assume it takes first 10 bytes of SHA1 hash then converts to hex,
|
||||
// just to match the c++ code, and for no other reason.
|
||||
String result = hex.substring( 0, 20 );
|
||||
LOGGER.log( Level.FINE, "Serial : " + result );
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the MAC address of a non-loopback network interface.
|
||||
*
|
||||
* @return The MAC address as a byte array (6 bytes), or a static fallback if not found.
|
||||
*/
|
||||
private byte[] getMacAddress()
|
||||
{
|
||||
try
|
||||
{
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
for (NetworkInterface ni : Collections.list( networkInterfaces ))
|
||||
{
|
||||
// Skip loopback, down interfaces, and interfaces without a physical address
|
||||
if (!ni.isLoopback() && ni.isUp() && ni.getHardwareAddress() != null && ni.getHardwareAddress().length == 6)
|
||||
{
|
||||
return ni.getHardwareAddress();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.log( Level.WARNING, "Error getting MAC address: " + e.getMessage() );
|
||||
// For a robust system, we might want to throw a common.DeviceException here.
|
||||
}
|
||||
LOGGER.log( Level.INFO, "get_mac_address() not found or error, using a static address." );
|
||||
// Fallback MAC address if none found or error
|
||||
return new byte[] {(byte) 0x8D, (byte) 0x70, 0x13, (byte) 0x8D, 0x43, 0x27};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device fingerprint.
|
||||
* base64(SHA1 (serial + privateKey))
|
||||
*
|
||||
* @param serial The device serial number.
|
||||
* @return The generated fingerprint as a Base64 string.
|
||||
*/
|
||||
private String makeFingerprint( String serial )
|
||||
throws NoSuchAlgorithmException
|
||||
{
|
||||
MessageDigest md = MessageDigest.getInstance( "SHA1" );
|
||||
md.update( serial.getBytes( StandardCharsets.UTF_8 ) );
|
||||
md.update( deviceKey );
|
||||
|
||||
String result = Base64.getEncoder().encodeToString( md.digest() );
|
||||
LOGGER.log( Level.FINE, "Fingerprint : " + result );
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the device XML file by writing the class values
|
||||
*
|
||||
* @param hobbes The "hobbes" version string.
|
||||
* @param randomSerial if true generates a random serial number otherwise derives the serial
|
||||
* number from system properties
|
||||
* @throws DeviceException if XML generation or file writing fails.
|
||||
*/
|
||||
void createDeviceFile( String hobbes, boolean randomSerial )
|
||||
throws NoSuchAlgorithmException //, UnknownHostException
|
||||
{
|
||||
deviceSerial = makeSerial( randomSerial ); // other than making a fingerprint, I don't think this is ever used!!
|
||||
if (null == fingerprint || fingerprint.isBlank())
|
||||
{
|
||||
fingerprint = makeFingerprint( deviceSerial );
|
||||
}
|
||||
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
|
||||
docFactory.setNamespaceAware( true ); // Crucial for handling XML namespaces properly
|
||||
DocumentBuilder docBuilder;
|
||||
Document deviceDoc;
|
||||
try
|
||||
{
|
||||
docBuilder = docFactory.newDocumentBuilder();
|
||||
deviceDoc = docBuilder.newDocument();
|
||||
}
|
||||
catch (ParserConfigurationException e)
|
||||
{
|
||||
throw new DeviceException( "Error configuring XML parser", e );
|
||||
}
|
||||
|
||||
Element root = deviceDoc.createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:deviceInfo" );
|
||||
deviceDoc.appendChild( root );
|
||||
|
||||
XmlUtils.appendTextElem( root, "adept:deviceClass", "Desktop" );
|
||||
XmlUtils.appendTextElem( root, "adept:deviceSerial", deviceSerial );
|
||||
try
|
||||
{
|
||||
InetAddress localHost = InetAddress.getLocalHost();
|
||||
|
||||
// Get the hostname from the InetAddress object
|
||||
deviceName = localHost.getHostName();
|
||||
}
|
||||
catch (UnknownHostException ex)
|
||||
{
|
||||
deviceName = "localhost";
|
||||
}
|
||||
XmlUtils.appendTextElem( root, "adept:deviceName", deviceName );
|
||||
XmlUtils.appendTextElem( root, "adept:deviceType", "standalone" );
|
||||
|
||||
// Version: hobbes
|
||||
Element versionHobbes = deviceDoc.createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:version" );
|
||||
if (null == hobbes)
|
||||
hobbes = Activation.HOBBES_DEFAULT_VERSION;
|
||||
versionHobbes.setAttribute( "name", "hobbes" );
|
||||
versionHobbes.setAttribute( "value", hobbes );
|
||||
root.appendChild( versionHobbes );
|
||||
|
||||
// Version: clientOS
|
||||
Element versionClientOS = deviceDoc.createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:version" );
|
||||
versionClientOS.setAttribute( "name", "clientOS" );
|
||||
String os = System.getProperty( "os.name" ) + " " + System.getProperty( "os.version" );
|
||||
versionClientOS.setAttribute( "value", os );
|
||||
root.appendChild( versionClientOS );
|
||||
|
||||
// Version: clientLocale
|
||||
Element versionClientLocale = deviceDoc.createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:version" );
|
||||
versionClientLocale.setAttribute( "name", "clientLocale" );
|
||||
versionClientLocale.setAttribute( "value",
|
||||
Locale.getDefault().toString() ); // C++: setlocale(LC_ALL, NULL) equivalent
|
||||
root.appendChild( versionClientLocale );
|
||||
|
||||
XmlUtils.appendTextElem( root, "adept:fingerprint", fingerprint );
|
||||
|
||||
String xmlContent = XmlUtils.docToString( deviceDoc, false );
|
||||
LOGGER.log( Level.INFO, "Create device file " + deviceFile );
|
||||
try
|
||||
{
|
||||
Files.write( deviceFile, xmlContent.getBytes( StandardCharsets.UTF_8 ) );
|
||||
}
|
||||
catch (IOException ioException)
|
||||
{
|
||||
throw new DeviceException( "Unable to write device file", ioException );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the device key file with random bytes.
|
||||
*
|
||||
* @throws DeviceException if file writing fails.
|
||||
*/
|
||||
protected void createDeviceKeyFile( Path deviceKeyFile )
|
||||
{
|
||||
byte[] key = new byte[DEVICE_KEY_SIZE];
|
||||
LOGGER.log( Level.INFO, "Create device key file " + deviceKeyFile );
|
||||
new SecureRandom().nextBytes( key );
|
||||
try
|
||||
{
|
||||
Files.write( deviceKeyFile, key );
|
||||
}
|
||||
catch (IOException ioException)
|
||||
{
|
||||
throw new DeviceException( "Unable to write device key file", ioException );
|
||||
}
|
||||
}
|
||||
|
||||
/* *
|
||||
* Static method to create and manage a common.Device instance, handling file existence.
|
||||
*
|
||||
* @param dirName Directory where to put files (.adept)
|
||||
* @param hobbesVersion Hobbes (client version) to set
|
||||
* @param randomSerial Create a random serial (new device each time) or not (serial computed from machine specs)
|
||||
* @return A new or existing common.Device instance.
|
||||
* @throws DeviceException if directory creation or file operations fail.
|
||||
* @ param processor Instance of DRMProcessor
|
||||
*/
|
||||
// public static Device createDevice( String dirName, String hobbesVersion, boolean randomSerial )
|
||||
// throws NoSuchAlgorithmException, UnknownHostException
|
||||
// {
|
||||
// // TODO: this can probably be moved to the device constructor, eliminating the need for
|
||||
// // a factory method.
|
||||
// Path dirPath = Paths.get( dirName );
|
||||
// if (!Files.exists( dirPath ))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// Files.createDirectories( dirPath ); // mkdir_p equivalent
|
||||
// }
|
||||
// catch (IOException e)
|
||||
// {
|
||||
// throw new DeviceException( DeviceException.DEV_MKPATH, "Unable to create directory: " + dirName, e );
|
||||
// }
|
||||
// }
|
||||
// return new Device( dirPath.resolve( "device.xml" ),
|
||||
// dirPath.resolve( "devicesalt" ),
|
||||
// hobbesVersion, randomSerial );
|
||||
// }
|
||||
|
||||
/**
|
||||
* Returns the device key.
|
||||
*
|
||||
* @return A copy of the device key byte array.
|
||||
*/
|
||||
public byte[] getDeviceKey()
|
||||
{
|
||||
return Arrays.copyOf( deviceKey, deviceKey.length );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the device XML file and populates the properties map.
|
||||
*
|
||||
* @throws DeviceException if file loading or XML parsing fails.
|
||||
*/
|
||||
private void parseDeviceFile()
|
||||
throws DeviceException
|
||||
{
|
||||
File file = deviceFile.toFile();
|
||||
if (!file.exists())
|
||||
{
|
||||
throw new DeviceException( DeviceException.DEV_INVALID_DEVICE_FILE,
|
||||
"common.Device file does not exist: " + deviceFile );
|
||||
}
|
||||
|
||||
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
|
||||
// Enable namespace awareness to correctly handle "adept:" prefix in XPath
|
||||
docFactory.setNamespaceAware( true );
|
||||
DocumentBuilder docBuilder;
|
||||
Document doc;
|
||||
try
|
||||
{
|
||||
docBuilder = docFactory.newDocumentBuilder();
|
||||
doc = docBuilder.parse( file );
|
||||
doc.getDocumentElement().normalize(); // Recommended for consistent parsing
|
||||
}
|
||||
catch (ParserConfigurationException | IOException | org.xml.sax.SAXException e)
|
||||
{
|
||||
throw new DeviceException( DeviceException.DEV_INVALID_DEVICE_FILE, "Invalid device file: " + deviceFile, e );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Element deviceInfo = doc.getDocumentElement();
|
||||
deviceClass = deviceInfo.getElementsByTagName( "adept:deviceClass" ).item( 0 ).getTextContent();
|
||||
deviceSerial = deviceInfo.getElementsByTagName( "adept:deviceSerial" ).item( 0 ).getTextContent();
|
||||
deviceName = deviceInfo.getElementsByTagName( "adept:deviceName" ).item( 0 ).getTextContent();
|
||||
deviceType = deviceInfo.getElementsByTagName( "adept:deviceType" ).item( 0 ).getTextContent();
|
||||
fingerprint = deviceInfo.getElementsByTagName( "adept:fingerprint" ).item( 0 ).getTextContent();
|
||||
|
||||
NodeList elements = deviceInfo.getElementsByTagName( "adept:version" );
|
||||
for (int i = 0; i < elements.getLength(); i++)
|
||||
{
|
||||
Node node = elements.item( i );
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE)
|
||||
{
|
||||
Element element = (Element) node;
|
||||
String name = element.getAttribute( "name" );
|
||||
String value = element.getAttribute( "value" );
|
||||
if (!name.isEmpty())
|
||||
{
|
||||
versions.put( name, value );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (DeviceException e)
|
||||
{
|
||||
// Catching specific common.DeviceException from XmlUtils for missing mandatory elements
|
||||
throw new DeviceException( DeviceException.DEV_INVALID_DEVICE_FILE,
|
||||
"Invalid device file format: " + deviceFile, e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* reads the device key file (16 bytes).
|
||||
*
|
||||
* @throws DeviceException if the file is invalid or not found.
|
||||
*/
|
||||
private void readDeviceKeyFile()
|
||||
throws DeviceException
|
||||
{
|
||||
File file = deviceKeyFile.toFile();
|
||||
try
|
||||
{
|
||||
if (file.exists() && file.length() == DEVICE_KEY_SIZE)
|
||||
{
|
||||
deviceKey = Files.readAllBytes( file.toPath() );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DeviceException( DeviceException.DEV_INVALID_DEVICE_KEY_FILE,
|
||||
"Invalid device key file: " + deviceKeyFile
|
||||
+ ". Expected size: " + DEVICE_KEY_SIZE
|
||||
+ ", Actual size: " + (file.exists() ? file.length() : "N/A") );
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new DeviceException( DeviceException.DEV_INVALID_DEVICE_KEY_FILE,
|
||||
"Error reading device key file: " + deviceKeyFile, e );
|
||||
}
|
||||
}
|
||||
|
||||
public String getClientLocale()
|
||||
{
|
||||
return versions.get( "clientLocale" );
|
||||
}
|
||||
|
||||
public String getClientOS()
|
||||
{
|
||||
return versions.get( "clientOS" );
|
||||
}
|
||||
|
||||
public String getHobbes()
|
||||
{
|
||||
return versions.get( "hobbes" );
|
||||
}
|
||||
|
||||
String getDeviceSerial()
|
||||
{
|
||||
return deviceSerial;
|
||||
}
|
||||
|
||||
String getDeviceName()
|
||||
{
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public String getFingerprint()
|
||||
{
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public String getDeviceType()
|
||||
{
|
||||
return deviceType;
|
||||
}
|
||||
|
||||
public String getDeviceClass()
|
||||
{
|
||||
return deviceClass;
|
||||
}
|
||||
|
||||
public void updateDeviceFiles( Path deviceFile, Path deviceKeyFile )
|
||||
throws NoSuchAlgorithmException
|
||||
{
|
||||
this.deviceFile = deviceFile;
|
||||
this.deviceKeyFile = deviceKeyFile;
|
||||
createDeviceKeyFile( deviceKeyFile );
|
||||
createDeviceFile( this.getHobbes(), false );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user