From d5ce4d625ee0ba34643e3018310aa3c857cb7c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Soutad=C3=A9?= Date: Sat, 3 Jul 2021 21:57:53 +0200 Subject: [PATCH] Initial commit --- .gitignore | 8 + LICENSE | 165 ++++++++ Makefile | 55 +++ README.md | 85 ++++ include/bytearray.h | 143 +++++++ include/device.h | 84 ++++ include/drmprocessorclient.h | 350 ++++++++++++++++ include/fulfillment_item.h | 65 +++ include/libgourou.h | 190 +++++++++ include/libgourou_common.h | 350 ++++++++++++++++ include/libgourou_log.h | 50 +++ include/user.h | 105 +++++ scripts/setup.sh | 10 + src/bytearray.cpp | 173 ++++++++ src/device.cpp | 305 ++++++++++++++ src/fulfillment_item.cpp | 91 +++++ src/libgourou.cpp | 662 +++++++++++++++++++++++++++++++ src/pugixml.cpp | 1 + src/user.cpp | 198 +++++++++ utils/LICENSE | 26 ++ utils/Makefile | 24 ++ utils/acsmdownloader.cpp | 255 ++++++++++++ utils/activate.cpp | 263 ++++++++++++ utils/drmprocessorclientimpl.cpp | 385 ++++++++++++++++++ utils/drmprocessorclientimpl.h | 110 +++++ 25 files changed, 4153 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 include/bytearray.h create mode 100644 include/device.h create mode 100644 include/drmprocessorclient.h create mode 100644 include/fulfillment_item.h create mode 100644 include/libgourou.h create mode 100644 include/libgourou_common.h create mode 100644 include/libgourou_log.h create mode 100644 include/user.h create mode 100755 scripts/setup.sh create mode 100644 src/bytearray.cpp create mode 100644 src/device.cpp create mode 100644 src/fulfillment_item.cpp create mode 100644 src/libgourou.cpp create mode 120000 src/pugixml.cpp create mode 100644 src/user.cpp create mode 100644 utils/LICENSE create mode 100644 utils/Makefile create mode 100644 utils/acsmdownloader.cpp create mode 100644 utils/activate.cpp create mode 100644 utils/drmprocessorclientimpl.cpp create mode 100644 utils/drmprocessorclientimpl.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c47778 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +obj +lib +*.a +*.so +*~ +utils/acsmdownloader +utils/activate +.adept diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d4ee6dd --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ + +AR ?= $(CROSS)ar +CXX ?= $(CROSS)g++ + +CXXFLAGS=-Wall -fPIC -I./include -I./lib -I./lib/pugixml/src/ +LDFLAGS= + +ifneq ($(DEBUG),) +CXXFLAGS += -ggdb -O0 +else +CXXFLAGS += -O2 +endif + +SRCDIR := src +INCDIR := inc +BUILDDIR := obj +TARGETDIR := bin +SRCEXT := cpp +OBJEXT := o + +SOURCES=src/libgourou.cpp src/user.cpp src/device.cpp src/fulfillment_item.cpp src/bytearray.cpp src/pugixml.cpp +OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT))) + +.PHONY: utils + +all: lib obj libgourou utils + +lib: + mkdir lib + ./scripts/setup.sh + +obj: + mkdir obj + +$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) + $(CXX) $(CXXFLAGS) -c $^ -o $@ + +libgourou: libgourou.a libgourou.so + +libgourou.a: $(OBJECTS) + $(AR) crs $@ obj/*.o + +libgourou.so: libgourou.a + $(CXX) obj/*.o $(LDFLAGS) -o $@ -shared + +utils: + make -C utils ROOT=$(PWD) CXX=$(CXX) AR=$(AR) DEBUG=$(DEBUG) + +clean: + rm -rf libgourou.a libgourou.so obj + make -C utils clean + +ultraclean: clean + rm -rf lib + make -C utils ultraclean diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dda844 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +Introduction +------------ + +libgourou is a free implementation of Adobe's ADEPT protocol used to add DRM on ePub files. It overcome the lacks of Adobe support for Linux platforms. + + +Architecture +------------ + +Like RMSDK, libgourou has a client/server scheme. All platform specific functions (crypto, network...) has to be implemented in a client class (that derives from DRMProcessorClient) while server implements ADEPT protocol. +A reference implementation using Qt, OpenSSL and libzip is provided (in _utils_ directory). + +Main fucntions to use from gourou::DRMProcessor are : + + * Get an ePub from an ACSM file : _fulfill()_ and _download()_ + * Create a new device : _createDRMProcessor()_ + * Register a new device : _signIn()_ and _activateDevice()_ + + +You can import configuration from (at least) : + + * Kobo device : .adept/device.xml, .adept/devicesalt and .adept/activation.xml + * Bookeen device : .adobe-digital-editions/device.xml, root/devkey.bin and .adobe-digital-editions/activation.xml + +Or create a new one. Be careful : there is a limited number of devices that can be created bye one account. + +ePub are encrypted using a shared key : one account / multiple devices, so you can create and register a device into your computer and read downloaded (and encrypted) ePub file with your eReader configured using the same AdobeID account. + + +Dependencies +------------ + +For libgourou : + + * None + +For utils : + + * QT5Core + * QT5Network + * OpenSSL + * libzip + + +Compilation +----------- + +User _make_ + + _make_ [CROSS=XXX] [DEBUG=1] + +CROSS can define a cross compiler prefix (ie arm-linux-gnueabihf-) + +DEBUG can be set to compile in DEBUG mode + + +Utils +----- + +You can import configuration from your eReader or create a new one with utils/activate : + + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD + ./utils/activate -u + +Then a _./.adept_ directory is created with all configuration file + +To download an ePub : + + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD + ./utils/acsmdownloader -f + + +Copyright +--------- + +Grégory Soutadé + + + +License +------- + +libgourou : LGPL v3 or later + +utils : BSD diff --git a/include/bytearray.h b/include/bytearray.h new file mode 100644 index 0000000..5e3f508 --- /dev/null +++ b/include/bytearray.h @@ -0,0 +1,143 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _BYTEARRAY_H_ +#define _BYTEARRAY_H_ + +#include +#include + +namespace gourou +{ + /** + * @brief Utility class for byte array management. + * + * It's an equivalent of QByteArray + * + * Data handled is first copied in a newly allocated buffer + * and then shared between all copies until last object is destroyed + */ + class ByteArray + { + public: + + /** + * @brief Create an empty byte array + */ + ByteArray(); + + /** + * @brief Initialize ByteArray with a copy of data + * + * @param data Data to be copied + * @param length Length of data + */ + ByteArray(const unsigned char* data, unsigned int length); + + /** + * @brief Initialize ByteArray with a copy of data + * + * @param data Data to be copied + * @param length Optional length of data. If length == -1, it use strlen(data) as length + */ + ByteArray(const char* data, int length=-1); + + /** + * @brief Initialize ByteArray with a copy of str + * + * @param str Use internal data of str + */ + ByteArray(const std::string& str); + + ByteArray(const ByteArray& other); + ~ByteArray(); + + /** + * @brief Encode "other" data into base64 and put it into a ByteArray + */ + static ByteArray fromBase64(const ByteArray& other); + + /** + * @brief Encode data into base64 and put it into a ByteArray + * + * @param data Data to be encoded + * @param length Optional length of data. If length == -1, it use strlen(data) as length + */ + static ByteArray fromBase64(const char* data, int length=-1); + + /** + * @brief Encode str into base64 and put it into a ByteArray + * + * @param str Use internal data of str + */ + static ByteArray fromBase64(const std::string& str); + + /** + * @brief Return a string with base64 encoded internal data + */ + std::string toBase64(); + + /** + * @brief Return a string with human readable hex encoded internal data + */ + std::string toHex(); + + /** + * @brief Append a byte to internal data + */ + void append(unsigned char c); + + /** + * @brief Append data to internal data + */ + void append(const unsigned char* data, unsigned int length); + + /** + * @brief Append str to internal data + */ + void append(const char* str); + + /** + * @brief Append str to internal data + */ + void append(const std::string& str); + + /** + * @brief Get internal data. Must bot be modified nor freed + */ + const unsigned char* data() {return _data;} + + /** + * @brief Get internal data length + */ + unsigned int length() {return _length;} + + ByteArray& operator=(const ByteArray& other); + + private: + void initData(const unsigned char* data, unsigned int length); + void addRef(); + void delRef(); + + const unsigned char* _data; + unsigned int _length; + static std::map refCounter; + }; +} +#endif diff --git a/include/device.h b/include/device.h new file mode 100644 index 0000000..a562ab9 --- /dev/null +++ b/include/device.h @@ -0,0 +1,84 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _DEVICE_H_ +#define _DEVICE_H_ + +namespace gourou +{ + class DRMProcessor; + + /** + * @brief This class is a container for device.xml (device info) and devicesalt (device private key). It should not be used by user. + */ + class Device + { + public: + static const int DEVICE_KEY_SIZE = 16; + static const int DEVICE_SERIAL_LEN = 10; + + /** + * @brief Main Device constructor + * + * @param processor Instance of DRMProcessor + * @param deviceFile Path of device.xml + * @param deviceKeyFile Path of devicesalt + */ + Device(DRMProcessor* processor, const std::string& deviceFile, const std::string& deviceKeyFile); + + /** + * @brief Return value of devicesalt file (DEVICE_KEY_SIZE len) + */ + const unsigned char* getDeviceKey(); + + /** + * @brief Get one value of device.xml (deviceClass, deviceSerial, deviceName, deviceType, jobbes, clientOS, clientLocale) + */ + std::string getProperty(const std::string& property, const std::string& _default=std::string("")); + std::string operator[](const std::string& property); + + /** + * @brief Create device.xml and devicesalt files when they did not exists + * + * @param processor Instance of DRMProcessor + * @param dirName Directory where to put files (.adept) + * @param hobbes Hobbes (client version) to set + * @param randomSerial Create a random serial (new device each time) or not (serial computed from machine specs) + */ + static Device* createDevice(DRMProcessor* processor, const std::string& dirName, const std::string& hobbes, bool randomSerial); + + private: + DRMProcessor* processor; + std::string deviceFile; + std::string deviceKeyFile; + unsigned char deviceKey[DEVICE_KEY_SIZE]; + std::map properties; + + Device(DRMProcessor* processor); + + std::string makeFingerprint(const std::string& serial); + std::string makeSerial(bool random); + void parseDeviceFile(); + void parseDeviceKeyFile(); + void createDeviceFile(const std::string& hobbes, bool randomSerial); + void createDeviceKeyFile(); + }; +} + +#endif diff --git a/include/drmprocessorclient.h b/include/drmprocessorclient.h new file mode 100644 index 0000000..5563332 --- /dev/null +++ b/include/drmprocessorclient.h @@ -0,0 +1,350 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _DRMPROCESSORCLIENT_H_ +#define _DRMPROCESSORCLIENT_H_ + +#include + +namespace gourou +{ + /** + * @brief All fucntions that must be implemented by a client + * This allow libgourou to have only few external libraries dependencies + * and improve code portability + */ + + class DigestInterface + { + public: + /** + * @brief Create a digest handler (for now only SHA1 is used) + * + * @param digestName Digest name to instanciate + */ + virtual void* createDigest(const std::string& digestName) = 0; + + /** + * @brief Update digest engine with new data + * + * @param handler Digest handler + * @param data Data to digest + * @param length Length of data + * + * @return OK/KO + */ + virtual int digestUpdate(void* handler, unsigned char* data, unsigned int length) = 0; + + /** + * @brief Finalize digest with remained buffered data and destroy handler + * + * @param handler Digest handler + * @param digestOut Digest result (buffer must be pre allocated with right size) + * + * @return OK/KO + */ + virtual int digestFinalize(void* handler, unsigned char* digestOut) = 0; + + /** + * @brief Global digest function + * + * @param digestName Digest name to instanciate + * @param data Data to digest + * @param length Length of data + * @param digestOut Digest result (buffer must be pre allocated with right size) + * + * @return OK/KO + */ + virtual int digest(const std::string& digestName, unsigned char* data, unsigned int length, unsigned char* digestOut) = 0; + }; + + class RandomInterface + { + public: + /** + * @brief Generate random bytes + * + * @param bytesOut Buffer to fill with random bytes + * @param length Length of bytesOut + */ + virtual void randBytes(unsigned char* bytesOut, unsigned int length) = 0; + }; + + class HTTPInterface + { + public: + + /** + * @brief Send HTTP (GET or POST) request + * + * @param URL HTTP URL + * @param POSTData POST data if needed, if not set, a GET request is done + * @param contentType Optional content type of POST Data + */ + virtual std::string sendHTTPRequest(const std::string& URL, const std::string& POSTData=std::string(""), const std::string& contentType=std::string("")) = 0; + }; + + class RSAInterface + { + public: + enum RSA_KEY_TYPE { + RSA_KEY_PKCS12 = 0, + RSA_KEY_X509 + }; + + /** + * @brief Encrypt data with RSA private key. Data is padded using PKCS1.5 + * + * @param RSAKey RSA key in binary form + * @param RSAKeyLength RSA key length + * @param keyType Key type + * @param password Optional password for RSA PKCS12 certificate + * @param data Data to encrypt + * @param dataLength Data length + * @param res Encryption result (pre allocated buffer) + */ + virtual void RSAPrivateEncrypt(const unsigned char* RSAKey, unsigned int RSAKeyLength, + const RSA_KEY_TYPE keyType, const std::string& password, + const unsigned char* data, unsigned dataLength, + unsigned char* res) = 0; + + /** + * @brief Encrypt data with RSA public key. Data is padded using PKCS1.5 + * + * @param RSAKey RSA key in binary form + * @param RSAKeyLength RSA key length + * @param keyType Key type + * @param password Optional password for RSA PKCS12 certificate + * @param data Data to encrypt + * @param dataLength Data length + * @param res Encryption result (pre allocated buffer) + */ + virtual void RSAPublicEncrypt(const unsigned char* RSAKey, unsigned int RSAKeyLength, + const RSA_KEY_TYPE keyType, + const unsigned char* data, unsigned dataLength, + unsigned char* res) = 0; + + /** + * @brief Generate RSA key. Expnonent is fixed (65537 / 0x10001) + * + * @param keyLengthBits Length of key (in bits) to generate + * + * @return generatedKey + */ + virtual void* generateRSAKey(int keyLengthBits) = 0; + + /** + * @brief Destroy key previously generated + * + * @param handler Key to destroy + */ + virtual void destroyRSAHandler(void* handler) = 0; + + /** + * @brief Extract public key (big number) from RSA handler + * + * @param handler RSA handler (generated key) + * @param keyOut Pre allocated buffer (if *keyOut != 0). If *keyOut is 0, memory is internally allocated (must be freed) + * @param keyOutLength Length of result + */ + virtual void extractRSAPublicKey(void* handler, unsigned char** keyOut, unsigned int* keyOutLength) = 0; + + /** + * @brief Extract private key (big number) from RSA handler + * + * @param handler RSA handler (generated key) + * @param keyOut Pre allocated buffer (if *keyOut != 0). If *keyOut is 0, memory is internally allocated (must be freed) + * @param keyOutLength Length of result + */ + virtual void extractRSAPrivateKey(void* handler, unsigned char** keyOut, unsigned int* keyOutLength) = 0; + }; + + class CryptoInterface + { + public: + enum CHAINING_MODE { + CHAIN_ECB=0, + CHAIN_CBC + }; + + /** + * @brief Do AES encryption. If length of data is not multiple of 16, PKCS#5 padding is done + * + * @param chaining Chaining mode + * @param key AES key + * @param keyLength AES key length + * @param iv IV key + * @param ivLength IV key length + * @param dataIn Data to encrypt + * @param dataInLength Data length + * @param dataOut Encrypted data + * @param dataOutLength Length of encrypted data + */ + virtual void AESEncrypt(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength, + const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) = 0; + + /** + * @brief Init AES CBC encryption + * + * @param chaining Chaining mode + * @param key AES key + * @param keyLength AES key length + * @param iv IV key + * @param ivLength IV key length + * + * @return AES handler + */ + virtual void* AESEncryptInit(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv=0, unsigned int ivLength=0) = 0; + + /** + * @brief Encrypt data + * + * @param handler AES handler + * @param dataIn Data to encrypt + * @param dataInLength Data length + * @param dataOut Encrypted data + * @param dataOutLength Length of encrypted data + */ + virtual void AESEncryptUpdate(void* handler, const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) = 0; + + /** + * @brief Finalize AES encryption (pad and encrypt last block if needed) + * Destroy handler at the end + * + * @param handler AES handler + * @param dataOut Last block of encrypted data + * @param dataOutLength Length of encrypted data + */ + virtual void AESEncryptFinalize(void* handler, unsigned char* dataOut, unsigned int* dataOutLength) = 0; + + /** + * @brief Do AES decryption. If length of data is not multiple of 16, PKCS#5 padding is done + * + * @param chaining Chaining mode + * @param key AES key + * @param keyLength AES key length + * @param iv IV key + * @param ivLength IV key length + * @param dataIn Data to encrypt + * @param dataInLength Data length + * @param dataOut Encrypted data + * @param dataOutLength Length of encrypted data + */ + virtual void AESDecrypt(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength, + const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) = 0; + + /** + * @brief Init AES decryption + * + * @param chaining Chaining mode + * @param key AES key + * @param keyLength AES key length + * @param iv IV key + * @param ivLength IV key length + * + * @return AES handler + */ + virtual void* AESDecryptInit(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv=0, unsigned int ivLength=0) = 0; + + /** + * @brief Decrypt data + * + * @param handler AES handler + * @param dataIn Data to decrypt + * @param dataInLength Data length + * @param dataOut Decrypted data + * @param dataOutLength Length of decrypted data + */ + virtual void AESDecryptUpdate(void* handler, const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) = 0; + /** + * @brief Finalize AES decryption (decrypt last block and remove padding if it is set). + * Destroy handler at the end + * + * @param handler AES handler + * @param dataOut Last block decrypted data + * @param dataOutLength Length of decrypted data + */ + virtual void AESDecryptFinalize(void* handler, unsigned char* dataOut, unsigned int* dataOutLength) = 0; + }; + + + class ZIPInterface + { + public: + /** + * @brief Open a zip file and return an handler + * + * @param path Path of zip file + * + * @return ZIP file handler + */ + virtual void* zipOpen(const std::string& path) = 0; + + /** + * @brief Read zip internal file + * + * @param handler ZIP file handler + * @param path Internal path inside zip file + * + * @return File content + */ + virtual std::string zipReadFile(void* handler, const std::string& path) = 0; + + /** + * @brief Write zip internal file + * + * @param handler ZIP file handler + * @param path Internal path inside zip file + * @param content Internal file content + */ + virtual void zipWriteFile(void* handler, const std::string& path, const std::string& content) = 0; + + /** + * @brief Delete zip internal file + * + * @param handler ZIP file handler + * @param path Internal path inside zip file + */ + virtual void zipDeleteFile(void* handler, const std::string& path) = 0; + + /** + * @brief Close ZIP file handler + * + * @param handler ZIP file handler + */ + virtual void zipClose(void* handler) = 0; + }; + + class DRMProcessorClient: public DigestInterface, public RandomInterface, public HTTPInterface, \ + public RSAInterface, public CryptoInterface, public ZIPInterface + {}; +} +#endif diff --git a/include/fulfillment_item.h b/include/fulfillment_item.h new file mode 100644 index 0000000..7f7b847 --- /dev/null +++ b/include/fulfillment_item.h @@ -0,0 +1,65 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _FULFILLMENT_ITEM_H_ +#define _FULFILLMENT_ITEM_H_ + +#include "bytearray.h" + +#include + +namespace gourou +{ + class User; + + /** + * @brief This class is a container for a fulfillment object + */ + class FulfillmentItem + { + public: + FulfillmentItem(pugi::xml_document& doc, User* user); + + /** + * @brief Return metadata value from ACSM metadata section + * + * @param name Name of key to return + */ + std::string getMetadata(std::string name); + + /** + * @brief Return rights generated by ACS server (XML format) + */ + std::string getRights(); + + /** + * @brief Return epub download URL + */ + std::string getDownloadURL(); + + private: + pugi::xml_node metadatas; + pugi::xml_document rights; + std::string downloadURL; + + void buildRights(const pugi::xml_node& licenseToken, User* user); + }; +} + +#endif diff --git a/include/libgourou.h b/include/libgourou.h new file mode 100644 index 0000000..2aa6017 --- /dev/null +++ b/include/libgourou.h @@ -0,0 +1,190 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _LIBGOUROU_H_ +#define _LIBGOUROU_H_ + +#include "bytearray.h" +#include "device.h" +#include "user.h" +#include "fulfillment_item.h" +#include "drmprocessorclient.h" + +#include + +#ifndef HOBBES_DEFAULT_VERSION +#define HOBBES_DEFAULT_VERSION "10.0.4" +#endif + +#ifndef DEFAULT_ADEPT_DIR +#define DEFAULT_ADEPT_DIR "./.adept" +#endif + +#ifndef ACS_SERVER +#define ACS_SERVER "http://adeactivate.adobe.com/adept" +#endif + +namespace gourou +{ + /** + * @brief Main class that handle all ADEPTS functions (fulfill, download, signIn, activate) + */ + class DRMProcessor + { + public: + + /** + * @brief Main constructor. To be used once all is configured (user has signedIn, device is activated) + * + * @param client Client processor + * @param deviceFile Path of device.xml + * @param activationFile Path of activation.xml + * @param deviceKeyFile Path of devicesalt + */ + DRMProcessor(DRMProcessorClient* client, const std::string& deviceFile, const std::string& activationFile, const std::string& deviceKeyFile); + + ~DRMProcessor(); + + /** + * @brief Fulfill ACSM file to server in order to retrieve ePub fulfillment item + * + * @param ACSMFile Path of ACSMFile + * + * @return a FulfillmentItem if all is OK + */ + FulfillmentItem* fulfill(const std::string& ACSMFile); + + /** + * @brief Once fulfilled, ePub file needs to be downloaded. + * During this operation, DRM information is added into downloaded file + * + * @param item Item from fulfill() method + * @param path Output file path + */ + void download(FulfillmentItem* item, std::string path); + + /** + * @brief SignIn into ACS Server (required to activate device) + * + * @param adobeID AdobeID username + * @param adobePassword Adobe password + */ + void signIn(const std::string& adobeID, const std::string& adobePassword); + + /** + * @brief Activate newly created device (user must have successfuly signedIn before) + */ + void activateDevice(); + + /** + * @brief Create a new ADEPT environment (device.xml, devicesalt and activation.xml). + * + * @param client Client processor + * @param randomSerial Always generate a new device (or not) + * @param dirName Directory where to put generated files (.adept) + * @param hobbes Override hobbes default version + * @param ACSServer Override main ACS server (default adeactivate.adobe.com) + */ + static DRMProcessor* createDRMProcessor(DRMProcessorClient* client, + bool randomSerial=false, const std::string& dirName=std::string(DEFAULT_ADEPT_DIR), + const std::string& hobbes=std::string(HOBBES_DEFAULT_VERSION), + const std::string& ACSServer=ACS_SERVER); + + /** + * @brief Get current log level + */ + static int getLogLevel(); + + /** + * @brief Set log level (higher number for verbose output) + */ + static void setLogLevel(int logLevel); + + /** + * Functions used internally, should not be called by user + */ + + /** + * @brief Send HTTP (GET or POST) request + * + * @param URL HTTP URL + * @param POSTData POST data if needed, if not set, a GET request is done + * @param contentType Optional content type of POST Data + */ + ByteArray sendRequest(const std::string& URL, const std::string& POSTData=std::string(), const char* contentType=0); + + /** + * @brief Send HTTP POST request to URL with document as POSTData + */ + ByteArray sendRequest(const pugi::xml_document& document, const std::string& url); + + /** + * @brief In place encrypt data with private device key + */ + ByteArray encryptWithDeviceKey(const unsigned char* data, unsigned int len); + + /** + * @brief In place decrypt data with private device key + */ + ByteArray decryptWithDeviceKey(const unsigned char* data, unsigned int len); + + /** + * @brief Return base64 encoded value of RSA public key + */ + std::string serializeRSAPublicKey(void* rsa); + + /** + * @brief Return base64 encoded value of RSA private key encrypted with private device key + */ + std::string serializeRSAPrivateKey(void* rsa); + + /** + * @brief Get current user + */ + User* getUser() { return user; } + + /** + * @brief Get current device + */ + Device* getDevice() { return device; } + + /** + * @brief Get current client + */ + DRMProcessorClient* getClient() { return client; } + + private: + gourou::DRMProcessorClient* client; + gourou::Device* device; + gourou::User* user; + + DRMProcessor(DRMProcessorClient* client); + + void pushString(void* sha_ctx, const std::string& string); + void pushTag(void* sha_ctx, uint8_t tag); + void hashNode(const pugi::xml_node& root, void *sha_ctx, std::map nsHash); + void hashNode(const pugi::xml_node& root, unsigned char* sha_out); + void buildFulfillRequest(pugi::xml_document& acsmDoc, pugi::xml_document& fulfillReq); + void buildActivateReq(pugi::xml_document& activateReq); + ByteArray sendFulfillRequest(const pugi::xml_document& document, const std::string& url); + void buildSignInRequest(pugi::xml_document& signInRequest, const std::string& adobeID, const std::string& adobePassword, const std::string& authenticationCertificate); + }; +} + +#endif diff --git a/include/libgourou_common.h b/include/libgourou_common.h new file mode 100644 index 0000000..5e20375 --- /dev/null +++ b/include/libgourou_common.h @@ -0,0 +1,350 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _LIBGOUROU_COMMON_H_ +#define _LIBGOUROU_COMMON_H_ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include "bytearray.h" + +namespace gourou +{ + /** + * Some common utilities + */ + + #define ADOBE_ADEPT_NS "http://ns.adobe.com/adept" + + static const int SHA1_LEN = 20; + static const int RSA_KEY_SIZE = 128; + static const int RSA_KEY_SIZE_BITS = (RSA_KEY_SIZE*8); + + enum GOUROU_ERROR { + GOUROU_DEVICE_DOES_NOT_MATCH = 0x1000, + GOUROU_INVALID_CLIENT, + GOUROU_TAG_NOT_FOUND, + GOUROU_ADEPT_ERROR, + GOUROU_FILE_ERROR + }; + + enum FULFILL_ERROR { + FF_ACSM_FILE_NOT_EXISTS = 0x1100, + FF_INVALID_ACSM_FILE, + FF_NO_HMAC_IN_ACSM_FILE, + FF_NOT_ACTIVATED, + FF_NO_OPERATOR_URL + }; + + enum DOWNLOAD_ERROR { + DW_NO_ITEM = 0x1200, + }; + + enum SIGNIN_ERROR { + SIGN_INVALID_CREDENTIALS = 0x1300, + }; + + enum ACTIVATE_ERROR { + ACTIVATE_NOT_SIGNEDIN = 0x1400 + }; + + enum DEV_ERROR { + DEV_MKPATH = 0x2000, + DEV_MAC_ERROR, + DEV_INVALID_DEVICE_FILE, + DEV_INVALID_DEVICE_KEY_FILE, + DEV_INVALID_DEV_PROPERTY, + }; + + enum USER_ERROR { + USER_MKPATH = 0x3000, + USER_INVALID_ACTIVATION_FILE, + USER_NO_AUTHENTICATION_URL, + USER_NO_PROPERTY, + }; + + enum FULFILL_ITEM_ERROR { + FFI_INVALID_FULFILLMENT_DATA = 0x4000 + }; + + enum CLIENT_ERROR { + CLIENT_BAD_PARAM = 0x5000, + CLIENT_INVALID_PKCS12, + CLIENT_INVALID_CERTIFICATE, + CLIENT_NO_PRIV_KEY, + CLIENT_RSA_ERROR, + CLIENT_BAD_CHAINING, + CLIENT_BAD_KEY_SIZE, + CLIENT_BAD_ZIP_FILE, + CLIENT_ZIP_ERROR, + CLIENT_GENERIC_EXCEPTION + + }; + + /** + * Generic exception class + */ + class Exception : public std::exception + { + public: + Exception(int code, const char* message, const char* file, int line): + code(code), line(line), file(file) + { + std::stringstream msg; + msg << "Exception code : 0x" << std::setbase(16) << code << std::endl; + msg << "Message : " << message << std::endl; + if (logLevel >= DEBUG) + msg << "File : " << file << ":" << std::setbase(10) << line << std::endl; + fullmessage = strdup(msg.str().c_str()); + } + + ~Exception() + { + free(fullmessage); + } + + const char * what () const throw () { return fullmessage; } + + int getErrorCode() {return code;} + + private: + int code, line; + const char* message, *file; + char* fullmessage; + }; + + /** + * @brief Throw an exception + */ +#define EXCEPTION(code, message) \ + {std::stringstream __msg;__msg << message; throw gourou::Exception(code, __msg.str().c_str(), __FILE__, __LINE__);} + + /** + * Stream writer for pugi::xml + */ + class StringXMLWriter : public pugi::xml_writer + { + public: + virtual void write(const void* data, size_t size) + { + result.append(static_cast(data), size); + } + + const std::string& getResult() {return result;} + + private: + std::string result; + }; + + static const char* ws = " \t\n\r\f\v"; + + /** + * @brief trim from end of string (right) + */ + inline std::string& rtrim(std::string& s, const char* t = ws) + { + s.erase(s.find_last_not_of(t) + 1); + return s; + } + + /** + * @brief trim from beginning of string (left) + */ + inline std::string& ltrim(std::string& s, const char* t = ws) + { + s.erase(0, s.find_first_not_of(t)); + return s; + } + + /** + * @brief trim from both ends of string (right then left) + */ + inline std::string& trim(std::string& s, const char* t = ws) + { + return ltrim(rtrim(s, t), t); + } + + /** + * @brief Extract text node from tag in document + * It can throw an exception if tag does not exists + * or just return an empty value + */ + static inline std::string extractTextElem(const pugi::xml_document& doc, const char* tagName, bool throwOnNull=true) + { + pugi::xpath_node xpath_node = doc.select_node(tagName); + + if (!xpath_node) + { + if (throwOnNull) + EXCEPTION(GOUROU_TAG_NOT_FOUND, "Tag " << tagName << " not found"); + + return ""; + } + + pugi::xml_node node = xpath_node.node().first_child(); + + if (!node) + { + if (throwOnNull) + EXCEPTION(GOUROU_TAG_NOT_FOUND, "Text element for tag " << tagName << " not found"); + + return ""; + } + + std::string res = node.value(); + return trim(res); + } + + /** + * @brief Append an element to root with a sub text element + * + * @param root Root node where to put child + * @param name Tag name for child + * @param value Text child value of tag element + */ + static inline void appendTextElem(pugi::xml_node& root, const std::string& name, const std::string& value) + { + pugi::xml_node node = root.append_child(name.c_str()); + node.append_child(pugi::node_pcdata).set_value(value.c_str()); + } + + /** + * @brief Write data in a file. If it already exists, it's truncated + */ + static inline void writeFile(std::string path, const unsigned char* data, unsigned int length) + { + int fd = open(path.c_str(), O_CREAT|O_WRONLY|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH); + + if (fd <= 0) + EXCEPTION(GOUROU_FILE_ERROR, "Unable to create " << path); + + if (write(fd, data, length) != length) + EXCEPTION(GOUROU_FILE_ERROR, "Write error for file " << path); + + close (fd); + } + + /** + * @brief Write data in a file. If it already exists, it's truncated + */ + static inline void writeFile(std::string path, ByteArray& data) + { + writeFile(path, data.data(), data.length()); + } + + /** + * @brief Write data in a file. If it already exists, it's truncated + */ + static inline void writeFile(std::string path, const std::string& data) + { + writeFile(path, (const unsigned char*)data.c_str(), data.length()); + } + + /** + * Read data from file + */ + static inline void readFile(std::string path, const unsigned char* data, unsigned int length) + { + int fd = open(path.c_str(), O_RDONLY); + + if (fd <= 0) + EXCEPTION(GOUROU_FILE_ERROR, "Unable to open " << path); + + if (read(fd, (void*)data, length) != length) + EXCEPTION(GOUROU_FILE_ERROR, "Read error for file " << path); + + close (fd); + } + +#define PATH_MAX_STRING_SIZE 256 + + // https://gist.github.com/ChisholmKyle/0cbedcd3e64132243a39 +/* recursive mkdir */ + static inline int mkdir_p(const char *dir, const mode_t mode) { + char tmp[PATH_MAX_STRING_SIZE]; + char *p = NULL; + struct stat sb; + size_t len; + + /* copy path */ + len = strnlen (dir, PATH_MAX_STRING_SIZE); + if (len == 0 || len == PATH_MAX_STRING_SIZE) { + return -1; + } + memcpy (tmp, dir, len); + tmp[len] = '\0'; + + /* remove trailing slash */ + if(tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + + /* check if path exists and is a directory */ + if (stat (tmp, &sb) == 0) { + if (S_ISDIR (sb.st_mode)) { + return 0; + } + } + + /* recursive mkdir */ + for(p = tmp + 1; *p; p++) { + if(*p == '/') { + *p = 0; + /* test path */ + if (stat(tmp, &sb) != 0) { + /* path does not exist - create directory */ + if (mkdir(tmp, mode) < 0) { + return -1; + } + } else if (!S_ISDIR(sb.st_mode)) { + /* not a directory */ + return -1; + } + *p = '/'; + } + } + /* test path */ + if (stat(tmp, &sb) != 0) { + /* path does not exist - create directory */ + if (mkdir(tmp, mode) < 0) { + return -1; + } + } else if (!S_ISDIR(sb.st_mode)) { + /* not a directory */ + return -1; + } + return 0; + } +} + +#endif diff --git a/include/libgourou_log.h b/include/libgourou_log.h new file mode 100644 index 0000000..19f3725 --- /dev/null +++ b/include/libgourou_log.h @@ -0,0 +1,50 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _LIBGOUROU_LOG_H_ +#define _LIBGOUROU_LOG_H_ + +#include + +namespace gourou { + enum GOUROU_LOG_LEVEL { + ERROR, + WARN, + INFO, + DEBUG, + TRACE + }; + + extern GOUROU_LOG_LEVEL logLevel; + +#define GOUROU_LOG(__lvl, __msg) if (__lvl <= gourou::logLevel) {std::cout << __msg << std::endl << std::flush;} +#define GOUROU_LOG_FUNC() GOUROU_LOG(TRACE, __FUNCTION__ << "() @ " << __FILE__ << ":" << __LINE__) + + /** + * @brief Get current log level + */ + GOUROU_LOG_LEVEL getLogLevel(); + + /** + * @brief Set log level + */ + void setLogLevel(GOUROU_LOG_LEVEL level); +} + +#endif diff --git a/include/user.h b/include/user.h new file mode 100644 index 0000000..f605df0 --- /dev/null +++ b/include/user.h @@ -0,0 +1,105 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#ifndef _USER_H_ +#define _USER_H_ + +#include +#include "bytearray.h" + +#include + +namespace gourou +{ + class DRMProcessor; + + /** + * @brief This class is a container for activation.xml (activation info). It should not be used by user. + */ + class User + { + public: + User(DRMProcessor* processor, const std::string& activationFile); + + /** + * @brief Retrieve some values from activation.xml + */ + std::string& getUUID(); + std::string& getPKCS12(); + std::string& getDeviceUUID(); + std::string& getDeviceFingerprint(); + std::string& getUsername(); + std::string& getLoginMethod(); + std::string& getCertificate(); + std::string& getAuthenticationCertificate(); + std::string& getPrivateLicenseKey(); + + /** + * @brief Read activation.xml and put result into doc + */ + void readActivation(pugi::xml_document& doc); + + /** + * @brief Update activation.xml with new data + */ + void updateActivationFile(const char* data); + + /** + * @brief Update activation.xml with doc data + */ + void updateActivationFile(const pugi::xml_document& doc); + + /** + * @brief Get one value of activation.xml + */ + std::string getProperty(const std::string property); + + /** + * @brief Create activation.xml and devicesalt files if they did not exists + * + * @param processor Instance of DRMProcessor + * @param dirName Directory where to put files (.adept) + * @param ACSServer Server used for signIn + */ + static User* createUser(DRMProcessor* processor, const std::string& dirName, const std::string& ACSServer); + + private: + DRMProcessor* processor; + pugi::xml_document activationDoc; + + std::string activationFile; + std::string pkcs12; + std::string uuid; + std::string deviceUUID; + std::string deviceFingerprint; + std::string username; + std::string loginMethod; + std::string certificate; + std::string authenticationCertificate; + std::string privateLicenseKey; + + User(DRMProcessor* processor); + + void parseActivationFile(bool throwOnNull=true); + ByteArray signIn(const std::string& adobeID, const std::string& adobePassword, + ByteArray authenticationCertificate); + }; +} + +#endif diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..3a0896d --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Pugixml +git clone https://github.com/zeux/pugixml.git lib/pugixml +pushd lib/pugixml +git checkout latest +popd + +# Base64 +git clone https://gist.github.com/f0fd86b6c73063283afe550bc5d77594.git lib/base64 diff --git a/src/bytearray.cpp b/src/bytearray.cpp new file mode 100644 index 0000000..7de443d --- /dev/null +++ b/src/bytearray.cpp @@ -0,0 +1,173 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ +#include + +#include + +#include + +namespace gourou +{ + std::map ByteArray::refCounter; + + ByteArray::ByteArray():_data(0), _length(0) + {} + + ByteArray::ByteArray(const unsigned char* data, unsigned int length) + { + initData(data, length); + } + + ByteArray::ByteArray(const char* data, int length) + { + if (length == -1) + length = strlen(data) + 1; + + initData((const unsigned char*)data, (unsigned int) length); + } + + ByteArray::ByteArray(const std::string& str) + { + initData((unsigned char*)str.c_str(), (unsigned int)str.length() + 1); + } + + void ByteArray::initData(const unsigned char* data, unsigned int length) + { + _data = new unsigned char[length]; + memcpy((void*)_data, data, length); + _length = length; + + addRef(); + } + + ByteArray::ByteArray(const ByteArray& other) + { + this->_data = other._data; + this->_length = other._length; + + addRef(); + } + + ByteArray& ByteArray::operator=(const ByteArray& other) + { + delRef(); + + this->_data = other._data; + this->_length = other._length; + + addRef(); + + return *this; + } + + ByteArray::~ByteArray() + { + delRef(); + } + + void ByteArray::addRef() + { + if (!_data) return; + + if (refCounter.count(_data) == 0) + refCounter[_data] = 1; + else + refCounter[_data]++; + } + + void ByteArray::delRef() + { + if (!_data) return; + + if (refCounter[_data] == 1) + { + delete[] _data; + refCounter.erase(_data); + } + else + refCounter[_data]--; + } + + ByteArray ByteArray::fromBase64(const ByteArray& other) + { + std::string b64; + + macaron::Base64::Decode(std::string((char*)other._data, other._length), b64); + + return ByteArray(b64); + } + + ByteArray ByteArray::fromBase64(const char* data, int length) + { + std::string b64; + + if (length == -1) + length = strlen(data); + + macaron::Base64::Decode(std::string(data, length), b64); + + return ByteArray(b64); + } + + ByteArray ByteArray::fromBase64(const std::string& str) + { + return ByteArray::fromBase64(str.c_str(), str.length()); + } + + std::string ByteArray::toBase64() + { + return macaron::Base64::Encode(std::string((char*)_data, _length)); + } + + std::string ByteArray::toHex() + { + char* tmp = new char[_length*2+1]; + + for(int i=0; i<(int)_length; i++) + sprintf(&tmp[i*2], "%02x", _data[i]); + + tmp[_length*2] = 0; + + std::string res = tmp; + delete[] tmp; + + return res; + } + + void ByteArray::append(const unsigned char* data, unsigned int length) + { + const unsigned char* oldData = _data; + unsigned char* newData = new unsigned char[_length+length]; + + memcpy(newData, oldData, _length); + + delRef(); + + memcpy(&newData[_length], data, length); + _length += length; + + _data = newData; + + addRef(); + } + + void ByteArray::append(unsigned char c) { append(&c, 1);} + void ByteArray::append(const char* str) { append((const unsigned char*)str, strlen(str));} + void ByteArray::append(const std::string& str) { append((const unsigned char*)str.c_str(), str.length()); } +} diff --git a/src/device.cpp b/src/device.cpp new file mode 100644 index 0000000..1f4a8ea --- /dev/null +++ b/src/device.cpp @@ -0,0 +1,305 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// From https://stackoverflow.com/questions/1779715/how-to-get-mac-address-of-your-machine-using-a-c-program/35242525 +#include +#include +#include +#include +#include + +int get_mac_address(unsigned char* mac_address) +{ + struct ifreq ifr; + struct ifconf ifc; + char buf[1024]; + int success = 0; + + int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sock == -1) { EXCEPTION(gourou::DEV_MAC_ERROR, "Unable to create socket"); }; + + ifc.ifc_len = sizeof(buf); + ifc.ifc_buf = buf; + if (ioctl(sock, SIOCGIFCONF, &ifc) == -1) { EXCEPTION(gourou::DEV_MAC_ERROR, "SIOCGIFCONF ioctl failed"); } + + struct ifreq* it = ifc.ifc_req; + const struct ifreq* const end = it + (ifc.ifc_len / sizeof(struct ifreq)); + + for (; it != end; ++it) { + strcpy(ifr.ifr_name, it->ifr_name); + if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) { + if (! (ifr.ifr_flags & IFF_LOOPBACK)) { // don't count loopback + if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) { + success = 1; + break; + } + } + } + else { EXCEPTION(gourou::DEV_MAC_ERROR, "SIOCGIFFLAGS ioctl failed"); } + } + + if (success) + { + memcpy(mac_address, ifr.ifr_hwaddr.sa_data, 6); + return 0; + } + + return 1; +} + + +namespace gourou +{ + Device::Device(DRMProcessor* processor): + processor(processor) + {} + + Device::Device(DRMProcessor* processor, const std::string& deviceFile, const std::string& deviceKeyFile): + processor(processor), deviceFile(deviceFile), deviceKeyFile(deviceKeyFile) + { + parseDeviceKeyFile(); + parseDeviceFile(); + } + + /* SHA1(uid ":" username ":" macaddress ":" */ + std::string Device::makeSerial(bool random) + { + unsigned char sha_out[SHA1_LEN]; + DRMProcessorClient* client = processor->getClient(); + + if (!random) + { + uid_t uid = getuid(); + struct passwd * passwd = getpwuid(uid); + // Default mac address in case of failure + unsigned char mac_address[6] = {0x01, 0x02, 0x03, 0x04, 0x05}; + + get_mac_address(mac_address); + + int dataToHashLen = 10 /* UID */ + strlen(passwd->pw_name) + sizeof(mac_address)*2 /*mac address*/ + 1 /* \0 */; + dataToHashLen += 8; /* Separators */ + unsigned char* dataToHash = new unsigned char[dataToHashLen]; + dataToHashLen = snprintf((char*)dataToHash, dataToHashLen, "%d:%s:%02x:%02x:%02x:%02x:%02x:%02x:", + uid, passwd->pw_name, + mac_address[0], mac_address[1], mac_address[2], + mac_address[3], mac_address[4], mac_address[5]); + + client->digest("SHA1", dataToHash, dataToHashLen+1, sha_out); + + delete[] dataToHash; + } + else + { + client->randBytes(sha_out, sizeof(sha_out)); + } + + std::string res = ByteArray((const char*)sha_out, DEVICE_SERIAL_LEN).toHex(); + GOUROU_LOG(DEBUG, "Serial : " << res); + return res; + } + + /* base64(SHA1 (serial + privateKey)) */ + std::string Device::makeFingerprint(const std::string& serial) + { + DRMProcessorClient* client = processor->getClient(); + unsigned char sha_out[SHA1_LEN]; + + void* handler = client->createDigest("SHA1"); + client->digestUpdate(handler, (unsigned char*) serial.c_str(), serial.length()); + client->digestUpdate(handler, deviceKey, sizeof(deviceKey)); + client->digestFinalize(handler, sha_out); + + std::string res = ByteArray(sha_out, sizeof(sha_out)).toBase64(); + GOUROU_LOG(DEBUG, "Fingerprint : " << res); + return res; + } + + void Device::createDeviceFile(const std::string& hobbes, bool randomSerial) + { + struct utsname sysname; + uname(&sysname); + + std::string serial = makeSerial(randomSerial); + std::string fingerprint = makeFingerprint(serial); + + pugi::xml_document deviceDoc; + pugi::xml_node decl = deviceDoc.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + + pugi::xml_node root = deviceDoc.append_child("adept:deviceInfo"); + root.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS; + + appendTextElem(root, "adept:deviceClass", "Desktop"); + appendTextElem(root, "adept:deviceSerial", serial); + appendTextElem(root, "adept:deviceName", sysname.nodename); + appendTextElem(root, "adept:deviceType", "standalone"); + + pugi::xml_node version = root.append_child("adept:version"); + version.append_attribute("name") = "hobbes"; + version.append_attribute("value") = hobbes.c_str(); + + version = root.append_child("adept:version"); + version.append_attribute("name") = "clientOS"; + std::string os = std::string(sysname.sysname) + " " + std::string(sysname.release); + version.append_attribute("value") = os.c_str(); + + version = root.append_child("adept:version"); + version.append_attribute("name") = "clientLocale"; + version.append_attribute("value") = setlocale(LC_ALL, NULL); + + appendTextElem(root, "adept:fingerprint", fingerprint); + + StringXMLWriter xmlWriter; + deviceDoc.save(xmlWriter, " "); + + GOUROU_LOG(DEBUG, "Create device file " << deviceFile); + + writeFile(deviceFile, xmlWriter.getResult()); + } + + void Device::createDeviceKeyFile() + { + unsigned char key[DEVICE_KEY_SIZE]; + + GOUROU_LOG(DEBUG, "Create device key file " << deviceKeyFile); + + processor->getClient()->randBytes(key, sizeof(key)); + + writeFile(deviceKeyFile, key, sizeof(key)); + } + + Device* Device::createDevice(DRMProcessor* processor, const std::string& dirName, const std::string& hobbes, bool randomSerial) + { + struct stat _stat; + + if (stat(dirName.c_str(), &_stat) != 0) + { + if (mkdir_p(dirName.c_str(), S_IRWXU)) + EXCEPTION(DEV_MKPATH, "Unable to create " << dirName) + } + + Device* device = new Device(processor); + + device->deviceFile = dirName + "/device.xml"; + device->deviceKeyFile = dirName + "/devicesalt"; + + try + { + device->parseDeviceKeyFile(); + } + catch (...) + { + device->createDeviceKeyFile(); + device->parseDeviceKeyFile(); + } + + try + { + device->parseDeviceFile(); + } + catch (...) + { + device->createDeviceFile(hobbes, randomSerial); + device->parseDeviceFile(); + } + + return device; + } + + const unsigned char* Device::getDeviceKey() + { + return deviceKey; + } + + void Device::parseDeviceFile() + { + pugi::xml_document doc; + + if (!doc.load_file(deviceFile.c_str())) + EXCEPTION(DEV_INVALID_DEVICE_FILE, "Invalid device file"); + + try + { + properties["deviceClass"] = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceClass"); + properties["deviceSerial"] = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceSerial"); + properties["deviceName"] = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceName"); + properties["deviceType"] = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceType"); + properties["fingerprint"] = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:fingerprint"); + + pugi::xpath_node_set nodeSet = doc.select_nodes("/adept:deviceInfo/adept:version"); + + for (pugi::xpath_node_set::const_iterator it = nodeSet.begin(); + it != nodeSet.end(); ++it) + { + pugi::xml_node node = it->node(); + pugi::xml_attribute name = node.attribute("name"); + pugi::xml_attribute value = node.attribute("value"); + + properties[name.value()] = value.value(); + } + } + catch (gourou::Exception& e) + { + EXCEPTION(DEV_INVALID_DEVICE_FILE, "Invalid device file"); + } + } + + void Device::parseDeviceKeyFile() + { + struct stat _stat; + + if (stat(deviceKeyFile.c_str(), &_stat) == 0 && + _stat.st_size == DEVICE_KEY_SIZE) + { + readFile(deviceKeyFile, deviceKey, sizeof(deviceKey)); + } + else + EXCEPTION(DEV_INVALID_DEVICE_KEY_FILE, "Invalid device key file"); + } + + std::string Device::getProperty(const std::string& property, const std::string& _default) + { + if (properties.find(property) == properties.end()) + { + if (_default == "") + EXCEPTION(DEV_INVALID_DEV_PROPERTY, "Invalid property " << property); + + return _default; + } + + return properties[property]; + } + + std::string Device::operator[](const std::string& property) + { + return getProperty(property); + } +} diff --git a/src/fulfillment_item.cpp b/src/fulfillment_item.cpp new file mode 100644 index 0000000..843df01 --- /dev/null +++ b/src/fulfillment_item.cpp @@ -0,0 +1,91 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#include +#include +#include "user.h" + +namespace gourou +{ + FulfillmentItem::FulfillmentItem(pugi::xml_document& doc, User* user) + { + metadatas = doc.select_node("//metadata").node(); + + if (!metadatas) + EXCEPTION(FFI_INVALID_FULFILLMENT_DATA, "No metadata tag in document"); + + pugi::xml_node node = doc.select_node("/envelope/fulfillmentResult/resourceItemInfo/src").node(); + downloadURL = node.first_child().value(); + + if (downloadURL == "") + EXCEPTION(FFI_INVALID_FULFILLMENT_DATA, "No download URL in document"); + + pugi::xml_node licenseToken = doc.select_node("/envelope/fulfillmentResult/resourceItemInfo/licenseToken").node(); + + if (!licenseToken) + EXCEPTION(FFI_INVALID_FULFILLMENT_DATA, "Any license token in document"); + + buildRights(licenseToken, user); + } + + void FulfillmentItem::buildRights(const pugi::xml_node& licenseToken, User* user) + { + pugi::xml_node decl = rights.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + + pugi::xml_node root = rights.append_child("adept:rights"); + root.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS; + + pugi::xml_node newLicenseToken = root.append_copy(licenseToken); + if (!newLicenseToken.attribute("xmlns")) + newLicenseToken.append_attribute("xmlns") = ADOBE_ADEPT_NS; + + pugi::xml_node licenseServiceInfo = root.append_child("licenseServiceInfo"); + licenseServiceInfo.append_attribute("xmlns") = ADOBE_ADEPT_NS; + licenseServiceInfo.append_copy(licenseToken.select_node("licenseURL").node()); + pugi::xml_node certificate = licenseServiceInfo.append_child("certificate"); + certificate.append_child(pugi::node_pcdata).set_value(user->getCertificate().c_str()); + } + + std::string FulfillmentItem::getMetadata(std::string name) + { + // https://stackoverflow.com/questions/313970/how-to-convert-an-instance-of-stdstring-to-lower-case + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char c){ return std::tolower(c); }); + name = std::string("dc:") + name; + pugi::xpath_node path = metadatas.select_node(name.c_str()); + + if (!path) + return ""; + + return path.node().first_child().value(); + } + + std::string FulfillmentItem::getRights() + { + StringXMLWriter xmlWriter; + rights.save(xmlWriter, " "); + return xmlWriter.getResult(); + } + + std::string FulfillmentItem::getDownloadURL() + { + return downloadURL; + } +} diff --git a/src/libgourou.cpp b/src/libgourou.cpp new file mode 100644 index 0000000..54ab1f7 --- /dev/null +++ b/src/libgourou.cpp @@ -0,0 +1,662 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#include +#include +#include + +#include +#include +#include + +#define ASN_NONE 0x00 +#define ASN_NS_TAG 0x01 +#define ASN_CHILD 0x02 +#define ASN_END_TAG 0x03 +#define ASN_TEXT 0x04 +#define ASN_ATTRIBUTE 0x05 + +namespace gourou +{ + GOUROU_LOG_LEVEL logLevel = WARN; + + DRMProcessor::DRMProcessor(DRMProcessorClient* client):client(client), device(0), user(0) + { + if (!client) + EXCEPTION(GOUROU_INVALID_CLIENT, "DRMProcessorClient is NULL"); + } + + DRMProcessor::DRMProcessor(DRMProcessorClient* client, + const std::string& deviceFile, const std::string& activationFile, + const std::string& deviceKeyFile): + client(client), device(0), user(0) + { + if (!client) + EXCEPTION(GOUROU_INVALID_CLIENT, "DRMProcessorClient is NULL"); + + device = new Device(this, deviceFile, deviceKeyFile); + user = new User(this, activationFile); + + if (user->getDeviceFingerprint() != "" && + (*device)["fingerprint"] != user->getDeviceFingerprint()) + EXCEPTION(GOUROU_DEVICE_DOES_NOT_MATCH, "User and device fingerprint does not match"); + } + + DRMProcessor::~DRMProcessor() + { + if (device) delete device; + if (user) delete user; + } + + DRMProcessor* DRMProcessor::createDRMProcessor(DRMProcessorClient* client, bool randomSerial, const std::string& dirName, + const std::string& hobbes, const std::string& ACSServer) + { + DRMProcessor* processor = new DRMProcessor(client); + + Device* device = Device::createDevice(processor, dirName, hobbes, randomSerial); + processor->device = device; + + User* user = User::createUser(processor, dirName, ACSServer); + processor->user = user; + + return processor; + } + + + void DRMProcessor::pushString(void* sha_ctx, const std::string& string) + { + int length = string.length(); + uint16_t nlength = htons(length); + char c; + + if (logLevel >= TRACE) + printf("%02x %02x ", ((uint8_t*)&nlength)[0], ((uint8_t*)&nlength)[1]); + + client->digestUpdate(sha_ctx, (unsigned char*)&nlength, sizeof(nlength)); + + for(int i=0; idigestUpdate(sha_ctx, (unsigned char*)&c, 1); + if (logLevel >= TRACE) + printf("%c", c); + } + if (logLevel >= TRACE) + printf("\n"); + } + + void DRMProcessor::pushTag(void* sha_ctx, uint8_t tag) + { + client->digestUpdate(sha_ctx, &tag, sizeof(tag)); + if (logLevel >= TRACE) + printf("%02x ", tag); + } + + void DRMProcessor::hashNode(const pugi::xml_node& root, void *sha_ctx, std::map nsHash) + { + switch(root.type()) + { + case pugi::node_element: + { + std::string name = root.name(); + + // Look for "xmlns[:]" attribute + for (pugi::xml_attribute_iterator ait = root.attributes_begin(); + ait != root.attributes_end(); ++ait) + { + std::string attrName(ait->name()); + + if (attrName.find("xmlns") == 0) + { + std::string ns("GENERICNS"); + // Compound xmlns:Name attribute + if (attrName.find(':') != std::string::npos) + ns = attrName.substr(attrName.find(':')+1); + + nsHash[ns] = ait->value(); + break; + } + } + + // Remove namespace from tag + // If we have a namespace for the first time, put it to hash + if (name.find(':') != std::string::npos) + { + size_t nsIndex = name.find(':'); + std::string nodeNS = name.substr(0, nsIndex); + + pushTag(sha_ctx, ASN_NS_TAG); + pushString(sha_ctx, nsHash[nodeNS]); + + name = name.substr(nsIndex+1); + } + // Global xmlns, always send to hash + else if (nsHash.find("GENERICNS") != nsHash.end()) + { + pushTag(sha_ctx, ASN_NS_TAG); + pushString(sha_ctx, nsHash["GENERICNS"]); + } + + pushString(sha_ctx, name); + + // Must be parsed in reverse order + for (pugi::xml_attribute attr = root.last_attribute(); + attr; attr = attr.previous_attribute()) + { + if (std::string(attr.name()).find("xmlns") != std::string::npos) + continue; + + pushTag(sha_ctx, ASN_ATTRIBUTE); + pushString(sha_ctx, ""); + + pushString(sha_ctx, attr.name()); + pushString(sha_ctx, attr.value()); + } + + pushTag(sha_ctx, ASN_CHILD); + + for (pugi::xml_node child : root.children()) + hashNode(child, sha_ctx, nsHash); + + pushTag(sha_ctx, ASN_END_TAG); + + break; + } + case pugi::node_pcdata: + { + std::string trimmed = root.value(); + trimmed = trim(trimmed); + + if (trimmed.length()) + { + pushTag(sha_ctx, ASN_TEXT); + pushString(sha_ctx, trimmed); + } + + break; + } + default: + break; + } + } + + void DRMProcessor::hashNode(const pugi::xml_node& root, unsigned char* sha_out) + { + void* sha_ctx = client->createDigest("SHA1"); + + std::map nsHash; + + hashNode(root, sha_ctx, nsHash); + + client->digestFinalize(sha_ctx, sha_out); + + if (logLevel >= DEBUG) + { + printf("\nSHA OUT : "); + for(int i=0; i<(int)SHA1_LEN; i++) + printf("%02x ", sha_out[i]); + printf("\n"); + } + } + + ByteArray DRMProcessor::sendRequest(const std::string& URL, const std::string& POSTdata, const char* contentType) + { + if (contentType == 0) + contentType = ""; + std::string reply = client->sendHTTPRequest(URL, POSTdata, contentType); + + pugi::xml_document replyDoc; + replyDoc.load_buffer(reply.c_str(), reply.length()); + + pugi::xml_node root = replyDoc.first_child(); + if (std::string(root.name()) == "error") + { + EXCEPTION(GOUROU_ADEPT_ERROR, root.attribute("data").value()); + } + + return ByteArray(reply); + } + + ByteArray DRMProcessor::sendRequest(const pugi::xml_document& document, const std::string& url) + { + StringXMLWriter xmlWriter; + document.save(xmlWriter, " "); + std::string xmlStr = xmlWriter.getResult(); + + return sendRequest(url, xmlStr, (const char*)"application/vnd.adobe.adept+xml"); + } + + void DRMProcessor::buildFulfillRequest(pugi::xml_document& acsmDoc, pugi::xml_document& fulfillReq) + { + pugi::xml_node decl = fulfillReq.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + + pugi::xml_node root = fulfillReq.append_child("adept:fulfill"); + root.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS; + + appendTextElem(root, "adept:user", user->getUUID()); + appendTextElem(root, "adept:device", user->getDeviceUUID()); + appendTextElem(root, "adept:deviceType", (*device)["deviceType"]); + + root.append_copy(acsmDoc.first_child()); + + pugi::xml_node targetDevice = root.append_child("adept:targetDevice"); + appendTextElem(targetDevice, "adept:softwareVersion", (*device)["hobbes"]); + appendTextElem(targetDevice, "adept:clientOS", (*device)["clientOS"]); + appendTextElem(targetDevice, "adept:clientLocale", (*device)["clientLocale"]); + appendTextElem(targetDevice, "adept:clientVersion", (*device)["deviceClass"]); + appendTextElem(targetDevice, "adept:deviceType", (*device)["deviceType"]); + appendTextElem(targetDevice, "adept:fingerprint", (*device)["fingerprint"]); + + pugi::xml_node activationToken = targetDevice.append_child("adept:activationToken"); + appendTextElem(activationToken, "adept:user", user->getUUID()); + appendTextElem(activationToken, "adept:device", user->getDeviceUUID()); + } + + FulfillmentItem* DRMProcessor::fulfill(const std::string& ACSMFile) + { + if (!user->getPKCS12().length()) + EXCEPTION(FF_NOT_ACTIVATED, "Device not activated"); + + pugi::xml_document acsmDoc; + + if (!acsmDoc.load_file(ACSMFile.c_str(), pugi::parse_ws_pcdata_single)) + EXCEPTION(FF_INVALID_ACSM_FILE, "Invalid ACSM file " << ACSMFile); + + GOUROU_LOG(INFO, "Fulfill " << ACSMFile); + + // Build req file + pugi::xml_document fulfillReq; + + buildFulfillRequest(acsmDoc, fulfillReq); + pugi::xpath_node root = fulfillReq.select_node("//adept:fulfill"); + pugi::xml_node rootNode = root.node(); + + // Remove HMAC + pugi::xpath_node xpathRes = fulfillReq.select_node("//hmac"); + + if (!xpathRes) + EXCEPTION(FF_NO_HMAC_IN_ACSM_FILE, "hmac tag not found in ACSM file"); + + pugi::xml_node hmacNode = xpathRes.node(); + pugi::xml_node hmacParentNode = hmacNode.parent(); + + hmacParentNode.remove_child(hmacNode); + + // Compute hash + unsigned char sha_out[SHA1_LEN]; + + hashNode(rootNode, sha_out); + + // Sign with private key + unsigned char res[RSA_KEY_SIZE]; + ByteArray deviceKey(device->getDeviceKey(), Device::DEVICE_KEY_SIZE); + std::string pkcs12 = user->getPKCS12(); + ByteArray privateRSAKey = ByteArray::fromBase64(pkcs12); + + client->RSAPrivateEncrypt(privateRSAKey.data(), privateRSAKey.length(), + RSAInterface::RSA_KEY_PKCS12, deviceKey.toBase64().data(), + sha_out, sizeof(sha_out), res); + if (logLevel >= DEBUG) + { + printf("Sig : "); + for(int i=0; i<(int)sizeof(res); i++) + printf("%02x ", res[i]); + printf("\n"); + } + + // Add removed HMAC + appendTextElem(hmacParentNode, hmacNode.name(), hmacNode.first_child().value()); + + // Add base64 encoded signature + ByteArray signature(res, sizeof(res)); + std::string b64Signature = signature.toBase64(); + + appendTextElem(rootNode, "adept:signature", b64Signature); + + pugi::xpath_node node = acsmDoc.select_node("//operatorURL"); + if (!node) + EXCEPTION(FF_NO_OPERATOR_URL, "OperatorURL not found in ACSM document"); + + std::string operatorURL = node.node().first_child().value(); + operatorURL = trim(operatorURL) + "/Fulfill"; + + ByteArray replyData = sendRequest(fulfillReq, operatorURL); + + pugi::xml_document fulfillReply; + + fulfillReply.load_string((const char*)replyData.data()); + + return new FulfillmentItem(fulfillReply, user); + } + + void DRMProcessor::download(FulfillmentItem* item, std::string path) + { + if (!item) + EXCEPTION(DW_NO_ITEM, "No item"); + + ByteArray replyData = sendRequest(item->getDownloadURL()); + + writeFile(path, replyData); + + GOUROU_LOG(INFO, "Download into " << path); + + std::string rightsStr = item->getRights(); + + void* handler = client->zipOpen(path); + client->zipWriteFile(handler, "META-INF/rights.xml", rightsStr); + client->zipClose(handler); + } + + void DRMProcessor::buildSignInRequest(pugi::xml_document& signInRequest, + const std::string& adobeID, const std::string& adobePassword, + const std::string& authenticationCertificate) + { + pugi::xml_node decl = signInRequest.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + pugi::xml_node signIn = signInRequest.append_child("adept:signIn"); + signIn.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS; + signIn.append_attribute("method") = user->getLoginMethod().c_str(); + + unsigned char encryptedSignInData[RSA_KEY_SIZE]; + const unsigned char* deviceKey = device->getDeviceKey(); + + ByteArray _authenticationCertificate = ByteArray::fromBase64(authenticationCertificate); + + // Build buffer + ByteArray ar(deviceKey, Device::DEVICE_KEY_SIZE); + ar.append((unsigned char)adobeID.length()); + ar.append(adobeID); + ar.append((unsigned char)adobePassword.length()); + ar.append(adobePassword); + + // Encrypt with authentication certificate (public part) + client->RSAPublicEncrypt(_authenticationCertificate.data(), + _authenticationCertificate.length(), + RSAInterface::RSA_KEY_X509, + ar.data(), ar.length(), encryptedSignInData); + + ar = ByteArray(encryptedSignInData, sizeof(encryptedSignInData)); + appendTextElem(signIn, "adept:signInData", ar.toBase64()); + + // Generate Auth key and License Key + void* rsaAuth = client->generateRSAKey(RSA_KEY_SIZE_BITS); + void* rsaLicense = client->generateRSAKey(RSA_KEY_SIZE_BITS); + + std::string serializedData = serializeRSAPublicKey(rsaAuth); + appendTextElem(signIn, "adept:publicAuthKey", serializedData); + serializedData = serializeRSAPrivateKey(rsaAuth); + appendTextElem(signIn, "adept:encryptedPrivateAuthKey", serializedData.data()); + + serializedData = serializeRSAPublicKey(rsaLicense); + appendTextElem(signIn, "adept:publicLicenseKey", serializedData.data()); + serializedData = serializeRSAPrivateKey(rsaLicense); + appendTextElem(signIn, "adept:encryptedPrivateLicenseKey", serializedData.data()); + + client->destroyRSAHandler(rsaAuth); + client->destroyRSAHandler(rsaLicense); + } + + void DRMProcessor::signIn(const std::string& adobeID, const std::string& adobePassword) + { + pugi::xml_document signInRequest; + std::string authenticationCertificate = user->getAuthenticationCertificate(); + + buildSignInRequest(signInRequest, adobeID, adobePassword, authenticationCertificate); + + GOUROU_LOG(INFO, "SignIn " << adobeID); + + std::string signInURL = user->getProperty("//adept:authURL"); + signInURL += "/SignInDirect"; + + ByteArray credentials = sendRequest(signInRequest, signInURL); + + pugi::xml_document credentialsDoc; + if (!credentialsDoc.load_buffer(credentials.data(), credentials.length())) + EXCEPTION(SIGN_INVALID_CREDENTIALS, "Invalid credentials reply"); + + struct adeptWalker: pugi::xml_tree_walker + { + void changeName(pugi::xml_node& node) + { + std::string name = std::string("adept:") + node.name(); + node.set_name(name.c_str()); + } + + bool begin(pugi::xml_node& node) + { + changeName(node); + return true; + } + + virtual bool for_each(pugi::xml_node& node) + { + if (node.type() == pugi::node_element) + changeName(node); + return true; // continue traversal + } + } adeptWalker; + + pugi::xml_node credentialsNode = credentialsDoc.first_child(); + + if (std::string(credentialsNode.name()) != "credentials") + EXCEPTION(SIGN_INVALID_CREDENTIALS, "Invalid credentials reply"); + + pugi::xpath_node encryptedPrivateLicenseKey = credentialsNode.select_node("encryptedPrivateLicenseKey"); + const char* privateKeyData = encryptedPrivateLicenseKey.node().first_child().value(); + ByteArray privateKeyDataStr = ByteArray::fromBase64(privateKeyData); + ByteArray privateKey = decryptWithDeviceKey(privateKeyDataStr.data(), privateKeyDataStr.length()); + credentialsNode.remove_child(encryptedPrivateLicenseKey.node()); + appendTextElem(credentialsNode, "privateLicenseKey", privateKey.toBase64().data()); + + // Add "adept:" prefix to all nodes + credentialsNode.remove_attribute("xmlns"); + credentialsNode.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS; + credentialsNode.traverse(adeptWalker); + + appendTextElem(credentialsNode, "adept:authenticationCertificate", authenticationCertificate.data()); + + pugi::xml_document activationDoc; + user->readActivation(activationDoc); + pugi::xml_node activationInfo = activationDoc.select_node("activationInfo").node(); + activationInfo.append_copy(credentialsNode); + + user->updateActivationFile(activationDoc); + } + + void DRMProcessor::buildActivateReq(pugi::xml_document& activateReq) + { + pugi::xml_node decl = activateReq.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + + pugi::xml_node root = activateReq.append_child("adept:activate"); + root.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS; + root.append_attribute("requestType") = "initial"; + + appendTextElem(root, "adept:fingerprint", (*device)["fingerprint"]); + appendTextElem(root, "adept:deviceType", (*device)["deviceType"]); + appendTextElem(root, "adept:clientOS", (*device)["clientOS"]); + appendTextElem(root, "adept:clientLocale", (*device)["clientLocale"]); + appendTextElem(root, "adept:clientVersion", (*device)["deviceClass"]); + + pugi::xml_node targetDevice = root.append_child("adept:targetDevice"); + appendTextElem(targetDevice, "adept:softwareVersion", (*device)["hobbes"]); + appendTextElem(targetDevice, "adept:clientOS", (*device)["clientOS"]); + appendTextElem(targetDevice, "adept:clientLocale", (*device)["clientLocale"]); + appendTextElem(targetDevice, "adept:clientVersion", (*device)["deviceClass"]); + appendTextElem(targetDevice, "adept:deviceType", (*device)["deviceType"]); + appendTextElem(targetDevice, "adept:fingerprint", (*device)["fingerprint"]); + + /* + r4 = tp->time + r3 = 0 + r2 = tm->militime + r0 = 0x6f046000 + r1 = 0x388a + + r3 += high(r4*1000) + r2 += low(r4*1000) + + r0 += r2 + r1 += r3 + */ + struct timeval tv; + gettimeofday(&tv, 0); + uint32_t nonce32[2] = {0x6f046000, 0x388a}; + uint64_t bigtime = tv.tv_sec*1000; + nonce32[0] += (bigtime & 0xFFFFFFFF) + (tv.tv_usec/1000); + nonce32[1] += ((bigtime >> 32) & 0xFFFFFFFF); + + ByteArray nonce((const unsigned char*)&nonce32, sizeof(nonce32)); + uint32_t tmp = 0; + nonce.append((const unsigned char*)&tmp, sizeof(tmp)); + appendTextElem(root, "adept:nonce", nonce.toBase64().data()); + + time_t _time = time(0) + 10*60; // Cur time + 10 minutes + struct tm* tm_info = localtime(&_time); + char buffer[32]; + + strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", tm_info); + appendTextElem(root, "adept:expiration", buffer); + + appendTextElem(root, "adept:user", user->getUUID()); + } + + void DRMProcessor::activateDevice() + { + pugi::xml_document activateReq; + + GOUROU_LOG(INFO, "Activate device"); + + buildActivateReq(activateReq); + + // Compute hash + unsigned char sha_out[SHA1_LEN]; + + pugi::xml_node root = activateReq.select_node("adept:activate").node(); + hashNode(root, sha_out); + + // Sign with private key + ByteArray RSAKey = ByteArray::fromBase64(user->getPKCS12()); + unsigned char res[RSA_KEY_SIZE]; + ByteArray deviceKey(device->getDeviceKey(), Device::DEVICE_KEY_SIZE); + + client->RSAPrivateEncrypt(RSAKey.data(), RSAKey.length(), RSAInterface::RSA_KEY_PKCS12, + deviceKey.toBase64().c_str(), + sha_out, sizeof(sha_out), + res); + + // Add base64 encoded signature + ByteArray signature(res, sizeof(res)); + std::string b64Signature = signature.toBase64(); + + root = activateReq.select_node("adept:activate").node(); + appendTextElem(root, "adept:signature", b64Signature); + + pugi::xml_document activationDoc; + user->readActivation(activationDoc); + + std::string activationURL = user->getProperty("//adept:activationURL"); + activationURL += "/Activate"; + + ByteArray reply = sendRequest(activateReq, activationURL); + + pugi::xml_document activationToken; + activationToken.load_buffer(reply.data(), reply.length()); + + root = activationDoc.select_node("activationInfo").node(); + root.append_copy(activationToken.first_child()); + user->updateActivationFile(activationDoc); + } + + ByteArray DRMProcessor::encryptWithDeviceKey(const unsigned char* data, unsigned int len) + { + const unsigned char* deviceKey = device->getDeviceKey(); + unsigned int outLen; + int remain = 0; + if ((len % 16)) + remain = 16 - (len%16); + int encrypted_data_len = 16 + len + remain; // IV + data + pad + unsigned char* encrypted_data = new unsigned char[encrypted_data_len]; + + // Generate IV in front + client->randBytes(encrypted_data, 16); + + client->AESEncrypt(CryptoInterface::CHAIN_CBC, + deviceKey, 16, encrypted_data, 16, + data, len, + encrypted_data+16, &outLen); + + ByteArray res(encrypted_data, outLen+16); + + delete[] encrypted_data; + + return res; + } + + /* First 16 bytes of data is IV for CBC chaining */ + ByteArray DRMProcessor::decryptWithDeviceKey(const unsigned char* data, unsigned int len) + { + unsigned int outLen; + const unsigned char* deviceKey = device->getDeviceKey(); + unsigned char* decrypted_data = new unsigned char[len-16]; + + client->AESDecrypt(CryptoInterface::CHAIN_CBC, + deviceKey, 16, data, 16, + data+16, len-16, + decrypted_data, &outLen); + + ByteArray res(decrypted_data, outLen); + + delete[] decrypted_data; + + return res; + } + + std::string DRMProcessor::serializeRSAPublicKey(void* rsa) + { + unsigned char* data = 0; + unsigned int len; + + client->extractRSAPublicKey(rsa, &data, &len); + + ByteArray res(data, len); + + free(data); + + return res.toBase64(); + } + + std::string DRMProcessor::serializeRSAPrivateKey(void* rsa) + { + unsigned char* data = 0; + unsigned int len; + + client->extractRSAPrivateKey(rsa, &data, &len); + + ByteArray res = encryptWithDeviceKey(data, len); + + free(data); + + return res.toBase64(); + } + + int DRMProcessor::getLogLevel() {return (int)gourou::logLevel;} + void DRMProcessor::setLogLevel(int logLevel) {gourou::logLevel = (GOUROU_LOG_LEVEL)logLevel;} +} diff --git a/src/pugixml.cpp b/src/pugixml.cpp new file mode 120000 index 0000000..5049a58 --- /dev/null +++ b/src/pugixml.cpp @@ -0,0 +1 @@ +../lib/pugixml/src/pugixml.cpp \ No newline at end of file diff --git a/src/user.cpp b/src/user.cpp new file mode 100644 index 0000000..ccc5019 --- /dev/null +++ b/src/user.cpp @@ -0,0 +1,198 @@ +/* + Copyright 2021 Grégory Soutadé + + This file is part of libgourou. + + libgourou is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libgourou is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libgourou. If not, see . +*/ + +#include +#include +#include +#include + +namespace gourou { + User::User(DRMProcessor* processor):processor(processor) {} + + User::User(DRMProcessor* processor, const std::string& activationFile): + processor(processor), activationFile(activationFile) + { + parseActivationFile(); + } + + void User::parseActivationFile(bool throwOnNull) + { + GOUROU_LOG(DEBUG, "Parse activation file " << activationFile); + + if (!activationDoc.load_file(activationFile.c_str())) + { + if (throwOnNull) + EXCEPTION(USER_INVALID_ACTIVATION_FILE, "Invalid activation file"); + return; + } + + try + { + pkcs12 = gourou::extractTextElem(activationDoc, "//adept:pkcs12", throwOnNull); + uuid = gourou::extractTextElem(activationDoc, "//adept:user", throwOnNull); + deviceUUID = gourou::extractTextElem(activationDoc, "//device", throwOnNull); + deviceFingerprint = gourou::extractTextElem(activationDoc, "//fingerprint", throwOnNull); + certificate = gourou::extractTextElem(activationDoc, "//adept:certificate", throwOnNull); + authenticationCertificate = gourou::extractTextElem(activationDoc, "//adept:authenticationCertificate", throwOnNull); + privateLicenseKey = gourou::extractTextElem(activationDoc, "//adept:privateLicenseKey", throwOnNull); + username = gourou::extractTextElem(activationDoc, "//adept:username", throwOnNull); + + pugi::xpath_node xpath_node = activationDoc.select_node("//adept:username"); + if (xpath_node) + loginMethod = xpath_node.node().attribute("method").value(); + else + { + if (throwOnNull) + EXCEPTION(USER_INVALID_ACTIVATION_FILE, "Invalid activation file"); + } + } + catch(gourou::Exception& e) + { + EXCEPTION(USER_INVALID_ACTIVATION_FILE, "Invalid activation file"); + } + } + + std::string& User::getUUID() { return uuid; } + std::string& User::getPKCS12() { return pkcs12; } + std::string& User::getDeviceUUID() { return deviceUUID; } + std::string& User::getDeviceFingerprint() { return deviceFingerprint; } + std::string& User::getUsername() { return username; } + std::string& User::getLoginMethod() { return loginMethod; } + std::string& User::getCertificate() { return certificate; } + std::string& User::getAuthenticationCertificate() { return authenticationCertificate; } + std::string& User::getPrivateLicenseKey() { return privateLicenseKey; } + + void User::readActivation(pugi::xml_document& doc) + { + if (!doc.load_file(activationFile.c_str())) + EXCEPTION(USER_INVALID_ACTIVATION_FILE, "Invalid activation file"); + } + + void User::updateActivationFile(const char* data) + { + GOUROU_LOG(INFO, "Update Activation file : " << std::endl << data); + + writeFile(activationFile, (unsigned char*)data, strlen(data)); + + parseActivationFile(false); + } + + void User::updateActivationFile(const pugi::xml_document& doc) + { + StringXMLWriter xmlWriter; + doc.save(xmlWriter, " "); + updateActivationFile(xmlWriter.getResult().c_str()); + } + + std::string User::getProperty(const std::string property) + { + pugi::xpath_node xpathRes = activationDoc.select_node(property.c_str()); + if (!xpathRes) + EXCEPTION(USER_NO_PROPERTY, "Property " << property << " not found in activation.xml"); + + std::string res = xpathRes.node().first_child().value(); + return trim(res); + } + + User* User::createUser(DRMProcessor* processor, const std::string& dirName, const std::string& ACSServer) + { + struct stat _stat; + + if (stat(dirName.c_str(), &_stat) != 0) + { + if (mkdir_p(dirName.c_str(), S_IRWXU)) + EXCEPTION(USER_MKPATH, "Unable to create " << dirName) + } + + User* user = new User(processor); + bool doUpdate = false; + + user->activationFile = dirName + "/activation.xml"; + user->parseActivationFile(false); + + pugi::xpath_node nodeActivationInfo = user->activationDoc.select_node("activation_info"); + pugi::xpath_node nodeActivationServiceInfo = nodeActivationInfo.node().select_node("adept:activationServiceInfo"); + pugi::xml_node activationInfo; + pugi::xml_node activationServiceInfo; + + if (nodeActivationInfo && nodeActivationServiceInfo) + { + GOUROU_LOG(DEBUG, "Read previous activation configuration"); + activationInfo = nodeActivationInfo.node(); + activationServiceInfo = nodeActivationServiceInfo.node(); + } + else + { + GOUROU_LOG(DEBUG, "Create new activation"); + + user->activationDoc.reset(); + + pugi::xml_node decl = user->activationDoc.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + activationInfo = user->activationDoc.append_child("activationInfo"); + activationInfo.append_attribute("xmlns") = ADOBE_ADEPT_NS; + activationServiceInfo = activationInfo.append_child("adept:activationServiceInfo"); + activationServiceInfo.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS; + + // Go to activation Service Info + std::string activationURL = ACSServer + "/ActivationServiceInfo"; + ByteArray activationServiceInfoReply = processor->sendRequest(activationURL); + pugi::xml_document docActivationServiceInfo; + docActivationServiceInfo.load_buffer(activationServiceInfoReply.data(), + activationServiceInfoReply.length()); + + pugi::xpath_node path = docActivationServiceInfo.select_node("//authURL"); + appendTextElem(activationServiceInfo, "adept:authURL", path.node().first_child().value()); + path = docActivationServiceInfo.select_node("//userInfoURL"); + appendTextElem(activationServiceInfo, "adept:userInfoURL", path.node().first_child().value()); + appendTextElem(activationServiceInfo, "adept:activationURL", ACSServer); + path = docActivationServiceInfo.select_node("//certificate"); + appendTextElem(activationServiceInfo, "adept:certificate", path.node().first_child().value()); + doUpdate = true; + } + + pugi::xpath_node nodeAuthenticationCertificate = activationServiceInfo.select_node("adept:authenticationCertificate"); + + if (!nodeAuthenticationCertificate) + { + GOUROU_LOG(DEBUG, "Create new activation, authentication part"); + + pugi::xpath_node xpathRes = activationServiceInfo.select_node("adept:authURL"); + if (!xpathRes) + EXCEPTION(USER_NO_AUTHENTICATION_URL, "No authentication URL"); + + std::string authenticationURL = xpathRes.node().first_child().value(); + authenticationURL = trim(authenticationURL) + "/AuthenticationServiceInfo"; + + // Go to authentication Service Info + ByteArray authenticationServiceInfo = processor->sendRequest(authenticationURL); + pugi::xml_document docAuthenticationServiceInfo; + docAuthenticationServiceInfo.load_buffer(authenticationServiceInfo.data(), authenticationServiceInfo.length()); + pugi::xpath_node path = docAuthenticationServiceInfo.select_node("//certificate"); + appendTextElem(activationServiceInfo, "adept:authenticationCertificate", path.node().first_child().value()); + doUpdate = true; + } + + if (doUpdate) + user->updateActivationFile(user->activationDoc); + + + return user; + } +} diff --git a/utils/LICENSE b/utils/LICENSE new file mode 100644 index 0000000..fd17007 --- /dev/null +++ b/utils/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2021, Grégory Soutadé + +All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/utils/Makefile b/utils/Makefile new file mode 100644 index 0000000..14c7c4a --- /dev/null +++ b/utils/Makefile @@ -0,0 +1,24 @@ + +TARGETS=acsmdownloader activate + +CXXFLAGS=-Wall `pkg-config --cflags Qt5Core Qt5Network` -fPIC -I$(ROOT)/include -I$(ROOT)/lib/pugixml/src/ +LDFLAGS=`pkg-config --libs Qt5Core Qt5Network` -L$(ROOT) -lgourou -lcrypto -lzip + +ifneq ($(DEBUG),) +CXXFLAGS += -ggdb -O0 +else +CXXFLAGS += -O2 +endif + +all: $(TARGETS) + +acsmdownloader: drmprocessorclientimpl.cpp acsmdownloader.cpp + $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ + +activate: drmprocessorclientimpl.cpp activate.cpp + $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ + +clean: + rm -f $(TARGETS) + +ultraclean: clean diff --git a/utils/acsmdownloader.cpp b/utils/acsmdownloader.cpp new file mode 100644 index 0000000..3c7a608 --- /dev/null +++ b/utils/acsmdownloader.cpp @@ -0,0 +1,255 @@ +/* + Copyright (c) 2021, Grégory Soutadé + + All rights reserved. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include "drmprocessorclientimpl.h" + +#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0])) + +static const char* deviceFile = "device.xml"; +static const char* activationFile = "activation.xml"; +static const char* devicekeyFile = "devicesalt"; +static const char* acsmFile = 0; +static const char* outputFile = 0; +static const char* outputDir = 0; +static const char* defaultDirs[] = { + ".adept/", + "./adobe-digital-editions/", + "./.adobe-digital-editions/" +}; + + +class ACSMDownloader: public QRunnable +{ +public: + ACSMDownloader(QCoreApplication* app): + app(app) + { + setAutoDelete(false); + } + + void run() + { + try + { + DRMProcessorClientImpl client; + gourou::DRMProcessor processor(&client, deviceFile, activationFile, devicekeyFile); + + gourou::FulfillmentItem* item = processor.fulfill(acsmFile); + + std::string filename; + if (!outputFile) + { + filename = item->getMetadata("title"); + if (filename == "") + filename = "output.epub"; + else + filename += ".epub"; + } + + if (outputDir) + { + QDir dir(outputDir); + if (!dir.exists(outputDir)) + dir.mkpath(outputDir); + + filename = std::string(outputDir) + "/" + filename; + } + + processor.download(item, filename); + std::cout << "Created " << filename << std::endl; + } catch(std::exception& e) + { + std::cout << e.what() << std::endl; + this->app->exit(1); + } + + this->app->exit(0); + } + +private: + QCoreApplication* app; +}; + +static const char* findFile(const char* filename, bool inDefaultDirs=true) +{ + QFile file(filename); + + if (file.exists()) + return strdup(filename); + + if (!inDefaultDirs) return 0; + + for (int i=0; i<(int)ARRAY_SIZE(defaultDirs); i++) + { + QString path = QString(defaultDirs[i]) + QString(filename); + file.setFileName(path); + if (file.exists()) + return strdup(path.toStdString().c_str()); + } + + return 0; +} + +static void usage(const char* cmd) +{ + std::cout << "Download EPUB file from ACSM request file" << std::endl; + + std::cout << "Usage: " << cmd << " [(-d|--device-file) device.xml] [(-a|--activation-file) activation.xml] [(-s|--device-key-file) devicesalt] [(-O|--output-dir) dir] [(-o|--output-file) output.epub] [(-v|--verbose)] [(-h|--help)] (-f|--acsm-file) file.acsm" << std::endl << std::endl; + + std::cout << " " << "-d|--device-file" << "\t" << "device.xml file from eReader" << std::endl; + std::cout << " " << "-a|--activation-file" << "\t" << "activation.xml file from eReader" << std::endl; + std::cout << " " << "-k|--device-key-file" << "\t" << "private device key file (eg devicesalt/devkey.bin) from eReader" << std::endl; + std::cout << " " << "-O|--output-dir" << "\t" << "Optional output directory were to put result (default ./)" << std::endl; + std::cout << " " << "-o|--output-file" << "\t" << "Optional output epub filename (default )" << std::endl; + std::cout << " " << "-f|--acsm-file" << "\t" << "ACSM request file for epub download" << std::endl; + std::cout << " " << "-v|--verbose" << "\t\t" << "Increase verbosity, can be set multiple times" << std::endl; + std::cout << " " << "-h|--help" << "\t\t" << "This help" << std::endl; + + std::cout << std::endl; + std::cout << "Device file, activation file and device key file are optionals. If not set, they are looked into :" << std::endl; + std::cout << " * Current directory" << std::endl; + std::cout << " * .adept" << std::endl; + std::cout << " * adobe-digital-editions directory" << std::endl; + std::cout << " * .adobe-digital-editions directory" << std::endl; +} + +int main(int argc, char** argv) +{ + int c, ret = -1; + + const char** files[] = {&devicekeyFile, &deviceFile, &activationFile}; + int verbose = gourou::DRMProcessor::getLogLevel(); + + while (1) { + int option_index = 0; + static struct option long_options[] = { + {"device-file", required_argument, 0, 'd' }, + {"activation-file", required_argument, 0, 'a' }, + {"device-key-file", required_argument, 0, 'k' }, + {"output-dir", required_argument, 0, 'O' }, + {"output-file", required_argument, 0, 'o' }, + {"acsm-file", required_argument, 0, 'f' }, + {"verbose", no_argument, 0, 'v' }, + {"help", no_argument, 0, 'h' }, + {0, 0, 0, 0 } + }; + + c = getopt_long(argc, argv, "d:a:k:O:o:f:vh", + long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'd': + deviceFile = optarg; + break; + case 'a': + activationFile = optarg; + break; + case 'k': + devicekeyFile = optarg; + break; + case 'f': + acsmFile = optarg; + break; + case 'O': + outputDir = optarg; + break; + case 'o': + outputFile = optarg; + break; + case 'v': + verbose++; + break; + case 'h': + usage(argv[0]); + return 0; + break; + default: + usage(argv[0]); + return -1; + } + } + + gourou::DRMProcessor::setLogLevel(verbose); + + if (!acsmFile || (outputDir && !outputDir[0]) || + (outputFile && !outputFile[0])) + { + usage(argv[0]); + return -1; + } + + QCoreApplication app(argc, argv); + ACSMDownloader downloader(&app); + + int i; + for (i=0; i<(int)ARRAY_SIZE(files); i++) + { + *files[i] = findFile(*files[i]); + if (!*files[i]) + { + std::cout << "Error : " << *files[i] << " doesn't exists" << std::endl; + ret = -1; + goto end; + } + } + + QFile file(acsmFile); + if (!file.exists()) + { + std::cout << "Error : " << acsmFile << " doesn't exists" << std::endl; + ret = -1; + goto end; + } + + QThreadPool::globalInstance()->start(&downloader); + + ret = app.exec(); + +end: + for (i=0; i<(int)ARRAY_SIZE(files); i++) + { + if (*files[i]) + free((void*)*files[i]); + } + + return ret; +} diff --git a/utils/activate.cpp b/utils/activate.cpp new file mode 100644 index 0000000..c089ec1 --- /dev/null +++ b/utils/activate.cpp @@ -0,0 +1,263 @@ +/* + Copyright (c) 2021, Grégory Soutadé + + All rights reserved. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include "drmprocessorclientimpl.h" + +#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0])) + +static const char* username = 0; +static const char* password = 0; +static const char* outputDir = 0; +static const char* hobbesVersion = HOBBES_DEFAULT_VERSION; +static bool randomSerial = false; + +// From http://www.cplusplus.com/articles/E6vU7k9E/ +static int getch() { + int ch; + struct termios t_old, t_new; + + tcgetattr(STDIN_FILENO, &t_old); + t_new = t_old; + t_new.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &t_new); + + ch = getchar(); + + tcsetattr(STDIN_FILENO, TCSANOW, &t_old); + return ch; +} + +static std::string getpass(const char *prompt, bool show_asterisk=false) +{ + const char BACKSPACE=127; + const char RETURN=10; + + std::string password; + unsigned char ch=0; + + std::cout <signIn(username, password); + processor->activateDevice(); + + std::cout << username << " fully signed and device activated in " << outputDir << std::endl; + } catch(std::exception& e) + { + std::cout << e.what() << std::endl; + this->app->exit(1); + } + + this->app->exit(0); + } + +private: + QCoreApplication* app; +}; + +static void usage(const char* cmd) +{ + std::cout << "Create new device files used by ADEPT DRM" << std::endl; + + std::cout << "Usage: " << cmd << " (-u|--username) username [(-p|--password) password] [(-O|--output-dir) dir] [(-r|--random-serial)] [(-v|--verbose)] [(-h|--help)]" << std::endl << std::endl; + + std::cout << " " << "-u|--username" << "\t\t" << "AdobeID username (ie adobe.com email account)" << std::endl; + std::cout << " " << "-p|--password" << "\t\t" << "AdobeID password (asked if not set via command line) " << std::endl; + std::cout << " " << "-O|--output-dir" << "\t" << "Optional output directory were to put result (default ./.adept). This directory must not already exists" << std::endl; + std::cout << " " << "-H|--hobbes-version" << "\t"<< "Force RMSDK version to a specific value (default: version of current librmsdk)" << std::endl; + std::cout << " " << "-r|--random-serial" << "\t"<< "Generate a random device serial (if not set, it will be dependent of your current configuration)" << std::endl; + std::cout << " " << "-v|--verbose" << "\t\t" << "Increase verbosity, can be set multiple times" << std::endl; + std::cout << " " << "-h|--help" << "\t\t" << "This help" << std::endl; + + std::cout << std::endl; +} + +static const char* abspath(const char* filename) +{ + const char* root = getcwd(0, PATH_MAX); + QString fullPath = QString(root) + QString("/") + QString(filename); + const char* res = strdup(fullPath.toStdString().c_str()); + + free((void*)root); + + return res; +} + +int main(int argc, char** argv) +{ + int c, ret = -1; + const char* _outputDir = outputDir; + int verbose = gourou::DRMProcessor::getLogLevel(); + + while (1) { + int option_index = 0; + static struct option long_options[] = { + {"username", required_argument, 0, 'u' }, + {"password", required_argument, 0, 'p' }, + {"output-dir", required_argument, 0, 'O' }, + {"hobbes-version",required_argument, 0, 'H' }, + {"random-serial", no_argument, 0, 'r' }, + {"verbose", no_argument, 0, 'v' }, + {"help", no_argument, 0, 'h' }, + {0, 0, 0, 0 } + }; + + c = getopt_long(argc, argv, "u:p:O:H:rvh", + long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'u': + username = optarg; + break; + case 'p': + password = optarg; + break; + case 'O': + _outputDir = optarg; + break; + case 'H': + hobbesVersion = optarg; + break; + case 'v': + verbose++; + break; + case 'h': + usage(argv[0]); + return 0; + break; + case 'r': + randomSerial = true; + break; + default: + usage(argv[0]); + return -1; + } + } + + gourou::DRMProcessor::setLogLevel(verbose); + + if (!username) + { + usage(argv[0]); + return -1; + } + + if (!_outputDir || _outputDir[0] == 0) + { + outputDir = abspath(DEFAULT_ADEPT_DIR); + } + else + { + // Relative path + if (_outputDir[0] == '.' || _outputDir[0] != '/') + { + QFile file(_outputDir); + // realpath doesn't works if file/dir doesn't exists + if (file.exists()) + outputDir = realpath(_outputDir, 0); + else + outputDir = abspath(_outputDir); + } + else + outputDir = strdup(_outputDir); + } + + if (!password) + { + char prompt[128]; + std::snprintf(prompt, sizeof(prompt), "Enter password for <%s> : ", username); + std::string pass = getpass((const char*)prompt, false); + password = pass.c_str(); + } + + QCoreApplication app(argc, argv); + + Activate activate(&app); + QThreadPool::globalInstance()->start(&activate); + + ret = app.exec(); + + free((void*)outputDir); + return ret; +} diff --git a/utils/drmprocessorclientimpl.cpp b/utils/drmprocessorclientimpl.cpp new file mode 100644 index 0000000..b47568c --- /dev/null +++ b/utils/drmprocessorclientimpl.cpp @@ -0,0 +1,385 @@ +/* + Copyright (c) 2021, Grégory Soutadé + + All rights reserved. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include "drmprocessorclientimpl.h" + +/* Digest interface */ +void* DRMProcessorClientImpl::createDigest(const std::string& digestName) +{ + EVP_MD_CTX *sha_ctx = EVP_MD_CTX_new(); + const EVP_MD* md = EVP_get_digestbyname(digestName.c_str()); + EVP_DigestInit(sha_ctx, md); + + return sha_ctx; +} + +int DRMProcessorClientImpl::digestUpdate(void* handler, unsigned char* data, unsigned int length) +{ + return EVP_DigestUpdate((EVP_MD_CTX *)handler, data, length); +} + +int DRMProcessorClientImpl::digestFinalize(void* handler, unsigned char* digestOut) +{ + int res = EVP_DigestFinal((EVP_MD_CTX *)handler, digestOut, NULL); + EVP_MD_CTX_free((EVP_MD_CTX *)handler); + return res; +} + +int DRMProcessorClientImpl::digest(const std::string& digestName, unsigned char* data, unsigned int length, unsigned char* digestOut) +{ + void* handler = createDigest(digestName); + if (!handler) + return -1; + if (digestUpdate(handler, data, length)) + return -1; + return digestFinalize(handler, digestOut); +} + +/* Random interface */ +void DRMProcessorClientImpl::randBytes(unsigned char* bytesOut, unsigned int length) +{ + RAND_bytes(bytesOut, length); +} + +/* HTTP interface */ +std::string DRMProcessorClientImpl::sendHTTPRequest(const std::string& URL, const std::string& POSTData, const std::string& contentType) +{ + QNetworkRequest request(QUrl(URL.c_str())); + QNetworkAccessManager networkManager; + QByteArray replyData; + + GOUROU_LOG(gourou::INFO, "Send request to " << URL); + if (POSTData.size()) + { + GOUROU_LOG(gourou::DEBUG, "<<< " << std::endl << POSTData); + } + + request.setRawHeader("Accept", "*/*"); + request.setRawHeader("User-Agent", "book2png"); + if (contentType.size()) + request.setRawHeader("Content-Type", contentType.c_str()); + + QNetworkReply* reply; + + if (POSTData.size()) + reply = networkManager.post(request, POSTData.c_str()); + else + reply = networkManager.get(request); + + QCoreApplication* app = QCoreApplication::instance(); + networkManager.moveToThread(app->thread()); + while (!reply->isFinished()) + app->processEvents(); + + replyData = reply->readAll(); + if (reply->rawHeader("Content-Type") == "application/vnd.adobe.adept+xml") + { + GOUROU_LOG(gourou::DEBUG, ">>> " << std::endl << replyData.data()); + } + + return std::string(replyData.data(), replyData.length()); +} + +void DRMProcessorClientImpl::RSAPrivateEncrypt(const unsigned char* RSAKey, unsigned int RSAKeyLength, + const RSA_KEY_TYPE keyType, const std::string& password, + const unsigned char* data, unsigned dataLength, + unsigned char* res) +{ + PKCS12 * pkcs12; + EVP_PKEY* pkey; + X509* cert; + STACK_OF(X509)* ca; + RSA * rsa; + + pkcs12 = d2i_PKCS12(NULL, &RSAKey, RSAKeyLength); + if (!pkcs12) + EXCEPTION(gourou::CLIENT_INVALID_PKCS12, ERR_error_string(ERR_get_error(), NULL)); + PKCS12_parse(pkcs12, password.c_str(), &pkey, &cert, &ca); + rsa = EVP_PKEY_get1_RSA(pkey); + + int ret = RSA_private_encrypt(dataLength, data, res, rsa, RSA_PKCS1_PADDING); + + if (ret < 0) + EXCEPTION(gourou::CLIENT_RSA_ERROR, ERR_error_string(ERR_get_error(), NULL)); + + if (gourou::logLevel >= gourou::DEBUG) + { + printf("Sig : "); + for(int i=0; i<(int)sizeof(res); i++) + printf("%02x ", res[i]); + printf("\n"); + } +} + +void DRMProcessorClientImpl::RSAPublicEncrypt(const unsigned char* RSAKey, unsigned int RSAKeyLength, + const RSA_KEY_TYPE keyType, + const unsigned char* data, unsigned dataLength, + unsigned char* res) +{ + X509 * x509 = d2i_X509(0, &RSAKey, RSAKeyLength); + if (!x509) + EXCEPTION(gourou::CLIENT_INVALID_CERTIFICATE, "Invalid certificate"); + + EVP_PKEY * evpKey = X509_get_pubkey(x509); + RSA* rsa = EVP_PKEY_get1_RSA(evpKey); + EVP_PKEY_free(evpKey); + + if (!rsa) + EXCEPTION(gourou::CLIENT_NO_PRIV_KEY, "No private key in certificate"); + + int ret = RSA_public_encrypt(dataLength, data, res, rsa, RSA_PKCS1_PADDING); + if (ret < 0) + EXCEPTION(gourou::CLIENT_RSA_ERROR, ERR_error_string(ERR_get_error(), NULL)); +} + +void* DRMProcessorClientImpl::generateRSAKey(int keyLengthBits) +{ + BIGNUM * bn = BN_new(); + RSA * rsa = RSA_new(); + BN_set_word(bn, 0x10001); + RSA_generate_key_ex(rsa, keyLengthBits, bn, 0); + BN_free(bn); + + return rsa; +} + +void DRMProcessorClientImpl::destroyRSAHandler(void* handler) +{ + RSA_free((RSA*)handler); +} + +void DRMProcessorClientImpl::extractRSAPublicKey(void* handler, unsigned char** keyOut, unsigned int* keyOutLength) +{ + EVP_PKEY * evpKey = EVP_PKEY_new(); + EVP_PKEY_set1_RSA(evpKey, (RSA*)handler); + X509_PUBKEY *x509_pubkey = 0; + X509_PUBKEY_set(&x509_pubkey, evpKey); + + *keyOutLength = i2d_X509_PUBKEY(x509_pubkey, keyOut); + + X509_PUBKEY_free(x509_pubkey); + EVP_PKEY_free(evpKey); +} + +void DRMProcessorClientImpl::extractRSAPrivateKey(void* handler, unsigned char** keyOut, unsigned int* keyOutLength) +{ + EVP_PKEY * evpKey = EVP_PKEY_new(); + EVP_PKEY_set1_RSA(evpKey, (RSA*)handler); + PKCS8_PRIV_KEY_INFO * privKey = EVP_PKEY2PKCS8(evpKey); + + *keyOutLength = i2d_PKCS8_PRIV_KEY_INFO(privKey, keyOut); + + PKCS8_PRIV_KEY_INFO_free(privKey); + EVP_PKEY_free(evpKey); +} + +/* Crypto interface */ +void DRMProcessorClientImpl::AESEncrypt(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength, + const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) +{ + void* handler = AESEncryptInit(chaining, key, keyLength, iv, ivLength); + AESEncryptUpdate(handler, dataIn, dataInLength, dataOut, dataOutLength); + AESEncryptFinalize(handler, dataOut+*dataOutLength, dataOutLength); +} + +void* DRMProcessorClientImpl::AESEncryptInit(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength) +{ + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + + switch(keyLength) + { + case 16: + switch(chaining) + { + case CHAIN_ECB: + EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, iv); + break; + case CHAIN_CBC: + EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv); + break; + default: + EXCEPTION(gourou::CLIENT_BAD_CHAINING, "Unknown chaining mode " << chaining); + break; + } + default: + EVP_CIPHER_CTX_free(ctx); + EXCEPTION(gourou::CLIENT_BAD_KEY_SIZE, "Invalid key size " << keyLength); + } + + return ctx; +} + +void* DRMProcessorClientImpl::AESDecryptInit(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength) +{ + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + + switch(keyLength) + { + case 16: + switch(chaining) + { + case CHAIN_ECB: + EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, iv); + break; + case CHAIN_CBC: + EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv); + break; + default: + EXCEPTION(gourou::CLIENT_BAD_CHAINING, "Unknown chaining mode " << chaining); + } + break; + default: + EVP_CIPHER_CTX_free(ctx); + EXCEPTION(gourou::CLIENT_BAD_KEY_SIZE, "Invalid key size " << keyLength); + } + + return ctx; +} + +void DRMProcessorClientImpl::AESEncryptUpdate(void* handler, const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) +{ + EVP_EncryptUpdate((EVP_CIPHER_CTX*)handler, dataOut, (int*)dataOutLength, dataIn, dataInLength); +} + +void DRMProcessorClientImpl::AESEncryptFinalize(void* handler, + unsigned char* dataOut, unsigned int* dataOutLength) +{ + int len; + EVP_EncryptFinal_ex((EVP_CIPHER_CTX*)handler, dataOut, &len); + *dataOutLength += len; + EVP_CIPHER_CTX_free((EVP_CIPHER_CTX*)handler); +} + +void DRMProcessorClientImpl::AESDecrypt(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength, + const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) +{ + void* handler = AESDecryptInit(chaining, key, keyLength, iv, ivLength); + AESDecryptUpdate(handler, dataIn, dataInLength, dataOut, dataOutLength); + AESDecryptFinalize(handler, dataOut+*dataOutLength, dataOutLength); +} + +void DRMProcessorClientImpl::AESDecryptUpdate(void* handler, const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength) +{ + EVP_DecryptUpdate((EVP_CIPHER_CTX*)handler, dataOut, (int*)dataOutLength, dataIn, dataInLength); +} + +void DRMProcessorClientImpl::AESDecryptFinalize(void* handler, unsigned char* dataOut, unsigned int* dataOutLength) +{ + int len; + EVP_DecryptFinal_ex((EVP_CIPHER_CTX*)handler, dataOut, &len); + *dataOutLength += len; + EVP_CIPHER_CTX_free((EVP_CIPHER_CTX*)handler); +} + +void* DRMProcessorClientImpl::zipOpen(const std::string& path) +{ + zip_t* handler = zip_open(path.c_str(), 0, 0); + + if (!handler) + EXCEPTION(gourou::CLIENT_BAD_ZIP_FILE, "Invalid zip file " << path); + + return handler; +} + +std::string DRMProcessorClientImpl::zipReadFile(void* handler, const std::string& path) +{ + std::string res; + unsigned char* buffer; + zip_stat_t sb; + + if (zip_stat((zip_t *)handler, path.c_str(), 0, &sb) < 0) + EXCEPTION(gourou::CLIENT_ZIP_ERROR, "Zip error " << zip_strerror((zip_t *)handler)); + + if (!(sb.valid & (ZIP_STAT_INDEX|ZIP_STAT_SIZE))) + EXCEPTION(gourou::CLIENT_ZIP_ERROR, "Required fields missing"); + + buffer = new unsigned char[sb.size]; + + zip_file_t *f = zip_fopen_index((zip_t *)handler, sb.index, ZIP_FL_COMPRESSED); + + zip_fread(f, buffer, sb.size); + zip_fclose(f); + + res = std::string((char*)buffer, sb.size); + delete[] buffer; + + return res; +} + +void DRMProcessorClientImpl::zipWriteFile(void* handler, const std::string& path, const std::string& content) +{ + zip_source_t* s = zip_source_buffer((zip_t*)handler, content.c_str(), content.length(), 0); + if (zip_file_add((zip_t*)handler, path.c_str(), s, ZIP_FL_OVERWRITE|ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(s); + EXCEPTION(gourou::CLIENT_ZIP_ERROR, "Zip error " << zip_strerror((zip_t *)handler)); + } +} + +void DRMProcessorClientImpl::zipDeleteFile(void* handler, const std::string& path) +{ + zip_int64_t idx = zip_name_locate((zip_t*)handler, path.c_str(), 0); + + if (idx < 0) + EXCEPTION(gourou::CLIENT_ZIP_ERROR, "No such file " << path.c_str()); + + if (zip_delete((zip_t*)handler, idx)) + EXCEPTION(gourou::CLIENT_ZIP_ERROR, "Zip error " << zip_strerror((zip_t *)handler)); +} + +void DRMProcessorClientImpl::zipClose(void* handler) +{ + zip_close((zip_t*)handler); +} diff --git a/utils/drmprocessorclientimpl.h b/utils/drmprocessorclientimpl.h new file mode 100644 index 0000000..3eca8bc --- /dev/null +++ b/utils/drmprocessorclientimpl.h @@ -0,0 +1,110 @@ +/* + Copyright (c) 2021, Grégory Soutadé + + All rights reserved. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef _DRMPROCESSORCLIENTIMPL_H_ +#define _DRMPROCESSORCLIENTIMPL_H_ + +#include + +#include + +class DRMProcessorClientImpl : public gourou::DRMProcessorClient +{ + public: + /* Digest interface */ + virtual void* createDigest(const std::string& digestName); + virtual int digestUpdate(void* handler, unsigned char* data, unsigned int length); + virtual int digestFinalize(void* handler,unsigned char* digestOut); + virtual int digest(const std::string& digestName, unsigned char* data, unsigned int length, unsigned char* digestOut); + + /* Random interface */ + virtual void randBytes(unsigned char* bytesOut, unsigned int length); + + /* HTTP interface */ + virtual std::string sendHTTPRequest(const std::string& URL, const std::string& POSTData=std::string(""), const std::string& contentType=std::string("")); + + virtual void RSAPrivateEncrypt(const unsigned char* RSAKey, unsigned int RSAKeyLength, + const RSA_KEY_TYPE keyType, const std::string& password, + const unsigned char* data, unsigned dataLength, + unsigned char* res); + + virtual void RSAPublicEncrypt(const unsigned char* RSAKey, unsigned int RSAKeyLength, + const RSA_KEY_TYPE keyType, + const unsigned char* data, unsigned dataLength, + unsigned char* res); + + virtual void* generateRSAKey(int keyLengthBits); + virtual void destroyRSAHandler(void* handler); + + virtual void extractRSAPublicKey(void* RSAKeyHandler, unsigned char** keyOut, unsigned int* keyOutLength); + virtual void extractRSAPrivateKey(void* RSAKeyHandler, unsigned char** keyOut, unsigned int* keyOutLength); + + /* Crypto interface */ + virtual void AESEncrypt(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength, + const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength); + + virtual void* AESEncryptInit(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv=0, unsigned int ivLength=0); + + + virtual void AESEncryptUpdate(void* handler, const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength); + virtual void AESEncryptFinalize(void* handler, unsigned char* dataOut, unsigned int* dataOutLength); + + virtual void AESDecrypt(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv, unsigned int ivLength, + const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength); + + virtual void* AESDecryptInit(CHAINING_MODE chaining, + const unsigned char* key, unsigned int keyLength, + const unsigned char* iv=0, unsigned int ivLength=0); + + virtual void AESDecryptUpdate(void* handler, const unsigned char* dataIn, unsigned int dataInLength, + unsigned char* dataOut, unsigned int* dataOutLength); + virtual void AESDecryptFinalize(void* handler, unsigned char* dataOut, unsigned int* dataOutLength); + + /* ZIP Interface */ + virtual void* zipOpen(const std::string& path); + + virtual std::string zipReadFile(void* handler, const std::string& path); + + virtual void zipWriteFile(void* handler, const std::string& path, const std::string& content); + + virtual void zipDeleteFile(void* handler, const std::string& path); + + virtual void zipClose(void* handler); + +}; + +#endif