Cette classe contient la logique métier principale pour créer les fichiers d'activation et pour activer un appareil auprès d'un serveur de contenu.

This commit is contained in:
2025-10-24 17:02:46 -07:00
parent 2f0a5f652b
commit 7605ed8fc3

View File

@@ -0,0 +1,481 @@
import common.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import javax.crypto.Cipher;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import static common.XmlUtils.ADOBE_ADEPT_NS;
// =====================================================================================
// Adept Activate
// =====================================================================================
public class AdeptActivate
{
private final Logger LOGGER = Logger.getLogger( AdeptActivate.class.getName() );
private boolean debug = false;
private final Activation activation;
private final Device device;
private final String outputDir;
AdeptActivate( Activation activation, Device device, String adeptDir )
{
this.activation = activation;
this.device = device;
this.outputDir = adeptDir;
}
public void setLogLevel( Level level )
{
LOGGER.setLevel( level );
if (level.intValue() <= Level.FINER.intValue())
debug = true;
}
/**
* Builds the SignIn request XML Document. Used exclusively by the {@link #signIn( Document ) }
* method. This SignIn request contains the devicekey, username, and password encrypted with
* Adobe's public key from the authentication Certificate (&lt;signInData&gt;). It also contains two
* RSA key pairs where the public keys merely Base64 encoded, but the private keys are encrypted
* with the device key before being Base64 encoded. Because the device key is in the &lt;signInData&gt;
* node, Adobe now has access to both the public and private keys.
*
* @param adobeID The Adobe ID.
* @param adobePassword The Adobe password.
* @param authenticationCertificate The authentication certificate (Base64 encoded). Used to
* encrypt the &lt;signInData&gt; content
* @return a sign in request with device data encrypted with Adobe's public key
* @throws GourouException on error
*/
public Document buildSignInRequest( String adobeID, String adobePassword,
String authenticationCertificate )
throws GeneralSecurityException, GourouException
{
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
docFactory.setNamespaceAware( true );
DocumentBuilder docBuilder;
try
{
docBuilder = docFactory.newDocumentBuilder();
Document signInRequest = docBuilder.newDocument();
Element signIn = signInRequest.createElementNS( ADOBE_ADEPT_NS, "adept:signIn" );
signInRequest.appendChild( signIn );
String loginMethod = activation.getLoginMethod();
if ("anonymous".equals( adobeID ))
{
signIn.setAttribute( "method", "anonymous" );
}
else if (null != loginMethod && !loginMethod.isEmpty())
{
signIn.setAttribute( "method", loginMethod );
}
else
{
signIn.setAttribute( "method", "AdobeID" );
}
byte[] encryptedSignInData;
byte[] deviceKeyBytes = device.getDeviceKey(); // AES session secret
// Build buffer <deviceKey> <len username> <username> <len password> <password>
byte[] adobeIDBytes = adobeID.getBytes( StandardCharsets.UTF_8 ); // UTF_8 encoded adobeID
byte[] adobePasswordBytes = adobePassword.getBytes( StandardCharsets.UTF_8 ); // ditto
ByteBuffer ar = ByteBuffer.allocate(
Device.DEVICE_KEY_SIZE + 1 + adobeIDBytes.length + 1 + adobePasswordBytes.length );
ar.put( deviceKeyBytes );
ar.put( (byte) adobeIDBytes.length ); // single byte
ar.put( adobeIDBytes );
ar.put( (byte) adobePasswordBytes.length ); // single byte
ar.put( adobePasswordBytes );
ar.flip();
// Encrypt with Adobe's authentication certificate (public key)
byte[] inData = new byte[ar.remaining()];
ar.get( inData ); // inData contains my private encryption key and the adobeID and password.
// encrypt with Adobe's public key
encryptedSignInData = CryptoUtils.x509Crypt( authenticationCertificate, Cipher.ENCRYPT_MODE, inData );
String signInData = Base64.getEncoder().encodeToString( encryptedSignInData );
XmlUtils.appendTextElem( signIn, "adept:signInData", signInData );
// Generate Auth key and License Key
int RSA_KEY_BITS = 1024;
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "RSA" );
keyPairGenerator.initialize( RSA_KEY_BITS );
// This key pair is for communications. The adobe server will return the private key
// in a pkcs12 object using the activation's UUID as the subject name and encrypted with
// a base-64 encoding of the device key. Presumably, it retains the public key for
// secure messaging.
KeyPair rsaAuthKeyPair = keyPairGenerator.generateKeyPair();
// Extract public key for Auth
PublicKey publicKey = rsaAuthKeyPair.getPublic();
String serializedData = Base64.getEncoder().encodeToString( publicKey.getEncoded() );
XmlUtils.appendTextElem( signIn, "adept:publicAuthKey", serializedData );
if (debug)
{
Files.writeString( Path.of( outputDir, "publicAuthKey.b64" ), serializedData );
}
// Extract private key for Auth and encrypt with device key
PrivateKey privateKey = rsaAuthKeyPair.getPrivate();
byte[] privateKeyData = privateKey.getEncoded();
if (debug)
{
serializedData = Base64.getEncoder().encodeToString( privateKeyData );
Files.writeString( Path.of( outputDir, "privateAuthKey.b64" ), serializedData );
}
byte[] encrypted = CryptoUtils.aesEncrypt( device.getDeviceKey(), privateKeyData );
// the encrypted data is prepended with a random 16 byte initialization vector,
serializedData = Base64.getEncoder().encodeToString( encrypted );
XmlUtils.appendTextElem( signIn, "adept:encryptedPrivateAuthKey", serializedData );
// This keypair is for licensing content
KeyPair rsaLicense = keyPairGenerator.generateKeyPair();
// Extract public key for License
publicKey = rsaLicense.getPublic();
serializedData = Base64.getEncoder().encodeToString( publicKey.getEncoded() );
XmlUtils.appendTextElem( signIn, "adept:publicLicenseKey", serializedData );
if (debug)
{
Files.writeString( Path.of( outputDir, "publicLicenseKey.b64" ), serializedData );
}
// Extract private key for License and encrypt with device key
privateKey = rsaLicense.getPrivate();
privateKeyData = privateKey.getEncoded();
if (debug)
{
serializedData = Base64.getEncoder().encodeToString( privateKeyData );
Files.writeString( Path.of( outputDir, "privateLicenseKey.b64" ), serializedData );
}
encrypted = CryptoUtils.aesEncrypt( device.getDeviceKey(), privateKeyData );
serializedData = Base64.getEncoder().encodeToString( encrypted );
XmlUtils.appendTextElem( signIn, "adept:encryptedPrivateLicenseKey", serializedData );
return signInRequest;
// TODO: At this point we have lost the Auth public key. We may want to remedy that.
// The publicLicenseKey is embedded in the <adept:licenseCertificate> node returned
}
catch (ParserConfigurationException e)
{
throw new GourouException( "Error building sign-in request XML", e );
}
catch (IOException e)
{
throw new RuntimeException( e );
}
}
/**
* Sign in to the activation server at the URL stored in the activation "authURL" property
* (required to activate device). The "signInRequest" contains two pairs of RSA encryption keys.
* The signIn response returns a UUID for the user (&lt;user&gt;) and a private authentication
* key encapsulated in a non-standard pkcs12 object using the UUID as the subject name and
* a base-64 encoding of the device key as the password. For anonymous sign-ins the private
* key is the same as that in the "signInRequest"; for existing Adobe IDs this may not be true.<br/><br/>
* It also returns the same private license key as was sent, re-encrypted with the device key
* and a new initialization vector(&lt;encryptedPrivateLicenseKey&gt;; decrypted and stored as the
* activation "privateLicenseKey" property). The &lt;licenseCertificate&gt; node contains the
* public license key from the request wrapped in an X509 certificate signed by the activation server.
*
* @param signInRequest The signIn request document. May not be null.
* @throws GourouException If the sign-in failed for any reason
*/
public void signIn( Document signInRequest )
throws GourouException, IOException, GeneralSecurityException
{
String xmlStr = XmlUtils.docToString( signInRequest, true );
if (debug)
{
Files.writeString( Paths.get( outputDir, "signInReq.xml" ), xmlStr );
}
String signInURL = activation.getAuthURL( ) + "/SignInDirect";
Document credentialsDoc;
try
{ // NOTE: the Document returned is not namespace aware.
credentialsDoc = activation.httpUtils.sendHTTPRequestForXML( signInURL, xmlStr,
"application/vnd.adobe.adept+xml", null, false );
}
catch (ParserConfigurationException | IOException | SAXException e)
{
throw new GourouException( GourouException.SIGN_INVALID_CREDENTIALS,
"Invalid credentials reply", e );
}
if (debug)
{
xmlStr = XmlUtils.docToString( credentialsDoc, false );
Files.writeString( Paths.get( outputDir, "signInResp.xml" ), xmlStr );
}
Element credentialsNode = credentialsDoc.getDocumentElement();
if (!"credentials".equals( credentialsNode.getTagName() ))
{
throw new GourouException( GourouException.SIGN_INVALID_CREDENTIALS,
"Invalid credentials reply: root is not 'credentials'" );
}
// The "buildSignInRequest" method created two pair of RSA encryption keys and sends them
// to the sign in server. TODO: The public authentication key is currently lost; the signIn
// response returns the private authentication key encapsulated in a non-standard pkcs12
// object using the activation's UUID as the subject name and a base-64 encoding of the device key
// as the password. It also returns the same private license key as was sent, simply
// re-encrypted with the device key and a new initialization vector. The licenseCertificate
// node is the public license key wrapped in an X509 certificate signed by Adobe
// Handle encryptedPrivateLicenseKey
Node encryptedPrivateLicenseKeyNode = XmlUtils.getNode( credentialsNode.getOwnerDocument(),
"//adept:encryptedPrivateLicenseKey", true );
String privateKeyDataBase64 = encryptedPrivateLicenseKeyNode.getFirstChild().getNodeValue();
byte[] privateKeyDataStr = Base64.getDecoder().decode( privateKeyDataBase64 );
// Decrypt the private key using device key, which we sent to Adobe in the sign in request
byte[] privateKey = decryptWithDeviceKey( privateKeyDataStr );
privateKeyDataBase64 = Base64.getEncoder().encodeToString( privateKey ); // Appears to match the private license key sent in the signin request.
// Remove existing encryptedPrivateLicenseKeyNode node from the credentialsNode...
credentialsNode.removeChild( encryptedPrivateLicenseKeyNode );
// and add the base64 encoded version of the decrypted privateLicenseKey into the credentialsNode instead
XmlUtils.appendTextElem( credentialsNode, "privateLicenseKey", privateKeyDataBase64 );
// copy the authenticationCertificate from the activation node to the credentials node
// TODO: why?? probably just because that's the way they are stored in the Windows registry
String authenticationCertificateB64 = activation.getAuthenticationCertificate();
XmlUtils.appendTextElem( credentialsNode, "adept:authenticationCertificate",
authenticationCertificateB64 );
activation.storeCredentials( credentialsNode );
try
{
activation.updateActivationFile();
}
catch (ParserConfigurationException e)
{
String message = "Cannot update activation file (" + activation.getActivationDirPath() + ")";
LOGGER.log( Level.SEVERE, message );
System.err.println( message );
throw new GourouException( GourouException.GGOUROU_FILE_ERROR, message );
}
}
/**
* Builds the activate` request XML. Used only by the {@link #activateDevice( Document )} method.
* Mostly just details about the device this process is running on.
*
* @return A new XML Document with the activation request.
*/
public Document buildActivateReq()
{
Document tempDoc;
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
docFactory.setNamespaceAware( true );
DocumentBuilder docBuilder;
try
{
docBuilder = docFactory.newDocumentBuilder();
tempDoc = docBuilder.newDocument();
Element root = tempDoc.createElementNS( ADOBE_ADEPT_NS, "adept:activate" );
tempDoc.appendChild( root );
root.setAttribute( "requestType", "initial" );
XmlUtils.appendTextElem( root, "adept:fingerprint",
device.getFingerprint() );
XmlUtils.appendTextElem( root, "adept:deviceType", device.getDeviceType());
XmlUtils.appendTextElem( root, "adept:clientOS", device.getClientOS() );
XmlUtils.appendTextElem( root, "adept:clientLocale",
device.getClientLocale() );
XmlUtils.appendTextElem( root, "adept:clientVersion",
device.getDeviceClass() );
Element targetDevice = tempDoc.createElementNS( ADOBE_ADEPT_NS, "adept:targetDevice" );
root.appendChild( targetDevice );
XmlUtils.appendTextElem( targetDevice, "adept:softwareVersion",
device.getHobbes() );
XmlUtils.appendTextElem( targetDevice, "adept:clientOS",
device.getClientOS() );
XmlUtils.appendTextElem( targetDevice, "adept:clientLocale",
device.getClientLocale() );
XmlUtils.appendTextElem( targetDevice, "adept:clientVersion",
device.getDeviceClass() );
XmlUtils.appendTextElem( targetDevice, "adept:deviceType",
device.getDeviceType() );
XmlUtils.appendTextElem( targetDevice, "adept:fingerprint",
device.getFingerprint() );
XmlUtils.addNonce( root );
XmlUtils.appendTextElem( root, "adept:user", activation.getUUID() );
}
catch (ParserConfigurationException e)
{
throw new GourouException( "Error building activate request XML", e );
}
return tempDoc;
}
/**
* Activate the device via network calls (activation must have successfully signed in prior to this).
* Only one new value is returned and stored in this process: A UUID for this device (presumably
* it is now registered with the activation server under this UUID).
*
* @throws GourouException if activation fails.
*/
public void activateDevice( Document activateReq )
throws GourouException, IOException, GeneralSecurityException,
ParserConfigurationException
{
LOGGER.log( Level.FINER, "Activate device" );
// Document activateReq = buildActivateReq();
String xmlStr; // = XmlUtils.docToString( activateReq, true );
Element root = activateReq.getDocumentElement();
ByteArrayOutputStream xer = new ByteArrayOutputStream();
//noinspection Convert2Diamond
XmlUtils.xml2ASN( root, new HashMap<String,String>(), xer );
byte[] encryptedHash = CryptoUtils.signData( xer.toByteArray(), activation.getPKCS12(), activation.getUUID(),
device.getDeviceKey() );
String signature = Base64.getEncoder().encodeToString( encryptedHash );
XmlUtils.appendTextElem( root, "adept:signature", signature );
String activationURL = activation.getActivationURL();
activationURL += "/Activate";
xmlStr = XmlUtils.docToString( activateReq, false );
if (debug)
{
Files.writeString( Paths.get( outputDir, "activationReq.xml" ), xmlStr );
}
Document activationTokenDoc; // This document has no newlines
try
{ // NOTE: this does not return a namespace aware document
activationTokenDoc = activation.httpUtils.sendHTTPRequestForXML( activationURL, xmlStr,
"application/vnd.adobe.adept+xml", null, false );
}
catch (IOException | SAXException | ParserConfigurationException e)
{
throw new GourouException( "Invalid activation token reply", e );
}
xmlStr = XmlUtils.docToString( activationTokenDoc, true );
if (activationTokenDoc.getDocumentElement().getTagName().equals( "error" ))
{
// TODO: If the request has expired, perhaps we can build a new request and try again?
// Are there other errors we should handle?
throw new GourouException( "Invalid activation token reply: " + xmlStr );
}
if (debug)
{
Files.writeString( Paths.get( outputDir, "activationResp.xml" ), xmlStr );
}
activation.updateActivationTokenData( activationTokenDoc.getDocumentElement() );
// the activation token is signed. Just for grins, lets see if it validates :-)
Element activationToken = activationTokenDoc.getDocumentElement();
Node sigNode = XmlUtils.getNode( activationTokenDoc,"//adept:signature", false );
activationToken.removeChild( sigNode );
xer = new ByteArrayOutputStream();
//noinspection Convert2Diamond
XmlUtils.xml2ASN( activationToken, new HashMap<String,String>(), xer );
MessageDigest shaMd = MessageDigest.getInstance( "SHA1" );
// the signature as I compute it
signature = Base64.getEncoder().encodeToString( shaMd.digest( xer.toByteArray() ) );
// now get the attached signature, decrypt it with the public key, and see if it matches.
String originalSig = sigNode.getTextContent();
byte[] enc = CryptoUtils.x509Crypt( activation.getCertificate(), Cipher.DECRYPT_MODE, Base64.getDecoder().decode( originalSig ) );
originalSig = Base64.getEncoder().encodeToString( enc );
if (!signature.equals( originalSig ))
System.out.println( "Signatures do not match" );
activation.updateActivationFile();
}
/**
* Decrypts data using the device key (AES-128 CBC).
*
* @param data The encrypted data with IV prefixed.
* @return The decrypted data.
*/
private byte[] decryptWithDeviceKey( byte[] data )
throws GeneralSecurityException
{
byte[] deviceKey = device.getDeviceKey();
byte[] decryptedData; // = new byte[data.length - 16]; // Maximum possible size: total length minus IV
// initializationVector is the first 16 bytes of `data`. Isolate it.
byte[] initializationVector = Arrays.copyOfRange( data, 0, 16 );
// Actual encrypted data starts from offset 16
byte[] encryptedPayload = Arrays.copyOfRange( data, 16, data.length );
decryptedData = CryptoUtils.aesDecrypt( deviceKey, initializationVector, encryptedPayload );
String serializedData = Base64.getEncoder().encodeToString( decryptedData );
LOGGER.log( Level.FINE, serializedData );
return decryptedData;
}
/**
* Exports the private license key to a file.
*
*/
@SuppressWarnings("UnusedReturnValue")
public byte[] exportPrivateLicenseKey( )
{
String uuid = activation.getUUID();
String userName = activation.getUsername();
if (null == userName)
userName = "anon";
Path outputPath = Paths.get( outputDir, userName + uuid.substring( 32 ) + ".pem" );
String plk = activation.getPrivateLicenseKey(); // Base64 encoded PKCS8 private key
try
{
BufferedWriter br = new BufferedWriter( new FileWriter( outputPath.toFile() ) );
br.write( "-----BEGIN RSA PRIVATE KEY-----" + System.lineSeparator() );
for (int remaining = plk.length(), start = 0; remaining > 0; remaining -= 64, start += 64)
{
br.write( plk.substring( start, start + (Math.min( remaining, 64 )) ) + System.lineSeparator() );
}
br.write( "-----END RSA PRIVATE KEY-----" + System.lineSeparator() );
br.close();
// Stored as Base 64 encoding, save as binary.
byte[] privateLicenseKey = Base64.getDecoder().decode( plk );
outputPath = Paths.get( outputDir, userName + uuid.substring( 32 ) + ".der" );
Files.write( outputPath, privateLicenseKey );
LOGGER.finest( "Exported private license keys to " + outputDir );
return privateLicenseKey;
}
catch (IOException e)
{
throw new GourouException( GourouException.GOUROU_FILE_ERROR,
"Unable to write " + outputPath + "to " + outputDir, e );
}
}
}