// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System.Security.Cryptography; namespace ReleaseBuilder.Build; public static partial class Command { /// /// Setup of the current runtime information /// /// /// Constructs a new /// /// The release info to use /// The keyfile password to use /// The sign keys /// The changelog news /// The command input private class RuntimeConfig( Configuration configuration, ReleaseInfo releaseInfo, IEnumerable signKeys, string keyfilePassword, string changelogNews, CommandInput input ) { /// /// The cached password for the pfx file /// private string? _pfxPassword = null; /// /// The commandline input /// private CommandInput Input => input; /// /// The configuration to use /// public Configuration Configuration => configuration; /// /// The release info for this run /// public ReleaseInfo ReleaseInfo => releaseInfo; /// /// The keyfile password for this run /// public IEnumerable SignKeys => signKeys; /// /// The primary password /// public string KeyfilePassword => keyfilePassword; /// /// The changelog news /// public string ChangelogNews => changelogNews; /// /// Gets the PFX password and throws if not possible /// public string PfxPassword => string.IsNullOrWhiteSpace(_pfxPassword) ? _pfxPassword = GetAuthenticodePassword(KeyfilePassword) : _pfxPassword; /// /// Cache value for checking if authenticode signing is enabled /// private bool? _useAuthenticodeSigning; /// /// Cache value for checking if jsign tool should be used for authenticode signing /// private bool? _useJsignToolForAuthenticode; /// /// Checks if Authenticode signing should be enabled /// public void ToggleAuthenticodeSigning() { if (!_useAuthenticodeSigning.HasValue) { if (Input.DisableAuthenticode) { _useAuthenticodeSigning = false; return; } if (Configuration.IsAuthenticodePossibleWithSignTool()) { _useAuthenticodeSigning = true; _useJsignToolForAuthenticode = false; return; } if (Configuration.IsAuthenticodePossibleWithJsignTool()) { if (string.IsNullOrWhiteSpace(Input.SignkeyPin)) { var signKeyPin = EnvHelper.GetEnvKey("SIGNKEY_PIN", ""); if (string.IsNullOrWhiteSpace(signKeyPin)) signKeyPin = ConsoleHelper.ReadPassword("Enter the pin for the signing key"); input = input with { SignkeyPin = signKeyPin }; } if (!string.IsNullOrWhiteSpace(Input.SignkeyPin)) { _useAuthenticodeSigning = true; _useJsignToolForAuthenticode = true; return; } } if (ConsoleHelper.ReadInput("Configuration missing for jsign/signtool/osslsigncode, continue without signing executables?", "Y", "n") == "Y") { _useAuthenticodeSigning = false; _useJsignToolForAuthenticode = false; return; } throw new Exception("Configuration is not set up for jsign/signtool/osslsigncode"); } } /// /// Cache value for checking if codesign is possible /// private bool? _useCodeSignSigning; /// /// Checks if codesign is enabled /// public void ToggleSignCodeSigning() { if (!_useCodeSignSigning.HasValue) { if (Input.DisableSignCode) { _useCodeSignSigning = false; return; } if (!OperatingSystem.IsMacOS()) _useCodeSignSigning = false; else if (Configuration.IsCodeSignPossible()) _useCodeSignSigning = true; else { if (ConsoleHelper.ReadInput("Configuration missing for signcode, continue without signing executables?", "Y", "n") == "Y") { _useCodeSignSigning = false; return; } throw new Exception("Configuration is not set up for signcode"); } } } /// /// Cache value for checking if docker build is enabled /// private bool? _dockerBuild; /// /// Checks if docker build is enabled /// public async Task ToggleDockerBuild() { if (!_dockerBuild.HasValue) { try { var res = await ProcessHelper.ExecuteWithOutput([Configuration.Commands.Docker!, "ps"], suppressStdErr: true); _dockerBuild = true; } catch { if (ConsoleHelper.ReadInput("Docker does not seem to be running, continue without docker builds?", "Y", "n") == "Y") { _dockerBuild = false; return; } throw new Exception("Docker is not running, and is required for building Docker images"); } } } /// /// Cache value for checking if notarize is enabled /// private bool? _useNotarizeSigning; /// /// Checks if notarize signing is enabled /// public void ToggleNotarizeSigning() { if (!_useNotarizeSigning.HasValue) { if (Input.DisableNotarizeSigning) { _useNotarizeSigning = false; return; } if (!OperatingSystem.IsMacOS()) _useNotarizeSigning = false; else if (Configuration.IsNotarizePossible()) _useNotarizeSigning = true; else { if (ConsoleHelper.ReadInput("Configuration missing for notarize, continue without notarizing executables?", "Y", "n") == "Y") { _useNotarizeSigning = false; return; } throw new Exception("Configuration is not set up for notarize"); } } } /// /// Cache value for checking if GPG signing is enabled /// private bool? _useGpgSigning; /// /// Checks if GPG signing is enabled /// public void ToggleGpgSigning() { if (!_useGpgSigning.HasValue) { if (Input.DisableGpgSigning) { _useGpgSigning = false; return; } if (Configuration.IsGpgPossible()) _useGpgSigning = true; else { if (ConsoleHelper.ReadInput("Configuration missing for gpg, continue without gpg signing packages?", "Y", "n") == "Y") { _useGpgSigning = false; return; } throw new Exception("Configuration is not set up for gpg"); } } } /// /// Cache value for checking if S3 upload is enabled /// private bool? _useS3Upload; /// /// Checks if S3 upload is enabled /// public void ToggleS3Upload() { if (!_useS3Upload.HasValue) { if (Input.DisableS3Upload) { _useS3Upload = false; return; } if (Configuration.IsAwsUploadPossible()) _useS3Upload = true; else { if (ConsoleHelper.ReadInput("Configuration missing for awscli, continue without uploading to S3?", "Y", "n") == "Y") { _useS3Upload = false; return; } throw new Exception("Configuration is not set up for awscli"); } } } /// /// Cache value for checking if Github upload is enabled /// private bool? _useGithubUpload; /// /// Checks if Github upload is enabled /// /// The release channel to use public void ToggleGithubUpload(ReleaseChannel channel) { if (!_useGithubUpload.HasValue) { if (Input.DisableGithubUpload || channel == ReleaseChannel.Debug || channel == ReleaseChannel.Nightly) { _useGithubUpload = false; return; } if (Configuration.IsGithubUploadPossible()) _useGithubUpload = true; else { if (ConsoleHelper.ReadInput("Configuration is missing a Github token, continue without uploading to Github?", "Y", "n") == "Y") { _useGithubUpload = false; return; } throw new Exception("Configuration is not set up for github releases"); } } } /// /// Cache value for checking if update server reload is enabled /// private bool? _useUpdateServerReload; /// /// Checks if update server reload is enabled /// public void ToggleUpdateServerReload() { if (!_useUpdateServerReload.HasValue) { if (Input.DisableUpdateServerReload) { _useUpdateServerReload = false; return; } if (Configuration.IsUpdateServerReloadPossible()) _useUpdateServerReload = true; else { if (ConsoleHelper.ReadInput("Configuration missing for update server, continue without reloading the update server?", "Y", "n") == "Y") { _useUpdateServerReload = false; return; } throw new Exception("Configuration is not set up for update server"); } } } /// /// Cache value for checking if forum posting is enabled /// private bool? _useDiscourseAnnounce; /// /// Checks if forum posting is enabled /// /// The release channel to use public void ToogleDiscourseAnnounce(ReleaseChannel channel) { if (!_useDiscourseAnnounce.HasValue) { if (Input.DisableDiscordAnnounce || channel == ReleaseChannel.Debug || channel == ReleaseChannel.Nightly) { _useDiscourseAnnounce = false; return; } if (Configuration.IsDiscourseAnnouncePossible()) _useDiscourseAnnounce = true; else { if (ConsoleHelper.ReadInput("Configuration missing for forum posting, continue without posting to the forum?", "Y", "n") == "Y") { _useDiscourseAnnounce = false; return; } throw new Exception("Configuration is not set up for forum posting"); } } } /// /// Returns a value indicating if codesign is enabled /// public bool UseCodeSignSigning => _useCodeSignSigning!.Value; /// /// Returns a value indicating if authenticode signing is enabled /// public bool UseAuthenticodeSigning => _useAuthenticodeSigning!.Value; /// /// Returns a value indicating if authenticode signing is enabled /// public bool UseJsingToolForAuthenticode => _useJsignToolForAuthenticode!.Value; /// /// Returns a value indicating if notarize is enabled /// public bool UseNotarizeSigning => _useNotarizeSigning!.Value; /// /// Returns a value indicating if GPG signing is enabled /// public bool UseGPGSigning => _useGpgSigning!.Value; /// /// Returns a value indicating if docker build is enabled /// public bool UseDockerBuild => _dockerBuild!.Value; /// /// Returns a value indicating if S3 upload is enabled /// public bool UseS3Upload => _useS3Upload!.Value; /// /// Returns a value indicating if Github upload is enabled /// public bool UseGithubUpload => _useGithubUpload!.Value; /// /// Returns a value indicating if update server reload is enabled /// public bool UseUpdateServerReload => _useUpdateServerReload!.Value; /// /// Returns a value indicating if forum posting is enabled /// public bool UseForumPosting => _useDiscourseAnnounce!.Value; /// /// Gets the MacOS app bundle name /// public string MacOSAppName => Input.MacOSAppName; /// /// The docker repository to use /// public string DockerRepo => Input.DockerRepo; /// /// Gets a value indicating if pushing should be enabled /// public bool PushToDocker => !Input.DisableDockerPush; /// /// Decrypts the password file and returns the PFX password /// /// Password for the password file /// The Authenticode password private string GetAuthenticodePassword(string keyfilepassword) => EncryptionHelper.DecryptPasswordFile(Configuration.ConfigFiles.AuthenticodePasswordFile, keyfilepassword).Trim(); /// /// Performs authenticode signing if enabled /// /// The file to sign /// An awaitable task public Task AuthenticodeSign(string file) { if (!UseAuthenticodeSigning) return Task.CompletedTask; if (UseJsingToolForAuthenticode) return ProcessRunner.JsignCodeSign(Configuration.Commands.JSign!, Input.SignkeyPin, file); else return ProcessRunner.OsslCodeSign( Configuration.Commands.SignCode!, Configuration.ConfigFiles.AuthenticodePfxFile, PfxPassword, file ); } /// /// Performs codesign on the given file /// /// The file to sign /// A value indicating if deep signing should be used /// The entitlements to apply /// An awaitable task public Task Codesign(string file, bool deep, string entitlements) => UseCodeSignSigning ? ProcessRunner.MacOSCodeSign( Configuration.Commands.Codesign!, Configuration.ConfigFiles.CodesignIdentity, entitlements, file, deep ) : Task.CompletedTask; /// /// Verifies the codesign of the given file or app bundle /// /// The file to verify /// An awaitable task public Task VerifyCodeSign(string file) => UseCodeSignSigning ? ProcessRunner.MacOSVerifyCodeSign( Configuration.Commands.Codesign!, file ) : Task.CompletedTask; /// /// Performs productsign on the given file /// /// The file to sign /// An awaitable task public Task Productsign(string file) => UseCodeSignSigning ? ProcessRunner.MacOSProductSign( Configuration.Commands.Productsign!, Configuration.ConfigFiles.CodesignIdentity, file ) : Task.CompletedTask; } }