Skip to content

Commit 3af173a

Browse files
committed
v1.0.4
- Password encryption/decryption added using DPAPI on Win and equivalent for Mac.
1 parent ae0f347 commit 3af173a

File tree

4 files changed

+186
-12
lines changed

4 files changed

+186
-12
lines changed

CLARiNET.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
<PackageIcon>CLARiNET.ico</PackageIcon>
88
<PackageIconUrl />
99
<ApplicationIcon>CLARiNET.ico</ApplicationIcon>
10-
<Version>1.0.2</Version>
10+
<Version>1.0.4</Version>
1111
</PropertyGroup>
1212

1313
<ItemGroup>
1414
<PackageReference Include="CommandLineParser" Version="2.8.0" />
15+
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="5.0.9" />
16+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
17+
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="5.0.0" />
1518
</ItemGroup>
1619

1720
<ItemGroup>

CommandLineOptions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,15 @@ public class Options
2424
[Value(index: 4, MetaName = "Username", Required = false, HelpText = "Username")]
2525
public string Username { get; set; }
2626

27-
[Value(index: 5, MetaName = "Password", Required = false, HelpText = "Password")]
27+
[Value(index: 5, MetaName = "Password", Required = false, HelpText = "Password (must be encrypted using the -e option)")]
2828
public string Password { get; set; }
2929

30-
[Option('e', "environments", Required = false,
30+
[Option('e', "encrypt", Required = false,
31+
HelpText =
32+
"Encrypt a password for use on the command line")]
33+
public bool Encrypt { get; set; }
34+
35+
[Option('w', "wdenvironments", Required = false,
3136
HelpText =
3237
"Display the Workday environments and their associated numbers")]
3338
public bool PrintEnvironments { get; set; }

Crypto.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System;
2+
using System.Text;
3+
using System.Security.Cryptography;
4+
using System.Runtime.InteropServices;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.AspNetCore.DataProtection;
7+
using System.IO;
8+
using System.Security.Cryptography.X509Certificates;
9+
10+
namespace CLARiNET
11+
{
12+
class Crypto
13+
{
14+
public static string Protect(string stringToEncrypt)
15+
{
16+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
17+
{
18+
return Convert.ToBase64String(
19+
ProtectedData.Protect(
20+
Encoding.UTF8.GetBytes(stringToEncrypt)
21+
, null
22+
, DataProtectionScope.CurrentUser));
23+
}
24+
else
25+
{
26+
return Convert.ToBase64String(
27+
ProtectMac(stringToEncrypt)
28+
);
29+
}
30+
}
31+
32+
public static string Unprotect(string encryptedString)
33+
{
34+
if (String.IsNullOrEmpty(encryptedString))
35+
{
36+
return "";
37+
}
38+
39+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
40+
{
41+
return Encoding.UTF8.GetString(
42+
ProtectedData.Unprotect(
43+
Convert.FromBase64String(encryptedString)
44+
, null
45+
, DataProtectionScope.CurrentUser));
46+
}
47+
else
48+
{
49+
return Encoding.UTF8.GetString(
50+
UnProtectMac(Convert.FromBase64String(encryptedString)));
51+
}
52+
}
53+
54+
// Credit: Oleg Batashov
55+
// https://simplecodesoftware.com/articles/how-to-encrypt-data-on-macos-without-dpapi
56+
private static IDataProtector MacEncryption()
57+
{
58+
IServiceCollection serviceCollection = new ServiceCollection();
59+
ConfigureServices(serviceCollection);
60+
IDataProtector dataProtector = serviceCollection
61+
.BuildServiceProvider()
62+
.GetDataProtector(purpose: "MacOsEncryption");
63+
64+
return dataProtector;
65+
}
66+
67+
private static byte[] ProtectMac(string stringToEncrypt)
68+
{
69+
return MacEncryption().Protect(Encoding.UTF8.GetBytes(stringToEncrypt));
70+
}
71+
72+
private static byte[] UnProtectMac(byte[] encryptedBytes)
73+
{
74+
return MacEncryption().Unprotect(encryptedBytes);
75+
}
76+
77+
private static void ConfigureServices(IServiceCollection serviceCollection)
78+
{
79+
X509Certificate2 cert = SetupDataProtectionCertificate();
80+
81+
serviceCollection.AddDataProtection()
82+
.PersistKeysToFileSystem(new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory))
83+
.SetApplicationName("CLARiNET")
84+
.ProtectKeysWithCertificate(cert);
85+
}
86+
87+
static X509Certificate2 SetupDataProtectionCertificate()
88+
{
89+
string subjectName = "CN=CLARiNET Data Protection Certificate";
90+
string subjectNameFind = subjectName.Substring(3);
91+
92+
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadOnly))
93+
{
94+
X509Certificate2Collection certificateCollection = store.Certificates.Find(X509FindType.FindBySubjectName,
95+
subjectNameFind,
96+
// self-signed certificate won't pass X509 chain validation
97+
validOnly: false);
98+
if (certificateCollection.Count > 0)
99+
{
100+
return certificateCollection[0];
101+
}
102+
103+
X509Certificate2 certificate = CreateSelfSignedDataProtectionCertificate(subjectName);
104+
InstallCertificateAsNonExportable(certificate);
105+
return certificate;
106+
}
107+
}
108+
109+
static X509Certificate2 CreateSelfSignedDataProtectionCertificate(string subjectName)
110+
{
111+
using (RSA rsa = RSA.Create(2048))
112+
{
113+
CertificateRequest request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256,
114+
RSASignaturePadding.Pkcs1);
115+
X509Certificate2 cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddYears(50));
116+
return cert;
117+
}
118+
}
119+
120+
static void InstallCertificateAsNonExportable(X509Certificate2 cert)
121+
{
122+
byte[] rawData = cert.Export(X509ContentType.Pkcs12, password: "CLARiNET" );
123+
X509Certificate2 certPersistKey = new X509Certificate2(rawData, "CLARiNET", X509KeyStorageFlags.PersistKeySet);
124+
125+
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
126+
{
127+
store.Open(OpenFlags.MaxAllowed | OpenFlags.ReadWrite);
128+
store.Add(certPersistKey);
129+
store.Close();
130+
}
131+
}
132+
}
133+
}

Program.cs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,33 @@ static void Main(string[] args)
3333
return;
3434
}
3535

36+
if (options.Encrypt)
37+
{
38+
Console.WriteLine("Enter a password to encrypt:\n");
39+
string pass = PasswordPrompt();
40+
string encPass = "";
41+
try
42+
{
43+
encPass = Crypto.Protect(pass);
44+
}
45+
// Perform a retry
46+
catch
47+
{
48+
encPass = Crypto.Protect(pass);
49+
}
50+
Console.WriteLine("\n\n" + encPass);
51+
Console.WriteLine("\n");
52+
return;
53+
}
54+
3655
if (options.PrintEnvironments)
3756
{
3857
PrintEnvironments(envs);
3958
return;
4059
}
4160

4261
// CLAR file
43-
if (options.ClarFile == null)
62+
if (String.IsNullOrEmpty(options.ClarFile))
4463
{
4564
// Check for a single CLAR file in this directory.
4665
string[] files = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.clar");
@@ -58,15 +77,15 @@ static void Main(string[] args)
5877
}
5978

6079
// Collection Name
61-
if (options.CollectionName == null)
80+
if (String.IsNullOrEmpty(options.CollectionName))
6281
{
6382
Console.WriteLine("Enter the Workday Cloud Collection name:\n");
6483
options.CollectionName = Console.ReadLine().Trim();
6584
Console.WriteLine("");
6685
}
6786

6887
// Environment Number
69-
if (options.EnvNum == null)
88+
if (String.IsNullOrEmpty(options.EnvNum))
7089
{
7190
do
7291
{
@@ -98,27 +117,31 @@ static void Main(string[] args)
98117
}
99118

100119
// Tenant
101-
if (options.Tenant == null)
120+
if (String.IsNullOrEmpty(options.Tenant))
102121
{
103122
Console.WriteLine("Enter the tenant:\n");
104123
options.Tenant = Console.ReadLine().Trim();
105124
Console.WriteLine("");
106125
}
107126

108127
// Username
109-
if (options.Username == null)
128+
if (String.IsNullOrEmpty(options.Username))
110129
{
111130
Console.WriteLine("Enter the username:\n");
112131
options.Username = Console.ReadLine().Trim();
113132
Console.WriteLine("");
114133
}
115134

116135
// Password
117-
if (options.Password == null)
136+
if (String.IsNullOrEmpty(options.Password))
118137
{
119138
Console.WriteLine("Enter the password (will not be displayed):\n");
120139
options.Password = PasswordPrompt();
121140
}
141+
else
142+
{
143+
options.Password = Crypto.Unprotect(options.Password);
144+
}
122145

123146
Console.WriteLine("\n\nDeploying the CLAR and awaiting the result...\n\n");
124147

@@ -127,12 +150,22 @@ static void Main(string[] args)
127150
options.Username = options.Username + "@" + options.Tenant;
128151
url = url.Replace("{host}", host) + options.CollectionName;
129152
string result = WDWebService.CallRest(options.Tenant, options.Username, options.Password, url, "PUT", bytes);
130-
Console.WriteLine("Result:\n");
131-
Console.WriteLine(result);
153+
154+
if (result.IndexOf("<?xml") < 0)
155+
{
156+
Console.WriteLine("No XML response detected. Workday may be unavailable or your parameters are incorrect.");
157+
}
158+
else
159+
{
160+
Console.WriteLine("Result:\n");
161+
Console.WriteLine(result);
162+
Console.WriteLine("\n");
163+
}
132164
}
133165
catch (Exception ex)
134166
{
135-
Console.WriteLine(ex.Message);
167+
Console.WriteLine("\n\n" + ex.Message);
168+
Console.WriteLine("\n");
136169
}
137170
}
138171

0 commit comments

Comments
 (0)