microsoft / PowerApps-TestEngine

MIT License
109 stars 33 forks source link

[Feature]: Storage State Encryption #492

Open Grant-Archibald-MS opened 6 days ago

Grant-Archibald-MS commented 6 days ago

Is your feature request related to a problem? Please describe.

How can the state.json which can contain browser cookies, be encrypted, to ensure that:

Describe the solution you'd like

To address this problem, we could consider a combination of AES (Advanced Encryption Standard) for encrypting the state.json content and RSA (Rivest-Shamir-Adleman) for encrypting the AES key. This goal to ensure robust security and allows for secure sharing of encrypted files across authorized users or application users.

Provide the ability to use Windows Data Protection (DAPI) or Certificate Public/Private key encryption of AES

Image

Summary

Primer on AES and RSA Encryption

AES (Advanced Encryption Standard):

Type: Symmetric encryption algorithm. Key Concept: Uses the same key for both encryption and decryption. Strengths: Fast and efficient, suitable for encrypting large amounts of data. Usage: Commonly used for securing data at rest and in transit. How It Protects: Encrypts data into an unreadable format that can only be decrypted with the same key, ensuring data confidentiality.

RSA (Rivest-Shamir-Adleman):

Type: Asymmetric encryption algorithm. Key Concept: Uses a pair of keys – a public key for encryption and a private key for decryption. Strengths: Secure key exchange, suitable for encrypting small amounts of data like encryption keys. Usage: Often used for securing data transmission and digital signatures. How It Protects: Encrypts data with a public key that can only be decrypted with the corresponding private key, ensuring secure key exchange and data integrity.

Possible Approach

  1. Encrypting state.json Content with unique AES:

    • AES Encryption: Use AES to encrypt the state.json file. AES is a symmetric encryption algorithm that is fast and efficient for encrypting large amounts of data.
    • Key Management: Generate a unique AES key for encryption.
  2. Encrypting the AES Key with RSA:

    • RSA Encryption: Use RSA to encrypt the AES key. RSA is an asymmetric encryption algorithm that uses a pair of keys (public and private). The public key encrypts the AES key, and the private key decrypts it.
    • Key Sharing: Store the RSA public key in a secure location accessible to all authorized users or application users. The RSA private key should be securely stored and only accessible to users or applications with decryption privileges (Dataverse Security Role).
  3. Storing and Managing Keys in Dataverse:

    • Dataverse Tables: Create tables in Dataverse to store the encrypted AES keys and RSA key pairs.
    • Store AES Encrypted value in dataverse with the public key
    • Support Windows Data Protaction API (DAPI) or Certificate based encryption
    • Access Control: Use Dataverse's role-based security to control access to the keys. Only authorized users or application users should have access to the RSA private key.
  4. Key Rotation and Audit Logging:

    • Key Rotation: Implement policies to periodically update encryption keys and re-encrypt the state.json content.
    • Audit Logging: Track key access and operations in an AuditLogs table to monitor and detect any unauthorized attempts.

Sharing Encrypted Files

Authorized Users: Ensure that users involved in the development and CI/CD process have the necessary roles to access the encrypted AES keys and RSA private key. Application Users: Configure application users with appropriate permissions to access and decrypt the state.json content during automated processes.

Technical Approach

Possible technical approach with Audit Enabled tables using direct Encryption libraries or ASP.NET Core Data Protection described below

Dataverse Table Schema

  1. Test Engine Keys Table: Columns:

    • KeyId (Primary Key)
    • Name (String)
    • Xml (IXmlRepository)
  2. Test Engine Key Data Table: Columns:

    • DataId (Primary Key)
    • KeyName (String)
    • ValueName (String)
    • Data (String, Base64 encoded encrypted)

Dataverse Record-Level Security and Column-Level Masking

Record-Level Security Dataverse allows you to control access to individual records based on user roles and permissions. This ensures that only authorized users can access specific records.

Key Concepts:

Describe alternatives you've considered

Alternative Solution: Windows Data Protection API (DAPI)

As Microsoft Learn How to: Use Data Protection states

.NET provides access to the data protection API (DPAPI), which allows you to encrypt data using information from the current user account or computer. When you use the DPAPI, you alleviate the difficult problem of explicitly generating and storing a cryptographic key.

This implementation which would be Microsoft Windows centric could provide default level of protection to state.json files. Given the encryption is tied to the user account and machine the state.json file would only be able to be decrypted on the machine.

Alternative Solution: ASP.NET Core Data Protection

ASP.NET Core security topics gives and overview of multi machine method to encrypt sensitive information.

How It Works

This could be combined with Dataverse based solution of a console application to read and save values for a set of secure keys. Key names would need to be unique to specific set of tests for a user context.

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System.Security.Cryptography.X509Certificates;
using System.Xml.Linq;

public class Program { 
    public static void Main(string[] args)
    {
        ServiceProvider services = null;

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddLogging(configure => configure.AddConsole());

        var api = new Uri("https://contoso.crm.dynamics.com/");

        string keyName = "Sample";

        // Configure Dataverse connection
        var serviceClient = new ServiceClient(api, (url) => Task.FromResult(AzureCliHelper.GetAccessToken(api)));

        serviceCollection.AddSingleton<IOrganizationService>(serviceClient);

        serviceCollection.AddDataProtection()
            .ProtectKeysWithCertificate(GetCertificateFromStore("localhost"))
            //.ProtectKeysWithDpapi()
            .AddKeyManagementOptions(options =>
            {
                options.XmlRepository = new DataverseKeyStore(services?.GetRequiredService<ILogger<Program>>(), serviceClient, keyName);
            });
        services = serviceCollection.BuildServiceProvider();
        var protector = services.GetDataProtector("ASP Data Protection");

        string valueName = string.Empty;
        while ( string.IsNullOrEmpty(valueName))
        {
            Console.WriteLine("Variable Name");
            valueName = Console.ReadLine();
        }

        var matches = FindMatch(serviceClient, keyName, valueName);

        if (matches.Count() == 0)
        {
            string newValue = string.Empty;
            while ( string.IsNullOrEmpty(newValue))
            {
                Console.WriteLine("Value does not exist. What would you like the value to be?");
                newValue = Console.ReadLine();
            }

            StoreValue(serviceClient, keyName, valueName, protector.Protect(newValue));

            Console.WriteLine($"Saved value for {valueName}");
        } 
        else
        {
            string data = protector.Unprotect(matches.First().Data);
            Console.WriteLine($"Value {valueName}: {data}");
        }

        Console.ReadLine();
    }
}

Possible code to query and store encrypted values


    public static IReadOnlyCollection<ProtectedKeyValue> FindMatch(IOrganizationService service, string keyName, string? valueName)
    {
        // Retrieve keys from Dataverse
        FilterExpression filter = new FilterExpression(LogicalOperator.And);
        filter.Conditions.Add(new ConditionExpression("te_keyname", ConditionOperator.Equal, keyName));
        filter.Conditions.Add(new ConditionExpression("te_valuename", ConditionOperator.Equal, valueName));

        var query = new QueryExpression("te_keydata")
        {
            ColumnSet = new ColumnSet("te_keyname", "te_valuename", "te_data"),
            Criteria = filter
        };

        var keys = service.RetrieveMultiple(query)
        .Entities
        .Select(e => new ProtectedKeyValue {
            KeyId = e.Id.ToString(),
            KeyName = e["te_keyname"]?.ToString(),
            ValueName = e["te_valuename"]?.ToString(),
            Data = e["te_data"]?.ToString(),
        })
        .ToList();

        return keys.AsReadOnly();
    }

    public static void StoreValue(IOrganizationService service, string keyName, string valueName, string data)
    {
        var keyEntity = new Entity("te_keydata")
        {
            ["te_keyname"] = keyName,
            ["te_valuename"] =  valueName,
            ["te_data"] = data,
        };

        service.Create(keyEntity);
    }

Optionally query certificate from Windows Store

    static X509Certificate2 GetCertificateFromStore(string friendlyName)
    {
        using (var store = new X509Store(StoreLocation.CurrentUser))
        {
            store.Open(OpenFlags.ReadOnly);
            var certs = store.Certificates.Find(X509FindType.FindBySubjectName, friendlyName, false);
            if (certs.Count == 0)
            {
                throw new Exception("Certificate not found");
            }
            return certs.First();
        }
    }

Possible code to query and store encryption key values encrypted via DAPI or public key of certificate

public class DataverseKeyStore : IXmlRepository
{
    private readonly ILogger<Program>? _logger;
    private readonly IOrganizationService _service;
    private string _friendlyName;

    public DataverseKeyStore(ILogger<Program>? logger, IOrganizationService organizationService, string friendlyName)
    {
        _logger = logger;
        _service = organizationService;
        _friendlyName = friendlyName;
    }

    public IReadOnlyCollection<XElement> GetAllElements()
    {
        // Retrieve keys from Dataverse
        var query = new QueryExpression("te_key")
        {
            ColumnSet = new ColumnSet("te_xml"),
            Criteria = new FilterExpression
            {
                Conditions =
                {
                    new ConditionExpression("te_name", ConditionOperator.Equal, _friendlyName)
                }
            }
        };

        var keys = _service.RetrieveMultiple(query)
        .Entities
        .Select(e => XElement.Parse(e.GetAttributeValue<string>("te_xml")))
        .ToList();

        return keys.AsReadOnly();
    }

    public void StoreElement(XElement element, string friendlyName)
    {
        var keyEntity = new Entity("te_key")
        {
            ["te_name"] = _friendlyName,
            ["te_xml"] = element.ToString(SaveOptions.DisableFormatting)
        };

        _service.Create(keyEntity);
    }
}

Class to query encrypted value from Dataverse

public class ProtectedKeyValue
{
    public string KeyId { get; set; }
    public string KeyName { get; set; }
    public string ValueName { get; set; }
    public string Data { get; set; }
}

Alternative Solution: Managed Information Protection (MIP)

Overview: Managed Information Protection (MIP) is a comprehensive data protection solution integrated with Microsoft 365. It provides advanced capabilities for data classification, labeling, and protection across various Microsoft 365 services.

Key Features

Pros

Comprehensive Protection: MIP offers a unified solution for data protection, covering classification, labeling, and encryption. User-Friendly: The labeling and classification features are intuitive, making it easy for users to apply protection to documents and emails. Policy Management: Centralized management of data protection policies ensures consistent enforcement across the organization.

Notes

Additional context?

No response

Grant-Archibald-MS commented 2 days ago

Key elements:

  1. No secrets are stored on the local file system
  2. Encrypted values are stored inside Dataverse
  3. Dataverse security rules applied to Dataverse tables
  4. Public Key values are stored in Dataverse and can be shared across users or Application user accounts
  5. CI/CD account using Managed Service Identity can be registered as Application Users. Key record can be shared with Application user accounts
  6. Private key values are managed via DAPI for the user and machine or private key of certificate