diff --git a/src/main/java/common/WinActivation.java b/src/main/java/common/WinActivation.java new file mode 100644 index 0000000..b1393be --- /dev/null +++ b/src/main/java/common/WinActivation.java @@ -0,0 +1,231 @@ +package common; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.NoSuchAlgorithmException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sun.jna.Library; +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.platform.win32.WinDef; +import org.w3c.dom.Document; + +import javax.xml.parsers.ParserConfigurationException; + +/** + * An extension of the Activation class that reads activation data from the + * Windows registry instead of from the file system. + */ +public class WinActivation extends Activation +{ + public interface ManualKernel32 extends Library + { + ManualKernel32 INSTANCE = Native.load( "kernel32", ManualKernel32.class); + WinDef.UINT GetSystemDirectoryW( Memory lpBuffer, int uSize); + } + + private final Logger LOGGER = Logger.getLogger( WinActivation.class.getName() ); + + private String section; + private String licenseURL = null; + + /** + * Constructor which loads existing activation info from the registry. + * + * @param activationFile the name of the activation file. This class reads its data + * from the Windows registry, so this file is only used when exporting windows data. + * @param httpUtils a Network helper object that makes http requests. Injected + * here, so we can create mock objects for unit testing + **/ + public WinActivation( Path activationFile, HttpUtils httpUtils, Level logLevel ) + { + super( activationFile, httpUtils, logLevel ); + parseActivationRegistry(); + LOGGER.setLevel( logLevel ); + } + + private String readNonBlank( BufferedReader in ) + throws IOException + { + String line; + do + { + line = in.readLine(); + } + while (line != null && line.isBlank()); + if (null == line) + { + return null; + } + return line.trim(); + } + + private String appendElement( String keyNode, String line, BufferedReader reader ) + throws IOException + { + String keyName = null; + String hkey = ""; + while (null != line) + { + // if the line starts with HKEY start a new node and call this recursively + if (line.startsWith( "HKEY" )) + { + if (null != keyNode && !line.startsWith( keyNode )) + { + break; + } + // a new element + hkey = line; + line = line.substring( keyNode.length() + 1 ); + if (line.contains( "\\" )) + { +// // if the line contains a slash, were going to need multiple elements. +// while (line.contains( "\\" )) +// { +// keyNode = line.substring( 0, line.indexOf( '\\' ) ); +// line = line.substring( keyNode.length() + 1 ).trim(); +// } +// if (!line.isBlank()) +// { +// +// } + line = readNonBlank( reader ); + line = appendElement( keyNode, line, reader ); + } + else + { + if (null != keyName) + { + // save off this name as the parent property. + if (!keyName.equalsIgnoreCase( section )) + { + section = keyName; + if ("activationToken".equalsIgnoreCase( section )) + { + hasActivationToken = true; + } + } + } + line = readNonBlank( reader ); + line = appendElement( hkey, line, reader ); + } + } + // if the line starts with "(Default)", change the parent node name + else if (line.startsWith( "(Default)" )) + { + if (line.contains( "REG_SZ" )) + { + keyName = line.substring( line.lastIndexOf( " " ) + 1 ); + + line = readNonBlank( reader ); + } + } + else if (line.contains( "REG_" )) + { + // has to be a name/value pair + String key = line.substring( 0, line.indexOf( ' ' ) ); + String value = line.substring( line.lastIndexOf( ' ' ) + 1 ).trim(); + if (line.contains( "REG_SZ" )) + { + // value is a string, add a text node. + if ("value".equals( key )) + { + // special case; operatorURL goes in a special list + if ("operatorURL".equalsIgnoreCase( keyName ) + && "operatorURLList".equalsIgnoreCase( section )) + { + operatorURLList.add( value ); + } + // special case: license info pairs are stored together + else if ("licenseServiceInfo".equalsIgnoreCase( section )) + { + if ("licenseURL".equalsIgnoreCase( keyName )) + { + licenseURL = value; + } + else if (null != licenseURL) + { + licenseServiceCertificates.put( licenseURL, value ); + licenseURL = null; + } + } + // loan token info is not stored in the Activation object + else if (!"loanToken".equalsIgnoreCase( section )) + { + properties.put( keyName, value ); + } + } + else + { + properties.put( key, value ); + } + } + line = readNonBlank( reader ); + } + else + { + break; + } + } + return line; + } + + public Document parseActivationRegistry() + throws GourouException + { + try + { + Process process = Runtime.getRuntime().exec( + "reg query HKEY_CURRENT_USER\\Software\\Adobe\\Adept\\Activation /s /v *" ); + BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream() ) ); + + String line = readNonBlank( reader ); + if (null == line) + { + return null; + } + String keyNode = line.substring( 0, line.lastIndexOf( '\\' ) ); + appendElement( keyNode, line, reader ); + process.waitFor(); + } + catch (Exception e) + { + e.printStackTrace(); + } + return null; + } + + /** + * Allows the WinDevice class to update the private license key with a decrypted value + * @param plk + */ + public void setPrivateLicenseKey( String plk ) + { + properties.put( "privateLicenseKey", plk ); + } + + /** + * Creates "activation.xml", "device.xml", and "devicesalt" from the values derived from windows. + * Simply calls the "update*File" methods on the Activation and Device objects. + * @param dirPath The output directory where the files will be written. + */ + public void updateFromWindows( Path dirPath, WinActivation activation, WinDevice device ) + throws ParserConfigurationException, IOException, NoSuchAlgorithmException + { + activation.updateActivationFile(); // Write the Windows registry values to libgourou compatible files. + device.updateDeviceFiles( dirPath.resolve( "device.xml" ), + dirPath.resolve( "devicesalt" ) ); // writes a new device.xml file + } + + + @Override + public Path getActivationDirPath() + { + return Paths.get( "").toAbsolutePath(); + } +} diff --git a/src/main/java/common/WinDevice.java b/src/main/java/common/WinDevice.java new file mode 100644 index 0000000..b6cc32d --- /dev/null +++ b/src/main/java/common/WinDevice.java @@ -0,0 +1,308 @@ +package common; + +import com.sun.jna.Memory; +import com.sun.jna.platform.win32.Advapi32Util; +import com.sun.jna.platform.win32.Crypt32Util; +import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.ptr.IntByReference; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Locale; +import java.util.logging.Level; + +import static com.sun.jna.platform.win32.Kernel32.INSTANCE; +import static com.sun.jna.platform.win32.WinDef.MAX_PATH; + +public class WinDevice extends Device +{ + private final WinActivation activation; + + private String windowsUsername; + + private String decryptedPrivateKey; + + public String getDecryptedPrivateKey() + { + return decryptedPrivateKey; + } + + + /** + * 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 WinDevice( WinActivation activation, Level logLevel ) + throws IOException, InterruptedException + { + LOGGER.setLevel( logLevel ); + this.activation = activation; + parsePrivateKey(); + fingerprint = activation.getDeviceFingerprint(); + deviceClass = "Desktop"; + InetAddress localHost = InetAddress.getLocalHost(); + deviceName = localHost.getHostName(); + deviceType = "standalone"; + } + + private static String toHex( byte[] data ) + { + final String HEXES = "0123456789abcdef"; + + final StringBuilder hex = new StringBuilder( 2 * data.length ); + for (final byte b : data) + { + hex.append( HEXES.charAt( (b & 0xF0) >> 4 ) ).append( HEXES.charAt( (b & 0x0F) ) ); + } + return hex.toString(); + } + + public static byte[] hexStringToByteArray( String hexString ) + { + if (hexString == null) + { + throw new IllegalArgumentException( "Invalid hex string: " + hexString ); + } + if (hexString.startsWith( "0x" ) || hexString.startsWith( "0X" )) + { + hexString = hexString.substring( 2 ); + } + if (hexString.length() % 2 != 0) + { + throw new IllegalArgumentException( "Hex string must have an even number of characters." ); + } + int hexLen = hexString.length(); + int byteLen = hexLen / 2; + byte[] data = new byte[byteLen]; + for (int i = 0; i < hexLen; i += 2) + { + data[i / 2] = (byte) ((Character.digit( hexString.charAt( i ), 16 ) << 4) + + Character.digit( hexString.charAt( i + 1 ), 16 )); + } + return data; + } + + private String readNonBlank( BufferedReader in ) + throws IOException + { + String line; + do + { + line = in.readLine(); + } + while (line != null && line.isBlank()); + if (null == line) + { + return null; + } + return line.trim(); + } + + private String constructEntropy() + { + long serial = getVolumeSerialNumber(); + byte[] buffer = new byte[ 4 ]; + for (int i = 3; i >= 0; i--) + { + buffer[i] = (byte) (serial & 0xFF); + serial >>= Byte.SIZE; + } + String serialHex = toHex( buffer ); + StringBuilder sb = new StringBuilder( serialHex ); + String vendor = getWMICInfo( "Manufacturer" ); + vendor = toHex( vendor.getBytes() ); + sb.append( vendor ); + String signature = getWMICInfo( "ProcessorId" ); + sb.append( signature.toLowerCase().substring( 10 ) ); + + sb.append( toHex( windowsUsername.getBytes( StandardCharsets.UTF_8 ) ) ); + while (sb.length() < 64) + sb.append( '0' ); + return sb.toString(); + } + + public String getWMICInfo( String key ) + { + try + { + String line; + Process p = Runtime.getRuntime().exec( "wmic cpu get " + key ); + BufferedReader input = new BufferedReader( new InputStreamReader( p.getInputStream() ) ); + while ((line = input.readLine()) != null) + { + if (line.isBlank()) + { + continue; + } + if (!line.toLowerCase().contains( key.toLowerCase() )) + { + return line.trim(); + } +// System.out.println( line ); + } + input.close(); + } + catch (Exception err) + { + err.printStackTrace(); + } + return ""; + } + + public long getVolumeSerialNumber() + { + Memory buffer = new Memory( MAX_PATH * 2); // Allocate space for Unicode (2 bytes per character) + + // Call the GetSystemDirectoryW function (Unicode version) + // This function retrieves the path of the system directory, e.g., C:\Windows\System32 + WinDef.UINT size = WinActivation.ManualKernel32.INSTANCE.GetSystemDirectoryW( buffer, MAX_PATH * 2 ); + String rootPathName = buffer.getWideString( 0 ); + rootPathName = rootPathName.substring( 0, rootPathName.indexOf( '\\' ) + 1 ); + + // Define buffers for the output + char[] volumeNameBuffer = new char[256]; + char[] fileSystemNameBuffer = new char[256]; + IntByReference volumeSerialNumber = new IntByReference(); + IntByReference maximumComponentLength = new IntByReference(); + IntByReference fileSystemFlags = new IntByReference(); + + if ( INSTANCE.GetVolumeInformation( + rootPathName, + volumeNameBuffer, + volumeNameBuffer.length, + volumeSerialNumber, + maximumComponentLength, + fileSystemFlags, + fileSystemNameBuffer, + fileSystemNameBuffer.length + )) + { + return Integer.toUnsignedLong( volumeSerialNumber.getValue()); + } + else + return -1; + } + + protected String parsePrivateKey() + throws IOException, InterruptedException + { + Process process = Runtime.getRuntime().exec( + "reg query HKEY_CURRENT_USER\\Software\\Adobe\\Adept\\Device /s /v *" ); + BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream() ) ); + + windowsUsername = Advapi32Util.getUserName(); + String line = readNonBlank( reader ); + if (null == line) + { + return null; + } + while (!line.contains( "key" )) + { + line = readNonBlank( reader ); + if (null == line) + { + return null; // TODO: throw an exception instead + } + if (line.startsWith( "username" )) + { + windowsUsername = line.substring( (line.lastIndexOf( ' ' )) ).trim(); + } + } + String key = line.substring( line.lastIndexOf( ' ' ) ).trim(); + byte[] entropy; + byte[] protectedData = hexStringToByteArray( key ); + try + { + entropy = hexStringToByteArray( constructEntropy() ); + deviceKey = Crypt32Util.cryptUnprotectData( protectedData, entropy, 0, null ); +// winDecrypt( protectedData, entropy ); // equivalent of devicesalt? + String privatekeystring = activation.getPrivateLicenseKey(); + byte[] privateKey = Base64.getDecoder().decode( privatekeystring ); + byte[] iv = new byte[16]; + byte[] decrypted = CryptoUtils.aesDecrypt( deviceKey, iv, privateKey ); + byte[] copy = new byte[decrypted.length - 26]; + System.arraycopy( decrypted, 26, copy, 0, decrypted.length - 26 ); + + byte[] plk = buildPkcs8KeyFromPkcs1Key( copy ); + String plkString = Base64.getEncoder().encodeToString( plk ); + activation.setPrivateLicenseKey( plkString ); + decryptedPrivateKey = Base64.getEncoder().encodeToString( copy ); +// System.out.println( decryptedPrivateKey ); + return decryptedPrivateKey; + } + catch (Exception e) + { + System.out.println( e.getMessage() ); + } + finally + { + process.waitFor(); + } + return null; // TODO: Throw exception instead. + } + + private static byte[] buildPkcs8KeyFromPkcs1Key(byte[] innerKey) + { + var result = new byte[innerKey.length + 26]; + System.arraycopy(Base64.getDecoder().decode("MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKY="), 0, result, 0, 26); + System.arraycopy( BigInteger.valueOf( result.length - 4).toByteArray(), 0, result, 2, 2); + System.arraycopy(BigInteger.valueOf(innerKey.length).toByteArray(), 0, result, 24, 2); + System.arraycopy(innerKey, 0, result, 26, innerKey.length); + return result; + } + + @Override + protected void createDeviceKeyFile( Path deviceKeyFile ) + { + try + { + Files.write( deviceKeyFile, deviceKey ); + } + catch (IOException ioException) + { + throw new DeviceException( "Unable to write device key file", ioException ); + } + } + +// @Override +// public String getHobbes() +// { +// return AdeptProcessor.HOBBES_DEFAULT_VERSION; +// } + + @Override + public String getClientOS() + { + return System.getProperty( "os.name" ) + " " + System.getProperty( "os.version" ); + } + + @Override + public String getClientLocale() + { + return Locale.getDefault().toString(); + } + + @Override + public String getDeviceClass() + { + return "Desktop"; + } + + @Override + public String getDeviceType() + { + return "standalone"; + } +}