PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Fragen zu C++: Design und Callbacks


Locutus2002
2016-02-11, 11:20:45
Hallo,

Gleich vorweg: ich betrachte mich in C++ als fortgeschrittenen Amateur. Viele Konzepte wie Templates habe ich mal gesehen, aber noch nie selbst angewendet. Außerdem gibt es immer mal wieder Phasen, wo ich es monatelang gar nicht verwende.

Ich beschäftige mich zu Zeit beruflich mit Verschlüsselungsalgorithmen und habe angefangen den Rijndael-Algorithmus selbst zu implementieren. Dafür habe ich Qt 5.5.1 und den aktuellen QT Creator benutzt und ein Testrahmenprogramm zur Textverschlüsselung erstellt, das neben den Standardeinstellungen für Block- und Schlüssellänge noch diverse Blickchiffren-Operationsmodi (ECB, CBC, P-CBC, CFR, OFR und CTR) und Padding-Methoden (u.a. Zero Padding und PKCS #7) bietet.

Um später auch andere Algorithmen Blockchiffre-Methoden einbauen zu können, habe ich eine Basisklasse "BlockCipher" erstellt und davon die Klasse "Rijndael" abgeleitet. In "BlockCipher" sind die grundsätzlichen Member für Blockgröße, Schlüssellänge, Schlüssel, Klartext und verschlüsseltem Text enthalten, sowie Funktionen für die Operationsmodi (als Verschlüsselungskern dient hier eine leere, aber virtuelle Funktion). In der Klasse "Rijndael" befinden sich dann nur die hierfür notwendigen zusätzlichen Variablen für den expandierten Schlüssel und die je nach Blockgröße variierenden Verschiebungen, sowie der Rijndael-Algorithmus selbst als Verschlüsselungskern zuzüglich seiner Unterfunktionen (AddKey, ShiftRows usw.).

Soweit so gut. So ziemlich alles funktioniert fehlerfrei, d.h. der Algorithmus wurde mit verschiedenen Blockgrößen, Schlüsseln, Schlüssellängen anhand im Netz zu Hauf zu findender Testvektoren durchgetestet.

Nur eine Sache führt noch zuverlässig zum Absturz: der Versuch einen verschlüsselten Text zu entschlüsseln, der aber zu kurz ist. Die ECB- und CBC-Modi verlangen, dass die Länge des verschlüsselten Textes ein Vielfaches der Blockgröße ist (Klartexte werden mit Padding entsprechend aufgefüllt, um das sicherzustellen).

Dazu jetzt die Design-Frage: Ich möchte meine BlockCipher- und Rijndael-Klassen portabel halten, um sie auch an jemand weitergeben zu können, der vielleicht nicht mit Qt, sondern z.B. mit Visual Studio entwickelt oder gar nur ein Konsolenprogramm schreibt. Daher sollte es keine Abhängigkeiten zum Rahmenprogramm und/oder Framework geben.
Sollte meine Klasse dann bereits in der entsprechenden ECB-/CBC-Entschlüsselungs-Funktion (die den entschlüsselten Text in den Klartext-Member schreibt) diesen Fehler abfangen? Woran kann man dann ein Rahmenprogramm erkennen, dass etwas schief lief die Entschlüsselung nicht funktioniert/funktionieren kann? Insbesondere wenn die Decodierungsfunktion eine Referenz auf das Objekt selbst zurückgibt? Oder sollte ich das Abfangen solcher Problematiken dem Rahmenprogramm und somit auch den späteren Weiterverwendern der Klasse überlassen (letzteres fände ich eher unschön, da es meiner Meinung nach das Black-Box-Prinzip verletzt).
Falls sich jemand mit Verschlüsselung auskennt: Gibt es hierfür eine Standardvorgehensweise?

Zweite Frage: Wie baue ich in die De-/Codierungsfunktion eine beliebige Callbackfunktion (z.B. eine reine Textausgabefunktion bei Konsolenprogrammen oder aber die Memberfunktion zur Fortschrittsbalkengenerierung der QProgressBar-Klasse in Qt) zur Fortschrittsanzeige ein? Der klassische C-Weg mit Funktionszeigern, den ich mal gelernt habe, hat sich als unbrauchbar erwiesen (wg. notwendiger Angabe einer konkreten Klasse).

Monger
2016-02-11, 11:43:24
Sollte meine Klasse dann bereits in der entsprechenden ECB-/CBC-Entschlüsselungs-Funktion (die den entschlüsselten Text in den Klartext-Member schreibt) diesen Fehler abfangen?

Gegenfrage: kannst du denn garantieren, dass alle Ableitungen deiner Basisklasse genau dieses Verhalten benötigen? Also dass alle Untertypen immer eine Textlänge von einem Vielfachen der Blocklänge benötigen?

Woran kann man dann ein Rahmenprogramm erkennen, dass etwas schief lief?

Laufzeitprobleme entdeckt man halt erst zur Laufzeit. Deshalb sind Unit Tests so essentiell.

Insbesondere wenn die Decodierungsfunktion eine Referenz auf das Objekt selbst zurückgibt?

Das hört sich... interessant an. Warum genau willst du die Objektreferenz zurückgeben? Zeig mal wie die Headerdatei deiner Basisklasse aussieht, am konkreten Beispiel lässt es sich leichter diskutieren.

Locutus2002
2016-02-11, 12:36:16
Gegenfrage: kannst du denn garantieren, dass alle Ableitungen deiner Basisklasse genau dieses Verhalten benötigen? Also dass alle Untertypen immer eine Textlänge von einem Vielfachen der Blocklänge benötigen?


Die Modi ECB, (P)CBC und CFB sehen dass allgemein für alle blockchiffren vor, unabhängig vom konkreten Algorithmus. Also kann das in diesem Fall garantiert werden, weil vorgeschrieben.


Laufzeitprobleme entdeckt man halt erst zur Laufzeit. Deshalb sind Unit Tests so essentiell.


Okay, hab ich ungünstig formuliert. Also nochmal besser: Wie könnte ich den Algorithmus so umbauen, dass das Rahmenprogramm erkennen kann, dass die Entschlüsselung nicht funktioniert hat? Man könnte natürlich einen Fehlercode zurückgeben, aber ich gebe ja bereits eine Objektreferenz zurück (siehe nächste Frage).


Das hört sich... interessant an. Warum genau willst du die Objektreferenz zurückgeben?
Damit ich sowas machen kann:
obj.decryptECB().getPlainText()

z3ck3
2016-02-11, 12:56:21
Ohne mich jetzt mit C++ auszukennen würde ich bei einem Fehler idR eine Exception werfen. Wie der Entwickler damit umgeht der meine Bibliothek nutzt, das ist dann ja seine Sache. Nur dann kannst du Ketten wie obj.decryptECB().getPlainText() machen, sonst würde ja getPlainText() einen leeren String zurück geben, was weder falsch noch richtig wäre ;)

del_4901
2016-02-11, 14:37:25
encryption und decryption sind einfach nur spezialfaelle von (de)serialisierung, decompression oder memcopy. Da kann kann man sich dann gerne eines interfaces bedienen. Im Error fall tuts einfach ein Fehlercode. Ich wuerde da auch keine Objekte zurueck geben.

ErrorCode [en/de]crypt(Cypher cypher, void* source, size_t sourceSize, void* dest, size_t destSize);

Dann kannst du entweder im Cypher einen encryptionTypeEnum hinterlegen und die Funktion branched entsprechend oder wenn es ein bissel sauberer sein soll einfach einen Builder mit passen oder alternativ einen Functionpointer/Closure.

Die GUI hat damit erstmal recht wenig zu tun.

Monger
2016-02-11, 20:37:23
Okay, hab ich ungünstig formuliert. Also nochmal besser: Wie könnte ich den Algorithmus so umbauen, dass das Rahmenprogramm erkennen kann, dass die Entschlüsselung nicht funktioniert hat?

In jeder anderen modernen Sprache wäre die Antwort ganz klar: Exceptions. Je nachdem wie portabel dein Code sein muss, ist das blöderweise nicht immer ne Option. Aber das selbe gilt für Foreach Schleifen und Smart Pointer... setz also deine Zielplattform realistisch.

Wenn du es dem Anwender erlauben willst, das werfen von Exceptions unterbinden zu können, dann wäre eine zusätzliche Methode wie z.B. "CanBeEncrypted(text)" üblich.

Aber dir ging es ja auch um die Frage, wie du deinen Code möglichst entkoppelt von den Frameworks transportieren kannst. Wenn ich dazu was sagen soll, müsste ich wirklich einen Blick auf die Headerdatei werfen.

Marscel
2016-02-11, 23:20:12
Zweite Frage: Wie baue ich in die De-/Codierungsfunktion eine beliebige Callbackfunktion (z.B. eine reine Textausgabefunktion bei Konsolenprogrammen oder aber die Memberfunktion zur Fortschrittsbalkengenerierung der QProgressBar-Klasse in Qt) zur Fortschrittsanzeige ein? Der klassische C-Weg mit Funktionszeigern, den ich mal gelernt habe, hat sich als unbrauchbar erwiesen (wg. notwendiger Angabe einer konkreten Klasse).

Wenn du auf QObject-Signale verzichten möchtest, such dir was aus:

* Du reichst Objekte einer Klasse rein, die ein Interface implementiert hat, mit der dein Task was anfangen kann.
* Du nutzt Funktor-Objekte.
* Du nutzt std::bind.

Locutus2002
2016-02-12, 12:54:45
Aber dir ging es ja auch um die Frage, wie du deinen Code möglichst entkoppelt von den Frameworks transportieren kannst. Wenn ich dazu was sagen soll, müsste ich wirklich einen Blick auf die Headerdatei werfen.

Hier:
class BlockCipher
{
protected:
//plain and cipher text variables
char *plainText;
char *cipherText;
char *initVect; //initialisation vector for (P)CBC, CFB, OFB and CTR modes
unsigned int plainTextLength; //number of characters of the plain text (total length of the respective char array)
unsigned int cipherTextLength; //number of characters of the cipher text (total length of the respective char array)
unsigned short blockSize; //block size in bytes (length of the respective char array portion); also size of IV

unsigned char paddingMethod; //for ECB,(P)CBC and CFB modes; 0 = Zero Padding, 1 = ANSI X.923, 2 = PKCS #7, 3 = ISO/IEC 7816-4, 4 = ISO 10126, 5 = no padding (if possible, zero padding otherwise)

//key variables
char *key;
unsigned short keySize; //key size in bytes (length of the respective char array)

char * padding(unsigned int &numBlocks);
char * paddingReverse(void);

virtual void encryptBlock(char *blockArray){};
virtual void decryptBlock(char *blockArray){encryptBlock(blockArray);}

public:
BlockCipher();
BlockCipher(const char *Text, const unsigned int TextLength, const unsigned short bSize, const char *Key, const unsigned short kSize, bool enc); //enc: 0 = to decrypt, 1 = to encrypt
BlockCipher(const BlockCipher &rhs); //copy-constructor
~BlockCipher(void);

char * getPlainText(void) const {return plainText;}
char * getCipherText(void) const {return cipherText;}
char * getInitVect(void) const {return initVect;}

unsigned int getPlainTextLength(void) const {return plainTextLength;}
unsigned int getCipherTextLength(void) const {return cipherTextLength;}
unsigned short getBlockSize(void) const {return blockSize;}

void setPlainText(const char *newPlainText, const unsigned int newPlainTextLength);
void setCipherText(const char *newCipherText, const unsigned int newCipherTextLength);
void setInitVect(const char *newInitVect, unsigned short newBlockSize);
void setBlockSize(const unsigned short newBlockSize){blockSize = newBlockSize;}

void setPaddingMethod(const unsigned char method){paddingMethod = method;}

char * getKey(void) const {return key;}
unsigned short getKeySize(void) const {return keySize;}
void setKey(const char *newKey, const unsigned short newKeySize);

//Electronic Code Book
BlockCipher & encryptECB(void);
BlockCipher & decryptECB(void);

//(Propagating) Cipher Block Chaining
BlockCipher & encryptCBC(const bool propagating);
BlockCipher & decryptCBC(const bool propagating);

//Cipher Feedback
BlockCipher & encryptCFB(void);
BlockCipher & decryptCFB(void);

//Output Feedback
BlockCipher & encryptOFB(void);
BlockCipher & decryptOFB(void);

//Counter
BlockCipher & encryptCTR(void);
BlockCipher & decryptCTR(void);

BlockCipher & operator=(const BlockCipher &rhs);
};
(Natürlich weiß ich, dass manches, was hier protected ist, nicht für abgeleitete Klassen direkt verfügbar sein muss und daher private sein könnte, aber darum kümmere ich mich später.)

Die abgeleitete Klasse für den Rijndael-Algorithmus sieht so aus:
class Rijndael:public BlockCipher
{
private:
char *expandedKey;
char numMovesEnc[3];
char numMovesDec[3];

//key expansion
void KeyExpansion(void);

//encryption
void AddRoundKey(char *blockArray, const char *roundKey);
void SubBytes(char *blockArray, const unsigned short &length);
void ShiftRows(char *blockArray, const char *numMoves);
void MixColumns(char *blockArray);
void encryptBlock(char *blockArray);

//decryption
void inverseSubBytes(char *blockArray, const unsigned short &length);
void InverseMixColumns(char *blockArray);
void decryptBlock(char *blockArray);

public:
//contructors
Rijndael(void);
Rijndael(char *Text, unsigned int TextLength, unsigned short bSize, const char *Key, unsigned short kSize, bool enc);
Rijndael(const Rijndael &rhs);
~Rijndael(void);

//overloaded setters
void setBlockSize(const unsigned short newBlockSize);
void setKey(const char *newKey, const unsigned short newKeySize);

//overloaded operators
Rijndael & operator=(const Rijndael & rhs);
};

del_4901
2016-02-12, 13:07:05
Das Key management kannst du gerne in eine eigene Klasse packen. Aber die eigentliche en- und de-cryption sind in plain old C functions (welche viel zu unterbewertet in C++ sind) besser aufgehoben. Dann braucht auch keine Vererbung mehr. Shared Helperfunktionen kannst du einfach module static declarieren oder nur in die implementierung (.cpp) (ver)stecken.

cypher.h:

enum CypherError
{
CypherError_AlignmentError,
CypherError_DestinationSizeTooSmall,
CypherError_UnknownType,
/* ... */
CypherError_Count
};

interface Cypher
{
virtual ~Cypher() = 0;
};

//those are the factorys for the keys
unique_ptr<const Cypher> generateRijndaelKey(/* ... */);
unique_ptr<const Cypher> generateAesKey(/* ... */);

//function documentation
CypherError encrypt(unique_ptr<const Cypher> cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize);
//function documentation
CypherError decrypt(unique_ptr<const Cypher> cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize);


cypherRijndael.inc:

//function documentation
CypherError encryptRijndael(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize);
//function documentation
CypherError decryptRijndael(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize);

CypherError encryptRijndael(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize)
{
/* ... */
}

CypherError decryptRijndael(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize)
{
/* ... */
}


cypherAes.inc:

//function documentation
CypherError encryptAes(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize);
//function documentation
CypherError decryptAes(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize);

CypherError encryptAES(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize)
{
/* ... */
}

CypherError decryptAES(const CypherImpl& cypher, const void* source, size_t sourceSize, void* destination, size_t destinationSize)
{
/* ... */
}


cypherImpl.inc:

enum CypherType
{
CypherType_Rijndael,
CypherType_AES,
/* ... */
CypherType_Count
};

class CypherImpl : public Cypher
{
const CypherType m_type;

public:
CypherImpl(CypherType type);
~CypherImpl(CypherType type) {}
/* ... */

inline const CypherType getType() const { return m_type; }
};

CypherImpl::CypherImpl(CypherType type) : m_type(type)
{
/* ... */
}

unique_ptr<const Cypher> generateRijndaelKey(/* ... */)
{
const Cypher* cypher = new CypherImpl(CypherType_Rijndael /* ... */);
return unique_ptr<const Cypher>(cypher);
}

unique_ptr<const Cypher> generateAesKey(/* ... */)
{
const Cypher* cypher = new CypherImpl(CypherType_AES /* ... */);
return unique_ptr<const Cypher>(cypher);
}


cypher.cpp:

#include "cypherImpl.inc"
#include "cypherRijndael.inc"
#include "cypherAes.inc"

CypherError encrypt(unique_ptr<const Cypher> cypher_, const void* source, size_t sourceSize, void* destination, size_t destinationSize)
{
const CypherImpl& cypher = static_cast<CypherImpl&>(*cypher_.get());
if (cypher.getType() == CypherType_Rijndael)
{
return encryptRijndael(cypher, source, sourceSize, destination, destinationSize);
}
else if (cypher.getType() == CypherType_AES)
{
return encryptAES(cypher, source, sourceSize, destination, destinationSize);
}
else
{
return CypherError_UnknownType;
}
}

CypherError decrypt(unique_ptr<const Cypher> cypher_, const void* source, size_t sourceSize, void* destination, size_t destinationSize)
{
const CypherImpl& cypher = static_cast<CypherImpl&>(*cypher_.get());
if (cypher.getType() == CypherType_Rijndael)
{
return decryptRijndael(cypher, source, sourceSize, destination, destinationSize);
}
else if (cypher.getType() == CypherType_AES)
{
return decryptAES(cypher, source, sourceSize, destination, destinationSize);
}
else
{
return CypherError_UnknownType;
}
}

Monger
2016-02-12, 13:55:03
Hier:

Sieht gut aus. Hatte schlimmeres befürchtet.

Ich verstehe jetzt auch, warum du das Objekt sich selbst zurückgeben lassen willst. Wäre elegant, das wie String Operationen handhaben zu können. Vorschlag: denk darüber nach, das ganze - eben so wie üblicherweise Strings - Immutable zu machen. Das wird eventuell darauf hinaus laufen, Encryption und Decryption voneinander zu trennen.

Monger
2016-02-12, 13:59:35
Das Key management kannst du gerne in eine eigene Klasse packen. Aber die eigentliche en- und de-cryption sind in plain old C functions (welche viel zu unterbewertet in C++ sind) besser aufgehoben. Dann braucht auch keine Vererbung mehr. Shared Helperfunktionen kannst du einfach module static declarieren oder nur in die implementierung (.cpp) (ver)stecken.


Sorry, ich muss hier anmerken dass das was du zeigst ein Antipattern ist. Die Signaturen sehen vielleicht ne Spur schöner aus, aber die Implementierung wird dreckig. Übersichtlichkeit und Erweiterbarkeit sind damit für die Tonne. Interne Switch Cases auf den Type? Leidest du unter Polymorphismus Angst?

del_4901
2016-02-12, 14:10:27
Sorry, ich muss hier anmerken dass das was du zeigst ein Antipattern ist. Die Signaturen sehen vielleicht ne Spur schöner aus, aber die Implementierung wird dreckig. Übersichtlichkeit und Erweiterbarkeit sind damit für die Tonne. Interne Switch Cases auf den Type? Leidest du unter Polymorphismus Angst?

Was wird denn daran dreckig?
Wo leidet die Uebersichtlichkeit? Die einzelnen implementationen kann man immer noch in unterschiedliche .cpp files packen.
Und wo ist das Problem mit dem einen Switch?
Polymorphismus ist einfach unnoetig fuer den Use-Case. Ich wuerde sogar behaupten, dass dadurch die Uebersichtlichkeit mehr leidet.

Monger
2016-02-12, 21:31:24
Und wo ist das Problem mit dem einen Switch?

Wenn du auch nur einen Typus hinzufügen willst, musst du den Originalquellcode anfassen. Wäre das eine Library, wäre das ein No-Go.

Aber ich entschuldige mich: du hast recht, ob jetzt die Implementierung unbedingt polymorph besser ist: kann sein, muss aber nicht sein.

del_4901
2016-02-12, 22:03:31
Wenn du auch nur einen Typus hinzufügen willst, musst du den Originalquellcode anfassen. Wäre das eine Library, wäre das ein No-Go.

Aber ich entschuldige mich: du hast recht, ob jetzt die Implementierung unbedingt polymorph besser ist: kann sein, muss aber nicht sein.

Naja wenn man eine von aussen erweiterbare Library draus machen will kann man immer noch auf ein zwei Klassen Design wo eine davon polymorth ist setzen. Allerdings bin ich da eher pagmatisch und gebe sowenig wie moeglich vom Interface preis, wenn es nicht unbedingt sein muss. So behaelt man besser die Kontrolle wenn man doch mal was refactorn muss.
Da hast du mich grade noch auf eine Idee gebracht den die KeyImplementierung noch weiter zu verstecken (siehe edit oben). Wenn die Keys jetzt noch grosse Unterschiede aufweisen, dann kann man die auch vererben. Und ja ich hab Standard RTTI Angst. ;)

In dem speziellen Fall hier sehe ich noch keinen Vorteil in einer von aussen erweiterbaren Library.
Zumal man wenn es unbedingt sein muss und man den Source nicht hat immernoch eine ProxyFacade drum rum bauen kann, welche weitere Verschluesselungs Methoden anbietet.

Locutus2002
2016-02-15, 14:59:48
Wow! Vielen Dank! Da hab ich mir einiges anzusehen und nachzulesen (viele von euch verwendete Ausdrücke kannte ich vorher gar nicht ;-) bin ja wie eingangs gesagt nur Amateur).

Das Callback-Thema habe ich übrigens sehr elegant mit std::function und std::bind gelöst, was (für mich) enorm viel übersichtlicher und verständlicher ist als der Weg über Wrapper.