diff --git a/util/Setup/AppIdBuilder.cs b/util/Setup/AppIdBuilder.cs new file mode 100644 index 0000000000..23177e98d8 --- /dev/null +++ b/util/Setup/AppIdBuilder.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; + +namespace Bit.Setup +{ + public class AppIdBuilder + { + public AppIdBuilder(string url) + { + Url = url; + } + + public string Url { get; private set; } + + public void Build() + { + Console.WriteLine("Building FIDO U2F app id."); + Directory.CreateDirectory("/bitwarden/web/"); + using(var sw = File.CreateText("/bitwarden/web/app-id.json")) + { + sw.Write($@"{{ + ""trustedFacets"": [ + {{ + ""version"": {{ + ""major"": 1, + ""minor"": 0 + }}, + ""ids"": [ + ""{Url}"", + ""ios:bundle-id:com.8bit.bitwarden"", + ""android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI"" + ] + }} + ] +}}"); + } + } + } +} diff --git a/util/Setup/AppSettingsBuilder.cs b/util/Setup/AppSettingsBuilder.cs new file mode 100644 index 0000000000..6497d8e12f --- /dev/null +++ b/util/Setup/AppSettingsBuilder.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; + +namespace Bit.Setup +{ + public class AppSettingsBuilder + { + public AppSettingsBuilder(string url, string domain) + { + Url = url; + Domain = domain; + } + + public string Url { get; private set; } + public string Domain { get; private set; } + + public void Build() + { + Console.WriteLine("Building app settings."); + Directory.CreateDirectory("/bitwarden/web/"); + using(var sw = File.CreateText("/bitwarden/web/settings.js")) + { + sw.Write($@"// Config Parameters +// Parameter:Url={Url} +// Parameter:Domain={Domain} + +var bitwardenAppSettings = {{ + apiUri: ""{Url}/api"", + identityUri: ""{Url}/identity"", + iconsUri: ""{Url}/icons"", + stripeKey: null, + braintreeKey: null, + whitelistDomains: [""{Domain}""], + selfHosted: true +}};"); + } + } + } +} diff --git a/util/Setup/EnvironmentFileBuilder.cs b/util/Setup/EnvironmentFileBuilder.cs new file mode 100644 index 0000000000..8443f9a3d2 --- /dev/null +++ b/util/Setup/EnvironmentFileBuilder.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; + +namespace Bit.Setup +{ + public class EnvironmentFileBuilder + { + public string Url { get; set; } + public string Domain { get; set; } + public string IdentityCertPassword { get; set; } + public Guid? InstallationId { get; set; } + public string InstallationKey { get; set; } + public bool Push { get; set; } + public string DatabasePassword { get; set; } + public string OutputDirectory { get; set; } + + public void Build() + { + Console.WriteLine("Building docker environment override files."); + Directory.CreateDirectory("/bitwarden/env/"); + var dbConnectionString = Helpers.MakeSqlConnectionString("mssql", "vault", "sa", DatabasePassword); + + using(var sw = File.CreateText("/bitwarden/env/global.override.env")) + { + sw.Write($@"globalSettings__baseServiceUri__vault={Url} +globalSettings__baseServiceUri__api={Url}/api +globalSettings__baseServiceUri__identity={Url}/identity +globalSettings__sqlServer__connectionString=""{dbConnectionString}"" +globalSettings__identityServer__certificatePassword={IdentityCertPassword} +globalSettings__attachment__baseDirectory={OutputDirectory}/core/attachments +globalSettings__attachment__baseUrl={Url}/attachments +globalSettings__dataProtection__directory={OutputDirectory}/core/aspnet-dataprotection +globalSettings__logDirectory={OutputDirectory}/core/logs +globalSettings__licenseDirectory={OutputDirectory}/core/licenses +globalSettings__duo__aKey={Helpers.SecureRandomString(64, alpha: true, numeric: true)} +globalSettings__installation__id={InstallationId} +globalSettings__installation__key={InstallationKey} +globalSettings__yubico__clientId=REPLACE +globalSettings__yubico__key=REPLACE +globalSettings__mail__replyToEmail=no-reply@{Domain} +globalSettings__mail__smtp__host=REPLACE +globalSettings__mail__smtp__username=REPLACE +globalSettings__mail__smtp__password=REPLACE +globalSettings__mail__smtp__ssl=true +globalSettings__mail__smtp__port=587 +globalSettings__mail__smtp__useDefaultCredentials=false +globalSettings__disableUserRegistration=false"); + + if(!Push) + { + sw.Write(@" +globalSettings__pushRelayBaseUri=REPLACE"); + } + } + + using(var sw = File.CreateText("/bitwarden/env/mssql.override.env")) + { + sw.Write($@"ACCEPT_EULA=Y +MSSQL_PID=Express +SA_PASSWORD={DatabasePassword}"); + } + } + } +} diff --git a/util/Setup/Helpers.cs b/util/Setup/Helpers.cs index db0e230342..906288baac 100644 --- a/util/Setup/Helpers.cs +++ b/util/Setup/Helpers.cs @@ -3,7 +3,6 @@ using System.Data.SqlClient; using System.IO; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; namespace Bit.Setup { diff --git a/util/Setup/NginxConfigBuilder.cs b/util/Setup/NginxConfigBuilder.cs new file mode 100644 index 0000000000..29a5d5e0cf --- /dev/null +++ b/util/Setup/NginxConfigBuilder.cs @@ -0,0 +1,205 @@ +using System; +using System.IO; + +namespace Bit.Setup +{ + public class NginxConfigBuilder + { + private const string SslCiphers = + "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" + + "DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:" + + "ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:" + + "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:" + + "AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:@STRENGTH"; + + public NginxConfigBuilder(string domain, bool ssl, bool selfSignedSsl, bool letsEncrypt) + { + Domain = domain; + Ssl = ssl; + SelfSignedSsl = selfSignedSsl; + LetsEncrypt = letsEncrypt; + } + + public NginxConfigBuilder(string domain) + { + Domain = domain; + } + + public bool Ssl { get; private set; } + public bool SelfSignedSsl { get; private set; } + public bool LetsEncrypt { get; private set; } + public string Domain { get; private set; } + public bool DiffieHellman { get; private set; } + public bool Trusted { get; private set; } + + public void BuildForInstaller() + { + Build(true); + } + + public void BuildForUpdater() + { + Build(false); + } + + public bool UpdateContext() + { + if(!File.Exists("/bitwarden/nginx/default.conf")) + { + return false; + } + + var confContent = File.ReadAllText("/bitwarden/nginx/default.conf"); + Ssl = confContent.Contains("listen 443 ssl http2;"); + SelfSignedSsl = confContent.Contains("/etc/ssl/self/"); + LetsEncrypt = !SelfSignedSsl && confContent.Contains("/etc/letsencrypt/live/"); + DiffieHellman = confContent.Contains("/dhparam.pem;"); + Trusted = confContent.Contains("ssl_trusted_certificate "); + return true; + } + + private void Build(bool installer) + { + if(installer) + { + if(Ssl && !SelfSignedSsl && !LetsEncrypt) + { + Console.Write("(!) Use Diffie Hellman ephemeral parameters for SSL (requires dhparam.pem)? (y/n): "); + DiffieHellman = Console.ReadLine().ToLowerInvariant() == "y"; + } + else + { + DiffieHellman = LetsEncrypt; + } + + if(Ssl && !SelfSignedSsl && !LetsEncrypt) + { + Console.Write("(!) Is this a trusted SSL certificate (requires ca.crt)? (y/n): "); + Trusted = Console.ReadLine().ToLowerInvariant() == "y"; + } + else + { + Trusted = LetsEncrypt; + } + } + + Directory.CreateDirectory("/bitwarden/nginx/"); + + var sslPath = LetsEncrypt ? $"/etc/letsencrypt/live/{Domain}" : + SelfSignedSsl ? $"/etc/ssl/self/{Domain}" : $"/etc/ssl/{Domain}"; + var certFile = LetsEncrypt ? "fullchain.pem" : "certificate.crt"; + var keyFile = LetsEncrypt ? "privkey.pem" : "private.key"; + var caFile = LetsEncrypt ? "fullchain.pem" : "ca.crt"; + + Console.WriteLine("Building nginx config."); + using(var sw = File.CreateText("/bitwarden/nginx/default.conf")) + { + sw.WriteLine($@"# Config Parameters +# Parameter:Ssl={Ssl} +# Parameter:SelfSignedSsl={SelfSignedSsl} +# Parameter:LetsEncrypt={LetsEncrypt} +# Parameter:Domain={Domain} +# Parameter:DiffieHellman={DiffieHellman} +# Parameter:Trusted={Trusted} + +server {{ + listen 80 default_server; + listen [::]:80 default_server; + server_name {Domain};"); + + if(Ssl) + { + sw.WriteLine($@" return 301 https://$server_name$request_uri; +}} + +server {{ + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {Domain}; + + ssl_certificate {sslPath}/{certFile}; + ssl_certificate_key {sslPath}/{keyFile}; + + ssl_session_timeout 30m; + ssl_session_cache shared:SSL:20m; + ssl_session_tickets off;"); + + if(DiffieHellman) + { + sw.WriteLine($@" + # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits + ssl_dhparam {sslPath}/dhparam.pem;"); + } + + sw.WriteLine($@" + # SSL protocols TLS v1~TLSv1.2 are allowed. Disabed SSLv3 + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + # Disabled insecure ciphers suite. For example, MD5, DES, RC4, PSK + ssl_ciphers ""{SslCiphers}""; + # enables server-side protection from BEAST attacks + ssl_prefer_server_ciphers on;"); + + if(Trusted) + { + sw.WriteLine($@" + # OCSP Stapling --- + # fetch OCSP records from URL in ssl_certificate and cache them + ssl_stapling on; + ssl_stapling_verify on; + + ## verify chain of trust of OCSP response using Root CA and Intermediate certs + ssl_trusted_certificate {sslPath}/{caFile}; + + resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s; + + # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age + add_header Strict-Transport-Security max-age=15768000;"); + } + } + + sw.WriteLine($@" + # X-Frame-Options is to prevent from click-jacking attack + #add_header X-Frame-Options SAMEORIGIN; + + # disable content-type sniffing on some browsers. + add_header X-Content-Type-Options nosniff; + + # This header enables the Cross-site scripting (XSS) filter + add_header X-XSS-Protection ""1; mode=block""; + + # This header controls what referrer information is shared + add_header Referrer-Policy same-origin; + + # Content-Security-Policy is set via meta tag on the website so it is not included here"); + + sw.WriteLine($@" + location / {{ + proxy_pass http://web/; + }} + + location = /app-id.json {{ + proxy_pass http://web/app-id.json; + proxy_hide_header Content-Type; + add_header Content-Type $fido_content_type; + }} + + location /attachments/ {{ + proxy_pass http://attachments/; + }} + + location /api/ {{ + proxy_pass http://api/; + }} + + location /identity/ {{ + proxy_pass http://identity/; + }} + + location /icons/ {{ + proxy_pass http://icons/; + }} +}}"); + } + } + } +} diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs index e1db5bd662..c4683485ae 100644 --- a/util/Setup/Program.cs +++ b/util/Setup/Program.cs @@ -15,16 +15,13 @@ namespace Bit.Setup { private static string[] _args = null; private static IDictionary _parameters = null; - private static string _outputDir = "/etc/bitwarden"; private static string _domain = null; - private static string _url = null; private static string _identityCertPassword = null; private static bool _ssl = false; private static bool _selfSignedSsl = false; private static bool _letsEncrypt = false; private static Guid? _installationId = null; private static string _installationKey = null; - private static bool _push = false; public static void Main(string[] args) { @@ -50,8 +47,8 @@ namespace Bit.Setup private static void Install() { - _outputDir = _parameters.ContainsKey("out") ? - _parameters["out"].ToLowerInvariant() : _outputDir; + var outputDir = _parameters.ContainsKey("out") ? + _parameters["out"].ToLowerInvariant() : "/etc/bitwarden"; _domain = _parameters.ContainsKey("domain") ? _parameters["domain"].ToLowerInvariant() : "localhost"; _letsEncrypt = _parameters.ContainsKey("letsencrypt") ? @@ -78,15 +75,31 @@ namespace Bit.Setup _identityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true); MakeCerts(); - _url = _ssl ? $"https://{_domain}" : $"http://{_domain}"; - BuildNginxConfig(); + var url = _ssl ? $"https://{_domain}" : $"http://{_domain}"; + var nginxBuilder = new NginxConfigBuilder(_domain, _ssl, _selfSignedSsl, _letsEncrypt); + nginxBuilder.BuildForInstaller(); Console.Write("(!) Do you want to use push notifications? (y/n): "); - _push = Console.ReadLine().ToLowerInvariant() == "y"; + var push = Console.ReadLine().ToLowerInvariant() == "y"; - BuildEnvironmentFiles(); - BuildAppSettingsFiles(); - BuildAppId(); + var environmentFileBuilder = new EnvironmentFileBuilder + { + DatabasePassword = Helpers.SecureRandomString(32), + Domain = _domain, + IdentityCertPassword = _identityCertPassword, + InstallationId = _installationId, + InstallationKey = _installationKey, + OutputDirectory = outputDir, + Push = push, + Url = url + }; + environmentFileBuilder.Build(); + + var appSettingsBuilder = new AppSettingsBuilder(url, _domain); + appSettingsBuilder.Build(); + + var appIdBuilder = new AppIdBuilder(url); + appIdBuilder.Build(); } private static void Update() @@ -95,6 +108,10 @@ namespace Bit.Setup { MigrateDatabase(); } + else + { + RebuildConfigs(); + } } private static void PrintEnvironment() @@ -132,20 +149,6 @@ namespace Bit.Setup command.ExecuteNonQuery(); } - // Namespace changed, so migrate DbUp script names - using(var connection = new SqlConnection(vaultConnectionString)) - { - var command = new SqlCommand( - "IF ((SELECT COUNT(1) FROM INFORMATION_SCHEMA.TABLES " + - "WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'Migration') > 0) " + - "UPDATE [dbo].[Migration] " + - "SET [ScriptName] = REPLACE([ScriptName], 'Setup.DbScripts.', 'Bit.Setup.DbScripts.') " + - "WHERE [ScriptName] LIKE 'Setup.DbScripts.%';", - connection); - command.Connection.Open(); - command.ExecuteNonQuery(); - } - var upgrader = DeployChanges.To .SqlDatabase(vaultConnectionString) .JournalToSqlTable("dbo", "Migration") @@ -245,226 +248,25 @@ namespace Bit.Setup $"-in identity.crt -certfile identity.crt -passout pass:{_identityCertPassword}"); } - private static void BuildNginxConfig() + private static void RebuildConfigs() { - Directory.CreateDirectory("/bitwarden/nginx/"); - var sslCiphers = "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" + - "DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:" + - "ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:" + - "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:" + - "AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:@STRENGTH"; - - var dh = _letsEncrypt; - if(_ssl && !_selfSignedSsl && !_letsEncrypt) + var url = Helpers.GetValueFronEnvFile("global", "globalSettings__baseServiceUri__vault"); + if(!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) { - Console.Write("(!) Use Diffie Hellman ephemeral parameters for SSL (requires dhparam.pem)? (y/n): "); - dh = Console.ReadLine().ToLowerInvariant() == "y"; + Console.WriteLine("Unable to determine existing installation url."); + return; } + var domain = uri.Host; - var trusted = _letsEncrypt; - if(_ssl && !_selfSignedSsl && !_letsEncrypt) - { - Console.Write("(!) Is this a trusted SSL certificate (requires ca.crt)? (y/n): "); - trusted = Console.ReadLine().ToLowerInvariant() == "y"; - } + var nginxBuilder = new NginxConfigBuilder(domain); + nginxBuilder.UpdateContext(); + nginxBuilder.BuildForUpdater(); - var sslPath = _letsEncrypt ? $"/etc/letsencrypt/live/{_domain}" : - _selfSignedSsl ? $"/etc/ssl/self/{_domain}" : $"/etc/ssl/{_domain}"; - var certFile = _letsEncrypt ? "fullchain.pem" : "certificate.crt"; - var keyFile = _letsEncrypt ? "privkey.pem" : "private.key"; - var caFile = _letsEncrypt ? "fullchain.pem" : "ca.crt"; + var appSettingsBuilder = new AppSettingsBuilder(url, domain); + appSettingsBuilder.Build(); - Console.WriteLine("Building nginx config."); - using(var sw = File.CreateText("/bitwarden/nginx/default.conf")) - { - sw.WriteLine($@"server {{ - listen 80 default_server; - listen [::]:80 default_server; - server_name {_domain};"); - - if(_ssl) - { - sw.WriteLine($@" return 301 https://$server_name$request_uri; -}} - -server {{ - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name {_domain}; - - ssl_certificate {sslPath}/{certFile}; - ssl_certificate_key {sslPath}/{keyFile}; - - ssl_session_timeout 30m; - ssl_session_cache shared:SSL:20m; - ssl_session_tickets off;"); - - if(dh) - { - sw.WriteLine($@" - # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits - ssl_dhparam {sslPath}/dhparam.pem;"); - } - - sw.WriteLine($@" - # SSL protocols TLS v1~TLSv1.2 are allowed. Disabed SSLv3 - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - # Disabled insecure ciphers suite. For example, MD5, DES, RC4, PSK - ssl_ciphers ""{sslCiphers}""; - # enables server-side protection from BEAST attacks - ssl_prefer_server_ciphers on;"); - - if(trusted) - { - sw.WriteLine($@" - # OCSP Stapling --- - # fetch OCSP records from URL in ssl_certificate and cache them - ssl_stapling on; - ssl_stapling_verify on; - - ## verify chain of trust of OCSP response using Root CA and Intermediate certs - ssl_trusted_certificate {sslPath}/{caFile}; - - resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s; - - # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age - add_header Strict-Transport-Security max-age=15768000;"); - } - } - - sw.WriteLine($@" - # X-Frame-Options is to prevent from click-jacking attack - #add_header X-Frame-Options SAMEORIGIN; - - # disable content-type sniffing on some browsers. - add_header X-Content-Type-Options nosniff; - - # This header enables the Cross-site scripting (XSS) filter - add_header X-XSS-Protection ""1; mode=block""; - - # This header controls what referrer information is shared - add_header Referrer-Policy same-origin; - - # Content-Security-Policy is set via meta tag on the website so it is not included here"); - - sw.WriteLine($@" - location / {{ - proxy_pass http://web/; - }} - - location = /app-id.json {{ - proxy_pass http://web/app-id.json; - proxy_hide_header Content-Type; - add_header Content-Type $fido_content_type; - }} - - location /attachments/ {{ - proxy_pass http://attachments/; - }} - - location /api/ {{ - proxy_pass http://api/; - }} - - location /identity/ {{ - proxy_pass http://identity/; - }} - - location /icons/ {{ - proxy_pass http://icons/; - }} -}}"); - } - } - - private static void BuildEnvironmentFiles() - { - Console.WriteLine("Building docker environment override files."); - Directory.CreateDirectory("/bitwarden/env/"); - var dbPass = Helpers.SecureRandomString(32); - var dbConnectionString = Helpers.MakeSqlConnectionString("mssql", "vault", "sa", dbPass); - - using(var sw = File.CreateText("/bitwarden/env/global.override.env")) - { - sw.Write($@"globalSettings__baseServiceUri__vault={_url} -globalSettings__baseServiceUri__api={_url}/api -globalSettings__baseServiceUri__identity={_url}/identity -globalSettings__sqlServer__connectionString=""{dbConnectionString}"" -globalSettings__identityServer__certificatePassword={_identityCertPassword} -globalSettings__attachment__baseDirectory={_outputDir}/core/attachments -globalSettings__attachment__baseUrl={_url}/attachments -globalSettings__dataProtection__directory={_outputDir}/core/aspnet-dataprotection -globalSettings__logDirectory={_outputDir}/core/logs -globalSettings__licenseDirectory={_outputDir}/core/licenses -globalSettings__duo__aKey={Helpers.SecureRandomString(64, alpha: true, numeric: true)} -globalSettings__installation__id={_installationId} -globalSettings__installation__key={_installationKey} -globalSettings__yubico__clientId=REPLACE -globalSettings__yubico__key=REPLACE -globalSettings__mail__replyToEmail=no-reply@{_domain} -globalSettings__mail__smtp__host=REPLACE -globalSettings__mail__smtp__username=REPLACE -globalSettings__mail__smtp__password=REPLACE -globalSettings__mail__smtp__ssl=true -globalSettings__mail__smtp__port=587 -globalSettings__mail__smtp__useDefaultCredentials=false -globalSettings__disableUserRegistration=false"); - - if(!_push) - { - sw.Write(@" -globalSettings__pushRelayBaseUri=REPLACE"); - } - } - - using(var sw = File.CreateText("/bitwarden/env/mssql.override.env")) - { - sw.Write($@"ACCEPT_EULA=Y -MSSQL_PID=Express -SA_PASSWORD={dbPass}"); - } - } - - private static void BuildAppSettingsFiles() - { - Console.WriteLine("Building app settings."); - Directory.CreateDirectory("/bitwarden/web/"); - using(var sw = File.CreateText("/bitwarden/web/settings.js")) - { - sw.Write($@"var bitwardenAppSettings = {{ - apiUri: ""{_url}/api"", - identityUri: ""{_url}/identity"", - iconsUri: ""{_url}/icons"", - stripeKey: null, - braintreeKey: null, - whitelistDomains: [""{_domain}""], - selfHosted: true -}};"); - } - } - - private static void BuildAppId() - { - Console.WriteLine("Building FIDO U2F app id."); - Directory.CreateDirectory("/bitwarden/web/"); - using(var sw = File.CreateText("/bitwarden/web/app-id.json")) - { - sw.Write($@"{{ - ""trustedFacets"": [ - {{ - ""version"": {{ - ""major"": 1, - ""minor"": 0 - }}, - ""ids"": [ - ""{_url}"", - ""ios:bundle-id:com.8bit.bitwarden"", - ""android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI"" - ] - }} - ] -}}"); - } + var appIdBuilder = new AppIdBuilder(url); + appIdBuilder.Build(); } private static IDictionary ParseParameters() @@ -509,7 +311,7 @@ SA_PASSWORD={dbPass}"); } process.Start(); - string result = process.StandardOutput.ReadToEnd(); + var result = process.StandardOutput.ReadToEnd(); process.WaitForExit(); return result; }