sources/Google.Solutions.IapDesktop.Application/Host/Install.cs (255 lines of code) (raw):

// // Copyright 2023 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Apis.Client; using Google.Solutions.Common.Linq; using Google.Solutions.Common.Util; using Google.Solutions.IapDesktop.Application.Profile; using Google.Solutions.IapDesktop.Application.Properties; using Google.Solutions.Platform; using Microsoft.Win32; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text; namespace Google.Solutions.IapDesktop.Application.Host { /// <summary> /// Represents the current installation. /// /// The class also maintains a history of previously installed versions /// in the registry. This history is used to distinguish between "fresh" /// instals and upgraded installs. /// </summary> public interface IInstall { /// <summary> /// Currently installed and running version. /// </summary> Version CurrentVersion { get; } /// <summary> /// First version that was ever installed. This might be the same as the /// current version. /// </summary> Version InitialVersion { get; } /// <summary> /// Version that was installed previously. Null if the user never upgraded. /// </summary> Version? PreviousVersion { get; } /// <summary> /// Base registry key for profiles, etc. /// </summary> string BaseKeyPath { get; } /// <summary> /// Base directory. /// </summary> string BaseDirectory { get; } /// <summary> /// Unique ID for this installation. /// </summary> string UniqueId { get; } /// <summary> /// List profiles. /// </summary> IEnumerable<string> Profiles { get; } /// <summary> /// Create a new (secondary) profile. /// </summary> UserProfile CreateProfile(string name); /// <summary> /// Open the default profile or a secondary profile. The default profile /// is created automatically if it doesn't exist yet. /// </summary> UserProfile OpenProfile(string? name); /// <summary> /// Delete a secondary profile. /// </summary> void DeleteProfile(string name); } public class Install : IInstall { private const string VersionHistoryValueName = "InstalledVersionHistory"; private const string DefaultProfileKey = "1.0"; private const string ProfileKeyPrefix = DefaultProfileKey + "."; /// <summary> /// Base path to profile settings. /// </summary> public const string DefaultBaseKeyPath = @"Software\Google\IapDesktop"; /// <summary> /// Path to policies. This path is independent of the profile. /// </summary> private const string PoliciesKeyPath = @"Software\Policies\Google\IapDesktop\1.0"; private static readonly Version assemblyVersion; private static readonly string uniqueId; //--------------------------------------------------------------------- // Static properties (based on assembly metadata). //--------------------------------------------------------------------- /// <summary> /// Friendly name. /// </summary> public const string ProductName = "IAP Desktop"; /// <summary> /// Default product icon to use for windows. /// </summary> public static Icon ProductIcon => Resources.logo; /// <summary> /// User agent to use in all HTTP requests. /// </summary> public static UserAgent UserAgent { get; } public static bool IsExecutingTests { get; } static Install() { var platform = $"{Environment.OSVersion.VersionString}; " + $"{ProcessEnvironment.ProcessArchitecture.ToString().ToLower()}/" + $"{ProcessEnvironment.NativeArchitecture.ToString().ToLower()}"; assemblyVersion = typeof(Install).Assembly.GetName().Version; UserAgent = new UserAgent( "IAP-Desktop", assemblyVersion, platform); IsExecutingTests = Assembly.GetEntryAssembly() == null; using (var hklm = RegistryKey.OpenBaseKey( RegistryHive.LocalMachine, RegistryView.Default)) using (var crptoKey = hklm.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography")) { // // Read the machine GUID. This is unique ID that's generated // during Windows setup. // var machineGuid = (string?)crptoKey?.GetValue("MachineGuid") ?? string.Empty; // // Create a hash and use the first few bytes as unique ID. // // NB. Use SHA256.Create for FIPS-awareness. // using (var hash = SHA256.Create()) { uniqueId = Convert.ToBase64String( hash.ComputeHash(Encoding.UTF8.GetBytes(machineGuid)), 0, 12); } } } //--------------------------------------------------------------------- // Public properties (based on registry data). //--------------------------------------------------------------------- public Version CurrentVersion => assemblyVersion; public string UniqueId => uniqueId; public string BaseKeyPath { get; } public string BaseDirectory { get; } public Install(string baseKeyPath) { this.BaseKeyPath = baseKeyPath.ExpectNotNull(nameof(baseKeyPath)); this.BaseDirectory = new FileInfo(Assembly.GetExecutingAssembly().Location) .DirectoryName; // // Create or amend version history. // using (var hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) { using (var key = hkcu.CreateSubKey(baseKeyPath)) { var history = ((string[])key.GetValue(VersionHistoryValueName)) .EnsureNotNull() .ToHashSet(); Debug.Assert(history != null); history = history.ExpectNotNull(nameof(history)); history.Add(this.CurrentVersion.ToString()); key.SetValue( VersionHistoryValueName, history.ToArray(), RegistryValueKind.MultiString); } } } public Version InitialVersion { get { using (var hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) { using (var key = hkcu.CreateSubKey(this.BaseKeyPath)) { var history = ((string[])key.GetValue(VersionHistoryValueName)) .EnsureNotNull() .Select(v => new Version(v)); return history.Any() ? history.Min() : this.CurrentVersion; } } } } public Version? PreviousVersion { get { using (var hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) { using (var key = hkcu.CreateSubKey(this.BaseKeyPath)) { var history = ((string[])key.GetValue(VersionHistoryValueName)) .EnsureNotNull() .Select(v => new Version(v)) .Where(v => v != this.CurrentVersion); return history.Any() ? history.Max() : null; } } } } //--------------------------------------------------------------------- // Manage profiles. //--------------------------------------------------------------------- public UserProfile CreateProfile(string name) { if (!UserProfile.IsValidProfileName(name)) { throw new ArgumentException("Invalid profile name"); } using (var hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) { using (var profileKey = hkcu.CreateSubKey($@"{this.BaseKeyPath}\{ProfileKeyPrefix}{name}")) { // // Store the current schema version to allow future readers // to decide whether certain backward-compatibility is needed // or not. // profileKey.SetValue( UserProfile.SchemaVersionValueName, UserProfile.SchemaVersion.Current, RegistryValueKind.DWord); } } return OpenProfile(name); } public UserProfile OpenProfile(string? name) { if (name != null && !UserProfile.IsValidProfileName(name)) { throw new ArgumentException($"Invalid profile name: {name}"); } using (var hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) using (var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default)) { if (name == null) { // // Open or create default profile. For backwards compatibility // reasons, the default profile uses the key "1.0". // var profileKeyPath = $@"{this.BaseKeyPath}\{DefaultProfileKey}"; var profileKey = hkcu.OpenSubKey(profileKeyPath, true); if (profileKey != null) { // // Default profile exists, open it. // } else { // // Key doesn't exist yet. Create new default profile and // mark it as latest-version. // profileKey = hkcu.CreateSubKey(profileKeyPath, true); profileKey.SetValue( UserProfile.SchemaVersionValueName, UserProfile.SchemaVersion.Current, RegistryValueKind.DWord); } return new UserProfile( UserProfile.DefaultName, profileKey, hklm.OpenSubKey(PoliciesKeyPath), hkcu.OpenSubKey(PoliciesKeyPath), true); } else { // // Open existing profile. // var profileKey = hkcu.OpenSubKey($@"{this.BaseKeyPath}\{ProfileKeyPrefix}{name}", true); if (profileKey == null) { throw new ProfileNotFoundException("Unknown profile: " + name); } return new UserProfile( name, profileKey, hklm.OpenSubKey(PoliciesKeyPath), hkcu.OpenSubKey(PoliciesKeyPath), false); } } } public void DeleteProfile(string name) { using (var hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) { var path = $@"{this.BaseKeyPath}\{ProfileKeyPrefix}{name}"; using (var key = hkcu.OpenSubKey(path)) { if (key != null) { hkcu.DeleteSubKeyTree(path); } } } } public IEnumerable<string> Profiles { get { using (var hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) using (var profiles = hkcu.OpenSubKey(this.BaseKeyPath)) { if (profiles == null) { return Enumerable.Empty<string>(); } else { return profiles.GetSubKeyNames() .EnsureNotNull() .Where(n => n == DefaultProfileKey || n.StartsWith(ProfileKeyPrefix)) .Select(n => n == DefaultProfileKey ? UserProfile.DefaultName : n.Substring(ProfileKeyPrefix.Length)); } } } } } }