Ho visto con infinito piacere l'ultimo post di Alessio Marziali dove ci fa vedere un utilizzo
pratico della classe Rijndael per:
- leggere un file di input e memorizzarlo in un byte[]
- criptare il file in un'area di memoria mantenuta da un MemoryStream
- decriptare il contenuto del MemoryStream su un file di output, ottenendo a
tutti gli effetti una copia del file di input iniziale
Nel codice, Alessio fa uso di tutta una serie di classi, alcune delle quali
derivano direttamente dalla classe astratta Stream. In questo
post desidero fare luce - per quanto mi è possibile e cercando di non fare
confusione - sui diversi tipi di stream gestiti da .NET. Lo faccio più che
altro perchè sono andato in confusione io stesso, specialmente quando ho
studiato la classe GZipStream di cui ho già parlato in passato.
La classe astratta Stream
La classe Stream rappresenta il concetto fondamentale sul quale si
basa l'elaborazione dei bytes nel framework .NET. La classe Stream è definita
come abstract, e come tale non può essere istanziata
direttamente: Stream è semplicemente un'astrazione, una sequenza di
bytes qualsiasi, indipendentemente dal modo con cui essi vengono letti, scritti,
manipolati. Per dirla come dice MSDN: "Provides a generic view of a sequence
of bytes". Siccome non ha una rappresentazione fisica, tutto quello che
possiamo fare con Stream è assegnarla ad un oggetto derivato di
Stream. Stream, fra le altre cose, definisce l'interfaccia che abbiamo a disposizione
quando lavoriamo con uno stream.
Detto questo, se non possiamo istanziare uno Stream, cosa possiamo fare?
Possiamo creare un oggetto che deriva da Stream, e che ne rappresenta una
specializzazione. Ecco quindi che .NET ci mette a disposizione FileStream, MemoryStream, BufferedStream. Queste classi non sono più astratte, ma
identificano in modo preciso una sequenza di bytes su files, o in memoria e così
via. Indipendentemente dal tipo di stream, avremo a disposizione la stessa
interfaccia, semplificando di molto il lavoro.
Il FX suddivide i tipi di stream in due ulteriori categorie: gli
stream di back-end e gli stream pass-through.
Io per documentarmi meglio ho trovato questo articolo di Billy Hollis un po' vecchiotto
(2002), che parla del FX1.1, che però può essere sempre utile.
Gli Stream di back-end
Lo Stream di back-end è uno
stream che legge o scrive bytes. Gli esempi che abbiamo fatto
prima sono un esempio di questo tipo di stream: FileStream legge o scrive su un
file, MemoryStream legge o scrive in memoria. La classe Stream espone il metodo
Read per leggere, ed il metodo
Write per scrivere.
Gli Stream di pass-through
Lo Stream pass-through è un
tipo particolare di stream (ma va? ): legge i bytes, li
elabora/trasforma/manipola, e li scrive in uno
stream back-end. La già citata classe GZipStream ne è un esempio: questa classe legge una
sequenza di bytes, li comprime e li reindirizza in uno stream di output. Io
mi immagino lo stream pass-through come una sorta di "filtro", perchè
trasforma i dati, ma di per sè non li memorizza. Un altro esempio di stream
pass-through è la CryptoStream, che è uno dei mattoni
principali della crittografia.
Perchè usare le classi astratte
Le classi astratte ci
permettono di scrivere codice più riutilizzabile, perchè ad esempio possiamo
scrivere metodi che possono lavorare su diversi tipi di oggetti,
se questi però derivano da una stessa classe base. Mi spiego meglio - spero!
- prendendo come esempio il codice di Alessio postato
ieri. Nel punto in cui dichiara l'oggetto il tipo di crittografia che vuole
applicare, Alessio la classe SymmetricAlgorithm:
SymmetricAlgorithm rijn = Rijndael.Create();
L'algoritmo di Rijndael è un algoritmo di crittografia simmetrico (la stessa
chiave viene usata sia per criptare le informazioni, che per decriptarle). Per
sua natura, la classe astratta SymmetricAlgorithm non solo
può contenere un oggetto di tipo Rinjndael come sopra, ma può contenere
qualsiasi altro oggetto derivato da SymmetricAlgorithm: DES, RC2, TripleDES.
Giusto per fare un esempio, ho riscritto brevemente il codice di Alessio per
fargli usare il TripleDES, un algoritmo che applica per 3 volte consecutive il
DES.
private void btnCripta_Click(object sender, EventArgs e)
{
FileStream fin = new FileStream(this.txtFileInput.Text,
FileMode.Open, FileAccess.Read);
byte[] finData = new byte[fin.Length];
FileStream fout = new FileStream(txtFileOutput.Text, FileMode.Create);
SymmetricAlgorithm rijn = TripleDES.Create();
CryptoStream coutenc = new CryptoStream(fout, rijn.CreateEncryptor(),
CryptoStreamMode.Write);
fin.Read(finData, 0, (int)fin.Length);
coutenc.Write(finData, 0, (int)fin.Length);
coutenc.FlushFinalBlock();
coutenc.Close();
fout.Close();
fin.Close();
}
Al contrario di quanto faceva il codice precedente, questo cripta un
file scrivendolo su disco. nonostante abbia cambiato l'algoritmo di criptazione,
ho usato la classe SymmetricAlgorithm proprio per i motivi elencati
prima.