domenica 24 marzo 2024

Crittografia e WCF per passare una password ( od una qualsiasi altra stringa (xml,json, etc.etc.) ) da un applicazione ad un' altra in relativa sicurezza

 Il codice che segue è da considerarsi in alpha e da non utilizzare in un ambiente di produzione , qui potete trovare il  "progetto" https://sourceforge.net/projects/sourcecodesafer/

L'ipotetico scenario è quello in cui vogliamo lasciar testare la nostra applicazione .Net Windows Forms ma vogliamo evitare che sia possibile decompilarla in fase di test ,

per ottenere questo risultato crittografiamo sul nostro pc l'eseguibile da proteggere con una password (chiamiamola P , si tratta di una password simmetrica(https://it.wikipedia.org/wiki/Crittografia_simmetrica) ) ,quando ci spostiamo sulla macchina di test decrittografiamo l'eseguibile con la stessa password e lo carichiamo ed eseguiamo grazie alla reflection ,

sulla macchina di test portiamo quindi l'eseguibile crittografato , se  ci venisse carpita la password P sulla macchina di test si potrebbe decrittografare l'eseguibile e risalire al codice sorgente ,per evitare che ciò accada dobbiamo evitare che possibili keylogger intercettino lo stream della tastiera o/e che uno screen recorder registri la nostra attività sulla tastiera virtuale dell'applicazione , per risolvere questa situazione potremmo utilizzare una seconda applicazione che in remoto fornisca la password P tramite un servizio WCF ( questa è ovviamente solo una delle soluzioni possibili ). Chiamando questo servizio forniamo una password asimmetrica pubblica ( https://it.wikipedia.org/wiki/Crittografia_asimmetrica ) , quest'ultima password viene utilizzata per crittografare la password P , l'array della password così crittografata viene restituito al chiamante il quale ha la chiave privata e può risalire alla password P in chiaro .

Questo "progetto" è costituito da 3 sotto-progetti :

1) un eseguibile Ethical_Hacking.exe che è l'applicazione che vogliamo non sia decompilata in fase di test

2) un applicazione WindowsFormsProtector che si occupa di :

   a) crittografare Ethical_Hacking.exe

   b) decrittografare Ethical_Hacking.exe

   c) creare una coppia di chiavi asimmetriche e chiamare il servizio WCF di  PasswordSupplier passando la chiave pubblica ed ottenendo da PasswordSupplier  la password P crittografata e decrittografarla con la chiave privata quando è ritornata dal metodo GetPassword della classe Service di PasswordSupplier      

   d) caricare in memoria ed eseguire Ethical_Hacking.exe partendo da un array di bytes ( ovvero Ethical_Hacking.exe decrittografato )

 3)  l'applicazione PasswordSupplier che si occupa di :

   a) scegliere l'indirizzo a cui sarà reso disponibile il servizio WCF

   b) consentire di inserire la password ,dovrà essere la stessa inserita al punto 2)a)

   c) rendere disponibile (Start) il servizio all'indirizzo scelto in 3)a)

   d) fermare (Stop) il servizio avviato in 3)c)

   e) fornire l'implementazione di GetPassword  prevista dall'interfaccia IService


Segue il dettaglio dei singoli punti :

1) Ethical_Hacking.exe è un eseguibile Windows Forms si tratta di un form che ne chiama un altro,la sua funzione è solo quella di essere un eseguibile .net chiamabile tramite reflection 

2) vediamo il codice relativo al punto 2)a)

    private void button_encrypt_Click(object sender, EventArgs e)
        {
            try { 
            byte[] encrypted = null;
            string file = this.textBox1.Text;
            string pwd = this.textBox2.Text;
            if (!file.Equals(""))
            {
                    if (pwd.Length > 7)
                    {
                        FileStream fs = new FileStream(this.textBox1.Text, FileMode.Open);
                        BinaryReader br = new BinaryReader(fs);
                        byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
                        fs.Close();
                        br.Close();
                        string EncryptionKey = pwd;
                        byte[] clearBytes = bin;
                        Aes encryptor = Aes.Create();
                        Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[]
                        { 0x20,0x6e, 0x20, 0x61, 0x6e, 0x20, 0x4d, 0x65,0x20, 0x61, 0x64, 0x76,0x65});
                        encryptor.Key = pdb.GetBytes(32);
                        encryptor.IV = pdb.GetBytes(16);
                        MemoryStream ms = new MemoryStream();
                        CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(),
                              CryptoStreamMode.Write);
                        cs.Write(clearBytes, 0, clearBytes.Length);
                        cs.Close();
                        encrypted = ms.ToArray();
                        FileStream fs1 = new FileStream(this.folderName + "\\encryptedExe.encrypt", FileMode.Create);
                        BinaryWriter br1 = new BinaryWriter(fs1);
                        br1.Write(encrypted);
                        fs1.Close();
                        br1.Close();
                    } else
                    {
                        MessageBox.Show("inserire una password di almeno 8 caratteri");
                    }       
            }          
            else {
                    MessageBox.Show("inserire il nome di un file");      
                }
            } catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }

       }        


-segue il codice relativo al punto 2)b) 

                string EncryptionKey = this.textBox2.Text;
                string dir = Directory.GetCurrentDirectory();
                FileStream fs = new FileStream(this.EncryptedFile, FileMode.Open);               
                BinaryReader br = new BinaryReader(fs);
                byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
                byte[] exe = null;
                fs.Close();
                br.Close();
                using (Aes encryptor = Aes.Create())
                {
                    Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[]
                         {0x20, 0x6e, 0x20, 0x61, 0x6e, 0x20, 0x4d, 0x65,0x20, 0x61, 0x64, 0x76,0x65 });
                    encryptor.Key = pdb.GetBytes(32);
                    encryptor.IV = pdb.GetBytes(16);
                    using (MemoryStream ms = new MemoryStream())
                    {
                        using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(),
                                                   CryptoStreamMode.Write))
                        {
                            cs.Write(bin, 0, bin.Length);
                            cs.Close();
                        }
                        exe = ms.ToArray();
                    }
                }
    

l'array exe consiste dell'eseguibile decrittografato 

-passiamo al punto 2)c)

    private void GetPasswordFromRemoteAddress(object sender, EventArgs e)
        {
            try
            {
                /*https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rsacryptoserviceprovider?view=net-7.0*/
                RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
                string publicKey = rsa.ToXmlString(false); // false to get the public key   
                string address = this.textBox5.Text;                
                ChannelFactory<IService> cf = new ChannelFactory<IService>(new WebHttpBinding(), address);
                //WebHttpBehavior abilita il modello di programmazione Web per un servizio Windows Communication Foundation (WCF).
                cf.Endpoint.Behaviors.Add(new WebHttpBehavior());
                //crea il canale di comunicazione
                IService channel = cf.CreateChannel();
                //viene invocato il metodo GetPassword sul servizio il cui indirizzo è address                
                byte[] encryptedData = channel.GetPassword(publicKey);
                //viene valorizzato decryptedData con l'array di bytes ritornati dal metodo RSADecrypt
                byte[] decryptedData = RSADecrypt(encryptedData, rsa.ExportParameters(true), true);
                UnicodeEncoding convertToString = new UnicodeEncoding();
                string str = "";
                str = convertToString.GetString(decryptedData);
                this.textBox2.Text = str;                              
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message.ToString());
            }
        }

      segue il metodo RSADecrypt che utilizza la chiave privata:

      static public byte[] RSADecrypt(byte[] DataToDecrypt, RSAParameters RSAKeyInfo, bool DoOAEPPadding)
        {
            try
            {
                byte[] decryptedData;
                //Create a new instance of RSACryptoServiceProvider.
                using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
                {
                    //Import the RSA Key information. This needs
                    //to include the private key information.
                    RSA.ImportParameters(RSAKeyInfo);
                    //Decrypt the passed byte array and specify OAEP padding.  
                    //OAEP padding is only available on Microsoft Windows XP or
                    //later.  
                    decryptedData = RSA.Decrypt(DataToDecrypt, DoOAEPPadding);
                }
                return decryptedData;
            }
            //Catch and display a CryptographicException  
            //using a message box
            catch (CryptographicException e)
            {                
                MessageBox.Show(e.Message.ToString());
                return null;
            }
        }

-punto 2)d)

private void button_decrypt_and_run_Click(object sender, EventArgs e)
        {
            try
            {
                string EncryptionKey = this.textBox2.Text;
                string dir = Directory.GetCurrentDirectory();
                FileStream fs = new FileStream(this.EncryptedFile, FileMode.Open);               
                BinaryReader br = new BinaryReader(fs);
                byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
                byte[] exe = null;
                fs.Close();
                br.Close();
                using (Aes encryptor = Aes.Create())
                {
                    Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[]
                         {0x20, 0x6e, 0x20, 0x61, 0x6e, 0x20, 0x4d, 0x65,0x20, 0x61, 0x64, 0x76,0x65 });
                    encryptor.Key = pdb.GetBytes(32);
                    encryptor.IV = pdb.GetBytes(16);
                    using (MemoryStream ms = new MemoryStream())
                    {
                        using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(),
                                                   CryptoStreamMode.Write))
                        {
                            cs.Write(bin, 0, bin.Length);
                            cs.Close();
                        }
                        exe = ms.ToArray();
                    }
                }
               //https://learn.microsoft.com/en-us/dotnet/api/system.reflection.assembly.load?view=net-8.0
                Assembly assembly1 = Assembly.Load(exe);
                var programType1 = assembly1.GetTypes().FirstOrDefault(c => c.Name == "Program");
                MethodInfo method1=programType1.GetMethod("Start", BindingFlags.Public | BindingFlags.Static);
                method1.Invoke(null, new object[] { });                
            }
            catch(Exception ex)
            {
                string msg= " controllare anche che la password per la decriptazione sia corretta";
                MessageBox.Show(ex.Message.ToString() + " " + msg);                
            }
        }

per utilizzare il codice soprastante bisogna modificare il codice di Ethical_Hacking.exe aggiungendo il metodo Start :     
    
static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            //Start();
        }
                
        public static void Start()  
        {
            Form1 f = new Form1();
            f.ShowDialog();           
        }
    }
    
il metodo chiama il primo Form dell'applicazione (Form1) .

3)  
  a) si tratta di un textbox dove possiamo inserire un indirizzo http a cui sarà possibile trovare il servizio WCF 
 b)  come sopra si tratta di un textbox in cui inserire la password che abbiamo utilizzato al punto 2)a) 
 c) al click del button1 (Start) eseguiamo il gestore dell'evento click
         private void button1_Click(object sender, EventArgs e)
        {
                try
                {
                  string uri = this.textBox1.Text;                    
                  string s = "";
                  s = this.textBox2.Text;                    
                  this.password = s;
                  //WebServiceHost è una classe derivata da ServiceHost che integra il modello di programmazione REST
                  //di Windows Communication Foundation (WCF).
                  //https://learn.microsoft.com/it-it/dotnet/api/system.servicemodel.web.webservicehost?view=netframework-4.8
                  host = new WebServiceHost(typeof(Service), new Uri(uri));                       
                  host.Open();               
                } catch(Exception ex)
                {
                   MessageBox.Show(ex.Message.ToString());
                }
        } 

d) al click del button2 (Stop) 
         private void button2_Click(object sender, EventArgs e)
        {
            try { 
                host.Close();
            }catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

e) segue la classe service che implementa IService 
public class Service : IService
    {        
        public byte[] GetPassword(string publicKey)
        {
             try {                
                byte[] encryptedData = null;
                Form1 f = (Form1)Application.OpenForms["Form1"];
                string text = f.password;
                UnicodeEncoding byteConverter = new UnicodeEncoding();
                byte[] dataToEncrypt = byteConverter.GetBytes(text);

                //load the encryptedData variable with the return of the RSACryptoServiceProvider encrypt method
                using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
                {
                    // load public key from publicKey parameter ,publicKey is a valid xml    
                    rsa.FromXmlString(publicKey);
                    // Encrypt the data and store it in the encyptedData bytes array                       
                    encryptedData = rsa.Encrypt(dataToEncrypt, true);
                }
                return encryptedData;
            } catch(Exception ex)
            {
                MessageBox.Show(ex.Message.ToString());
            } 
            return null;
        }       
    }
 
sia WindowsFormsProtector che PasswordSupplier possiedono ed utilizzano la seguente interfaccia :

//https://learn.microsoft.com/en-us/dotnet/framework/wcf/designing-service-contracts
    //https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.servicecontractattribute?view=dotnet-plat-ext-7.0
    [ServiceContract]
    public interface IService
    {
        //https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.operationcontractattribute?view=dotnet-plat-ext-7.0
        [OperationContract]
        //https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.web.webgetattribute?view=netframework-4.8.1
        [WebGet]
        byte[] GetPassword(string s);       

    }

Per suggerimenti ,critiche o segnalazione errori potete inviare un email a gianmarco.castagna@gmail.com

domenica 25 febbraio 2024

Scegliere password complesse ma facili da "indovinare" ? Come evitarlo..

 La lunghezza di una password non ne implica sempre la sicurezza , la password "Passw0rd!" è composta da 9 caratteri  di cui uno maiuscolo ,un numero ed un carattere speciale quindi formalmente è una password forte, in realtà non è così nella pratica se le credenziali sono sottoposte ad un attacco a dizionario usando le password più comuni ( https://it.wikipedia.org/wiki/Attacco_a_dizionario )  . Probabilmente un dizionario usato nell'attacco potrebbe contenere anche la password di cui sopra perchè è molto comune , per mitigare questo tipo di problemi possiamo utilizzare delle liste che contengono le password più utilizzate per verificare in fase di registrazione dell'utente che la password scelta dall'utente non sia tra queste . A quest'indirizzo   https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials potete trovare delle liste di password comuni (1000000 di password e c'è anche 'Passw0rd!' , :-) ) .

Ovviamente un attacco dizionario non è comunque facile da portare a segno se abbiamo configurato la nostra applicazione adeguatamente, ad esempio in asp.net core possiamo settare così la nostra applicazione :

builder.Services.AddDefaultIdentity<IdentityUser>
    (options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireUppercase = true;
        options.Password.RequireLowercase = true;
        options.Lockout.MaxFailedAccessAttempts = 3;
        options.Lockout.AllowedForNewUsers = true;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
    });

impostare il lockout con la valorizzazione dei tentativi falliti dopo i quali l'account è locked ed il periodo per cui esso sarà lockout è quindi relativamente semplice .

E' disponibile qui  https://info.microsoft.com/rs/157-GQE-382/images/EN-CNTNT-eBook-MicrosoftPasswordGuidance.pdf  un documento di Microsoft  che  è  una "Microsoft Password Guidance" , in questo paper è presente anche un link al sito di "Schneier on Security" ( https://www.schneier.com/ ) ,in particolare al post https://www.schneier.com/blog/archives/2014/03/choosing_secure_1.html , chi fosse interessato a tematiche di sicurezza e crittografia potrà trovare interessante la newsletter mensile del sito di Schneier

Per verificare se la password inserita dall'utente appartiene a quelle da ritenere troppo comuni per prima cosa carichiamo le "password comuni" in una tabella 'Passwords' creata nel db "GestioneUtenti" ,un pò di "pseudocodice" :

 private void button1_Click(object sender, EventArgs e)
        {
            IEnumerable<string> lines = File.ReadLines(@"C:\Users\gianm\Desktop\10-million-password-list-top-1000000.txt");
            long l = lines.LongCount<string>(); //999998

            SqlConnection conn = new SqlConnection("Data Source=(localdb)\\mssqllocaldb;Initial Catalog=GestioneUtenti;Integrated Security=True");
            conn.Open();

            foreach (string line in lines) {
                SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
                par.Value = line;

                SqlCommand cmd = new SqlCommand("insert into Passwords(password) values (" + par + ")",conn);
                cmd.Parameters.Add(par);
                cmd.ExecuteNonQuery();
                par = null;
                cmd = null;
            }
            conn.Close();
        }

il codice impiegherà da qualche decina di secondi ad alcuni minuti per caricare le passwords nella tabella 'Passwords' 

segue un metodo per verificare se la password inserita dall'utente esiste nella tabella Passwords:

 private bool CheckPasswordExists(string s)
        {
            bool b = false;
            SqlConnection conn = new SqlConnection("Data Source=(localdb)\\mssqllocaldb;Initial Catalog=GestioneUtenti;Integrated Security=True");
            conn.Open();
            SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
            par.Value = s;
            SqlCommand cmd = new SqlCommand();
            cmd.Parameters.Add(par);
            cmd.Connection = conn;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "select * from Passwords where Password like " + par;
            SqlDataReader reader = cmd.ExecuteReader();
            bool read = reader.Read();
            b = read;
            reader.Close();
            conn.Close();       
            return b;
        }

se il metodo torna true significa che la password esiste nella lista delle password comuni e quindi bisogna indicare all'utente di scegliere un' altra password ,
se il metodo torna false la password non è tra quelle della nostra lista e possiamo lasciar procedere l'utente nella registrazione .

Ora vedremo come utilizzare ulteriori funzionalità proprie di Asp .Net Core per verificare che la password immessa dall'utente non sia troppo comune :

nel file Program.cs ,di un progetto AspNetCore, possiamo aggiungere alla configurazione un AddPasswordValidator con cui costruire una nostra logica di validazione della password ( https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addpasswordvalidator?view=aspnetcore-7.0 ) :

builder.Services.AddDefaultIdentity<IdentityUser>
            (options =>
            {
                options.SignIn.RequireConfirmedAccount = true;
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 8;
                options.Password.RequireNonAlphanumeric = true;
                options.Password.RequireUppercase = true;
                options.Password.RequireLowercase = true;
                options.Lockout.MaxFailedAccessAttempts = 3;
                options.Lockout.AllowedForNewUsers = true;
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
            }) .AddPasswordValidator<TestPasswordValidator<IdentityUser>>()               
               .AddEntityFrameworkStores<AppDbContext>();

creiamo una classe TestPasswordValidator :

public class TestPasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
    {
        
public TestPasswordValidator()
{
}
    public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password)
    {
        SearchIntoDb s = new SearchIntoDb();
        bool res = s.CheckPasswordExists(password);        
        IdentityResult result;
        if (res)
        {
            result = IdentityResult.Failed(new IdentityError { Description = "Password troppo facile da indovinare" });
        } else {
            result = IdentityResult.Success;
        }
        s = null;
        return Task.FromResult(result);
    }
}

la classe deve implementare l'interfaccia IPasswordValidatorhttps://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasswordvalidator-1?view=aspnetcore-7.0 ) e quindi essa deve avere un' implementazione del metodo ValidateAsync previsto dall'interfaccia , sarà questo metodo a ritornare un IdentityResult . Creando l'IdentityError possiamo valorizzare la Description dell'errore , nel caso la password non sia presente nell'elenco la variabile "res" varrà false ed il metodo ritornerà un IdentityResult.Success.

Il metodo ValidateAsync utilizza una classe SearchIntoDb ed un metodo CheckPasswordExists (già riportato sopra ) per effettuare la select nel db al fine di verificare se la password appartiene alla tabella Passwords oppure no.

public class SearchIntoDb
{

    private string connectionString = null;    
    public SearchIntoDb()
    {
        var config = new ConfigurationBuilder()
                .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                .AddJsonFile("appsettings.json").Build();
        string t=config.GetSection("ConnectionStrings").GetSection("UtentiConnection").Value;
        connectionString = t;
    }
   
    public bool CheckPasswordExists(string password)
    {
        bool t = false;        
        SqlConnection conn = new SqlConnection(connectionString);
        conn.Open();
        SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
        par.Value = password;
        SqlCommand cmd = new SqlCommand();
        cmd.Parameters.Add(par);
        cmd.Connection = conn;
        cmd.CommandType = System.Data.CommandType.Text;
        cmd.CommandText = "select * from Passwords where Password like " + par;
        SqlDataReader reader = cmd.ExecuteReader();
        bool b = reader.Read();
        t = b;
        reader.Close();
        conn.Close();       
        return t;
    }

}

proviamo il codice in un applicazione asp .net core , inseriamo la famigerata password "Passw0rd!" 

                              

ed otteniamo quanto ci aspettavamo :



    


Il post ha solo scopo di esempio e la lista di password utilizzata non rappresenta un elenco esaustivo delle password "comuni" .


domenica 18 febbraio 2024

Appunti su Blazor seconda parte : i componenti dei template di esempio

 I componenti :

i componenti possono essere costruiti con 3 modalità differenti :

  1. usando la sintassi Razor si può scrivere in c# ed in html nello stesso file

  2. si può utilizzare un file c# di code behind insieme ad un .razor file

  3. si può utilizzare il solo file c#

Creiamo un applicazione di tipo App WebAssembly Blazor su .Net 7.0

osserviamo il progetto:

la soluzione è composta da 3 progetti :

1) AppWebAssemblyRazor.Client

2)AppWebAssemblyRazor.Server

3)AppWebAssemblyRazor.Shared

il primo progetto ha tra le dipendenze anche il progetto Shared

il secondo ha tra le dipendenze sia il progetto Client che il progetto Shared.

Portiamo la nostra attenzione al file Program.cs del progetto AppWebAssemblyRazor.Client :

public class Program

{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("#app");
        builder.RootComponents.Add<HeadOutlet>("head::after");
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new             Uri(builder.HostEnvironment.BaseAddress) });
    await builder.Build().RunAsync();
    }
}

per prima cosa viene creato un builder ,vengono poi aggiunti due RootComponents ,qui la collezione Rootcomponentmappingcollection https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.rootcomponentmappingcollection.add?view=aspnetcore-7.0 , i due rootcomponents aggiunti sono App ed HeadOutlet ,per vedere come è fatto HeadOutlet selezionate HeadOutlet dopo Add ovvero il tipo di componente aggiunto ,cliccate due volte sul nome della classe :


e vedrete il sorgente ,la classe è sealed (quindi in ogni caso niente ereditarietà da questa classe ) e deriva da ComponentBase :


dopo aver aggiunto i due RootComponents viene aggiunto alla collezione dei servizi un HttpClient in modalità AddScoped ovvero la durata della sua vita sarà quella di una singola request , il suo BaseAddress sarà: BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) ovvero la radice dell'host . Per imparare le differenze tra Scoped , Transient e Singleton un buon inizio è quest'articolo che tratta anche delle dependency injection https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0

In Blazor possiamo “mischiare” codice html e codice c# in vari modi ,per aggiungere il codice c# possiamo usare varie modalità :

  1. usare i Razor code blocks

  2. usare le espressioni implicite

  3. usare le espressioni esplicite

  4. le direttive

  1. per dichiarare un code block possiamo usare due sintassi differenti:

    a) possiamo usare “@code” , in questo modo

    @code {

    //qui codice 

    }

    b) oppure possiamo non utilizzare la keyword “code” e scrivere direttamente così :

    @{

    //qui codice 

    }

un esempio basico di c# ed html può essere il seguente :

@{
        void StampaUnaStringa (string stringaDaStampare) {
            <p>stringa: <bold> @stringaDaStampare </bold> </p>
        }
        //chiamiamo la funzione StampaUnaStringa
        StampaUnaStringa("Il mio nome è nessuno");
        StampaUnaStringa("rispose Ulisse al ciclope");
}

aggiungiamo lo snippet alla pagina counter ed otteniamo :



  1. le blazor implicit expression si hanno quando aggiungiamo del codice “dentro” dei tag :

    <td>@stringaDaStampare</td>

  2. le blazor expression esplicite si utilizzano quando il nostro codice contiene degli spazi , in questo caso il nostro codice sarà preceduto da '@' e contornato da una coppia si parentesi tonde ,ad esempio aggiungiamo questo snippet nel component Counter :  

      <div>
      <table>
      <tr>
      <td>tempo</td>
      <td>UTC:@DateTime.UtcNow</td>
      </tr>
      <tr>
      <td>tempo UTC </td>
      <td>@DateTime.UtcNow</td>
      </tr>
      <tr>
         <td>tempo UTC - 1minuto</td><td>@(DateTime.UtcNow - TimeSpan.FromSeconds(60))</td>
      </tr>
      </table>
      </div>

andando alla “pagina” Counter visualizzeremo : 

per la descrizione dell'acronimo UTC potete andare qui .

  1. se vogliamo aggiungere un attributo alla nostra pagina ,ad esempio [Authorize] possiamo fare così :

    @attribute [Authorize]

    se vogliamo aggiungere lo stesso attributo in un file di code behind basterà [Authorize] .

    Se vogliamo aggiungere un interfaccia sarà sufficiente aggiungere

    @implements NomeInterfaccia se siamo in una pagina oppure se siamo in un code behind

    public class ClasseDiTest : NomeInterfaccia { …..................... } .

    Se vogliamo ereditare da un altra classe possiamo usare la direttiva inherits ,se siamo nel contesto di una pagina :

    @inherits ClasseDaCuiEreditare

    se siamo in un contesto code-behind :

    public class ClasseDiProva: ClasseDaCuiEreditare { …............... }

    Se vogliamo applicare un particolare layout ad una pagina invece di utilizzare il layout di default definito nel file App.razor possiamo usare la direttiva layout :

    @layout UnLayoutSpecifico

    Se vogliamo che il nostro componente non sia nel namespace di default ma in un namespace da noi scelto possiamo usare la direttiva namespace :

    @namespace NamespaceNonDiDefault

    Per aggiungere un namespace al nostro componente possiamo usare la direttiva @using :

    @using System.Net , ovviamente possiamo aggiungere più namespace :
    @using System.CodeDom
    @using System.Net
    @using System.IO
        ............. etc etc

                                        Ritorniamo sui componenti di base:

Il componente App è contenuto dal file App.razor e contiene il seguente codice :


<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

il componente App utilizza il componente Router ,se la route è trovata la RouteView riceve routeData e visualizza lo specificato componente utilizzando il DefaultLayout , se la route non è trovata viene eseguito quanto indicato nel tag NotFound .

Il tag Found contiene anche il componente FocusOnNavigate del quale vengono valorizzate due proprietà : RouteData al quale viene passato il valore @routeData e Selector ,qui è possibile trovare la documentazione della classe FocusOnNavigate :  https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.routing.focusonnavigate?view=aspnetcore-7.0 .

Nella cartella Pages troviamo la “pagine” chiamabili tramite route , la route alla quale corrisponde la pagina è ormai chiaro come sia definito nella variabile @page valorizzata all'interno della pagina.

Il folder Client contiene il file Program.cs :

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("#app");
        builder.RootComponents.Add<HeadOutlet>("head::after");
        builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new             Uri(builder.HostEnvironment.BaseAddress) });
        await builder.Build().RunAsync();

    }
}

questo è l'entry point dell'applicazione, vengono aggiunti due componenti alla collezione RootComponents e viene aggiunto un servizio HttpClient ,questo potrà essere iniettato in un componente tramite l'attributo @inject .

La cartella Shared contiene di default tre componenti :

1) MainLayout

2) NavMenu
3) SurveyPrompt

1) segue il codice di MainLayout ,osserviamo il “tag” <NavMenu /> che inserisce il componente NavMenu :


@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>
        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

ereditando da LayoutComponentBase il componente ha una sola proprietà ovvero Body (https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase?view=aspnetcore-8.0)

la proprietà è di tipo RenderFragment ovvero rappresenta un segmento di contenuto dell'interfaccia utente (qui la doc )  


  1. il componente NavMenu : segue il codice :

    <div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
    <a class="navbar-brand" href="">Demo1</a>
    <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
    <span class="navbar-toggler-icon"></span>
    </button>
    </div>
    </div>
    <div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
    <nav class="flex-column">
    <div class="nav-item px-3">
    <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
    <span class="oi oi-home" aria-hidden="true"></span> Home
    </NavLink>
    </div>
    <div class="nav-item px-3">
    <NavLink class="nav-link" href="counter">
    <span class="oi oi-plus" aria-hidden="true"></span> Counter
    </NavLink>
    </div>
    <div class="nav-item px-3">
    <NavLink class="nav-link" href="fetchdata">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
    </NavLink>
    </div>
    </nav>
    </div>
    @code {
       private bool collapseNavMenu = true;
       private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
       private void ToggleNavMenu()
       {
           collapseNavMenu = !collapseNavMenu;
       }
    }

il componente utilizza la classe NavLink (qui la doc della classe ) , NavLink crea un tag “a” con l'attributo href ed alcune caratteristiche in più come l'attributo Match il quale può assumere un valore tra i due previsti dall' enumerato NavLinkMatch ( qui l'enum ) .

Nell'area @code troviamo un metodo “toggle” (attiva/disattiva) che assegna valore “collapse” o null alla variabile stringa NavMenuCssClass che valorizza l'attributo class del div che contiene il tag “nav” all'interno del quale sono definiti i singoli link (NavLink) .


  1. passiamo al componente SurveyPrompt : anche questo non ha un attributo @page e quindi non è richiamabile tramite route ,la pagina Index.razor lo chiama tramite “tag” e passando un valore per il parametro Title ,segue SurveyPrompt.razor :

    <div class="alert alert-secondary mt-4"><span class="oi oi-pencil me-2" aria-hidden="true"></span>
    <strong>@Title</strong>
    <span class="text-nowrap">
    Please take our
    <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186157">brief survey</a>
         </span>
         and tell us what you think.
    </div>
    @code {
    // Dimostriamo come un componente parent può fornire dei parametri (Title)
    [Parameter]
    public string? Title { get; set; }
    }

segue Index.razor in cui viene passato il valore di title a SurveyPrompt :


@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />

Analisi dei componenti nel folder "Pages" :

Esaminiamo i file sotto la directory Pages :

1)Index
2)Counter
3)FetchData

tutte e tre i componenti sono accomunati dall'avere una direttiva @page che consenti di chiamare i componenti tramite routing

1) Index.razor ,segue il codice :


@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />

osserviamo la presenza di PageTitle , si tratta di un componente già pronto e disponibile che fornisce le funzionalità di Title , qui la PageTitle class ,per il componente SurveyPrompt vale quanto già detto sopra

2) Counter.razor è un componente un po' più articolato di Index ,segue il codice:

@page "/counter"

<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
}
come per Index abbiamo la direttiva page , la quale nel caso specifico vale “/counter” ,segue PageTitle , in questo caso abbiamo un area @code la quale definisce una variabile currentCount di tipo int inizializzandola a zero , il valore della variabile è visualizzato dopo “count :” utilizzando @currentCount , nell'area code abbiamo anche un metodo che incrementa di 1 il valore di currentCount , il metodo è chiamato ogni volta che si clicca sul bottone “Click me” .

  1. FetchData ,è il componente più complesso tra quelli del progetto di esempio ,segue il codice :

@page "/fetchdata"
@inject HttpClient Http
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
    <thead>
    <tr>
    <th>Date</th>
    <th>Temp. (C)</th>
    <th>Temp. (F)</th>
    <th>Summary</th>
    </tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
    <tr>
        <td>@forecast.Date.ToShortDateString()</td>
        <td>@forecast.TemperatureC</td>
        <td>@forecast.TemperatureF</td>    
        <td>@forecast.Summary</td>
    </tr>
}
</tbody>
</table>
}
@code {

private WeatherForecast[]? forecasts;
    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
    }
    public class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

le prime due righe contengono due direttive ,la direttiva page che definisce la route e la direttiva inject che consente di utilizzare l'HttpClient nella pagina,HttpClient è aggiunto ai servizi nel file program.cs con la seguente istruzione :

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

segue un “PageTitle” nell 'area @code troviamo definita una variabile forecasts che può essere null ed è un array di oggetti WeatherForecast , nell'override del metodo OnInitializedAsync viene caricata la variabile forecast , per fare ciò viene chiamato il metodo GetFromJsonAsync di HttpClient il quale punta al file “sample-data/weather.json” ,da notare che la classe WeatherForecast contiene 4 proprietà ,le prime 3 sono valorizzate dalle proprietà con lo stesso nome presenti nel json ,la quarta è una proprietà calcolata ovvero la temperatura in gradi Fahrenheit ( TemperatureF ) partendo dalla temperatura in gradi Celsius ( TemperatureC ) .

                                                                        Logging:

di default i progetti WebAssembly hanno abilitato un provider di logging che scrive nella Console ,se dobbiamo far persistere i risultati dobbiamo utilizzare un altro provider per il logging , segue l'esempio del file Counter.razor che scrive nella Console, potete vederla selezionando la finestra di output di visual studio :


@page "/counter"
@inject ILogger<Counter> logger

<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        logger.LogInformation("incremento di 1");
        currentCount++;
    }
}

la direttiva inject inietta il logger , dentro il metodo IncrementCount viene aggiungiamo la riga             logger.LogInformation(“incremento di 1”);

Abbiamo 6 livelli diversi di Logging (per il logging sotto Microsoft.Extensions.Logging ) qui li troviamo descritti https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=dotnet-plat-ext-8.0

E' possibile usare altri provider di logging ,ad esempio Serilog , qui come utilizzarlo in Blazor .

                                                                        Il binding

    Il binding dei dati nei componenti può essere di due tipi :
         1)One way binding
         2)Two way binding

      1)Un esempio di one way binding è il componente Counter.razor (vedi sopra) ,il metodo IncrementCount aumenta tramite l'operatore ++ il valore di 1 e possiamo vedere aggiornato il valore di @currentCount

      2) quello che segue è un esempio di two way binding in cui abbiamo dei campi di input che vengono valorizzati dall'utente ed un risultato finale che viene “calcolato” dall'applicazione :

creiamo un componente BindTwoWay.razor con il seguente codice contenuto :

@page "/BindTwoWay"

<h3>Bind two ways </h3>
Titolo del libro: <input type="text" @bind="TitoloLibro"/>
<br />
Letto tutto :
<input type="checkbox" @bind="LettoTutto" />
<br />
Autore :
<input type="text" @bind="Autore" />
<br />
<button @onclick="Salva">Salva</button>
<p>@Messaggio</p>


@code {
    public string? TitoloLibro { get; set; }
    public bool LettoTutto { get; set; }    
    public string? Autore { get; set; }
    public string? Messaggio { get; set; }
    private void Salva()
    {
        Messaggio = TitoloLibro + ", letto tutto: " + (LettoTutto ? "sì" : "no") + ", Autore:" + Autore;
        TitoloLibro = null;
        Autore = null;
        LettoTutto = false;
    }
}

la prima riga contiene la direttiva @page ovvero per chiamare questo componente dovremmo chiamare /BindTwoWay , alla direttiva @bind vengono assegnate tre variabili ovvero TitoloLibro , LettoTutto , Autore ,queste tre variabili sono dichiarate nell'area @code , grazie a questa dichiarazione verrà assegnato alle variabili il valore dichiarato nei campi di input ,il metodo Salva viene chiamato quando l'utente clicca sul bottone in cui è assegnato ad @onclick il valore Salva , questo metodo compone un messaggio utilizzando i valori che arrivano dall'inputbox ed inoltre “svuota” i campi di input mettendoli a null ed a false il valore relativo al checkbox il quale viene quindi deselezionato qualora lo fosse ,compiliamo il form chiamando 


compiliamo i campi :

clicchiamo su Salva ed otteniamo :


La navigazione in un sito Blazor

Per navigare programmaticamente un sito possiamo utilizzare il componente NavigationManager ,qui la documentazione, segue un breve esempio , modifichiamo la pagina Index così :

@page "/"
@inject NavigationManager navman

<PageTitle>Index</PageTitle>
<h1>Hello from index page!</h1>
<button name="btNav" @onclick="GoTo">Naviga a counter</button>
<br />
<br />
<button name="btNav1" @onclick="UriAttuale">UriAttuale</button>
<button name="btNav2" @onclick="GoToUri">Navigare all'uri nel textbox</button>
<input name="uribox" type="text" @bind="UriValue" />
@code {
    public string? UriValue { get; set; }
    private void GoTo()
        {
            navman.NavigateTo("/counter");
        }
    private void UriAttuale()
    {
        UriValue=navman.Uri;
    }
    private void GoToUri()
    {
        try {
            navman.NavigateTo(UriValue);
        } catch(Exception ex)
        {
            //qui si potrebbe loggare l'errore :-)         
            UriValue = ex.Message.ToString();
        }
    }
}

notiamo l'uso della direttiva inject utilizzando la quale possiamo utilizzare un oggetto di nome navman e di tipo NavigationManager ,quest'oggetto fornisce il metodo NavigateTo che ci permette di navigare verso una pagina programmaticamente,NavigationManager contiene anche la proprietà Uri che restituisce l'indirizzo attuale del browser ,

il metodo UriAttuale viene invocato a seguito del click sul bottone btNav1 , il metodo assegna alla variabile UriValue il valore della proprietà Uri e poiché la variabile è legata tramite direttiva bind all'input box “uribox” il box risulta aggiornato con il valore impostato nell'assegnazione

UriValue=navman.Uri;

il metodo GoToUri viene chiamato quando si effettua il click sul bottone di nome btNav2 , il metodo naviga all'indirizzo contenuto in “uribox” ovvero ad UriValue ,nel caso si chiami GoToUri quando UriValue è nullo il metodo NavigateTo genera un'eccezione che viene catturata e valorizza l'”uribox”.Ripercorriamo il "flusso del programma" :


clicchiamo sul bottone “Naviga a counter” ed otteniamo :

torniamo alla index e clicchiamo sul bottone “UriAttuale” :

                                                            clicca sull'immagine per ingrandirla

clicchiamo su F5 per fare il refresh della pagina , inseriamo /fetchdata del text box

clicchiamo sul bottone “Navigare all'uri nel textbox” ed otteniamo :


clicchiamo su "Navigare all'uri del textbox" con il textbox vuoto:

l'intero testo d'errore è “Value cannot be null. (Parameter 'uri')” ,l'errore si riferisce al parametro passato al metodo NavigateTo di NavigationManager che non può essere nullo.

Per suggerimenti ,critiche o segnalazione errori potete inviare un email a gianmarco.castagna@gmail.com


Crittografia e WCF per passare una password ( od una qualsiasi altra stringa (xml,json, etc.etc.) ) da un applicazione ad un' altra in relativa sicurezza

 Il codice che segue è da considerarsi in alpha e da non utilizzare in un ambiente di produzione , qui potete trovare il  "progetto&quo...