Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions HpToolsLauncher/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* ___________________________________________________________________
*/
using HpToolsLauncher.Properties;
using HpToolsLauncher.Utils;
using System;
using System.Collections.Generic;
using System.IO;
Expand Down Expand Up @@ -68,6 +69,13 @@ static void Main(string[] args)
ShowHelp();
return;
}

if (Encrypter.USE_STDIN_KEY.In(true, args))
{
Encrypter.Create();
args = args.Exclude(Encrypter.USE_STDIN_KEY, true);
}

for (int i = 0; i < args.Count(); i += 2)
{
string key = args[i].StartsWith("-") ? args[i].Substring(1) : args[i];
Expand Down
278 changes: 161 additions & 117 deletions HpToolsLauncher/Utils/Encrypter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/*
* Certain versions of software accessible here may contain branding from
* Hewlett-Packard Company (now HP Inc.) and Hewlett Packard Enterprise Company.
* This software was acquired by Micro Focus on September 1, 2017, and is now
Expand All @@ -7,163 +7,207 @@
* in nature, and the HP and Hewlett Packard Enterprise/HPE marks are the
* property of their respective owners.
* OpenText is a trademark of Open Text.
* __________________________________________________________________
* MIT License
* __________________________________________________________________
* MIT License
*
* Copyright 2012-2026 Open Text.
*
* The only warranties for products and services of Open Text and
* its affiliates and licensors ("Open Text") are as may be set forth
* in the express warranty statements accompanying such products and services.
* Nothing herein should be construed as constituting an additional warranty.
* Open Text shall not be liable for technical or editorial errors or
* omissions contained herein. The information contained herein is subject
* to change without notice.
* The only warranties for products and services of Open Text and
* its affiliates and licensors ("Open Text") are as may be set forth
* in the express warranty statements accompanying such products and services.
* Nothing herein should be construed as constituting an additional warranty.
* Open Text shall not be liable for technical or editorial errors or
* omissions contained herein. The information contained herein is subject
* to change without notice.
*
* Except as specifically indicated otherwise, this document contains
* confidential information and a valid license is required for possession,
* use or copying. If this work is provided to the U.S. Government,
* consistent with FAR 12.211 and 12.212, Commercial Computer Software,
* Computer Software Documentation, and Technical Data for Commercial Items are
* licensed to the U.S. Government under vendor's standard commercial license.
* Except as specifically indicated otherwise, this document contains
* confidential information and a valid license is required for possession,
* use or copying. If this work is provided to the U.S. Government,
* consistent with FAR 12.211 and 12.212, Commercial Computer Software,
* Computer Software Documentation, and Technical Data for Commercial Items are
* licensed to the U.S. Government under vendor's standard commercial license.
*
* 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.
* ___________________________________________________________________
* 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 System;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;

namespace HpToolsLauncher.Utils
{
public static class Encrypter
public sealed class Encrypter
{
private const string KEY_PATH = @"secrets/.hptoolslaunchersecret.key";
private static readonly RSACryptoServiceProvider _rsa;
public const string USE_STDIN_KEY = "--use-stdin-key";

// Singleton instance — null until Create() is called.
private static Encrypter _instance = null;

// Per-instance secure keys set once in the private constructor.
private readonly byte[] _aesKey;
private readonly byte[] _hmacKey;

static Encrypter()
// =========================================================
// 🏭 SINGLETON FACTORY
// =========================================================

/// <summary>
/// Creates the singleton. Must be called once from Main, before any
/// Encrypt / Decrypt call, passing the raw base64 key read from stdin
/// (when --use-stdin-key is present)
/// </summary>
public static void Create()
{
string secretKey = Environment.GetEnvironmentVariable("hptoolslauncher.key");
string keyPath = Environment.GetEnvironmentVariable("hptoolslauncher.rootpath");
if (_instance != null)
throw new InvalidOperationException("Encrypter is already initialized.");

if (string.IsNullOrEmpty(secretKey) || string.IsNullOrEmpty(keyPath))
using (StreamReader reader = new StreamReader(Console.OpenStandardInput()))
{
ConsoleWriter.WriteErrLine("No secretkey or root path was set. Any encrypt / decrypt action will be bypassed.");
return;
string base64Key = reader.ReadLine();
if (base64Key.IsNullOrWhiteSpace())
throw new CryptographicException("--use-stdin-key was specified but no key was provided via stdin.");

_instance = new Encrypter(base64Key.Trim());
}
}

keyPath += Path.DirectorySeparatorChar + KEY_PATH;
private Encrypter(string base64Key)
{
if (base64Key.IsNullOrWhiteSpace()) return; // legacy / no-key mode

string cnt;
try
{
cnt = File.ReadAllText(keyPath);
}
catch (IOException)
{
ConsoleWriter.WriteErrLine("Failed to open file with decryption key.");
throw new ArgumentException(
"Check the secret key for the hptoolslauncher in the secrets directory or force a new key pair.");
}
catch (SecurityException)
{
ConsoleWriter.WriteErrLine("You do not have access to open the file with the decryption key.");
throw new ArgumentException("Check the permissions for the secret key in the secrets directory.");
}
catch (UnauthorizedAccessException)
{
ConsoleWriter.WriteErrLine("You do not have access to open the file with the decryption key or something else has occurred.");
throw new ArgumentException("Check the permissions for the secret key in the secrets directory or the existence of the file.");
}
byte[] key = Convert.FromBase64String(base64Key.Trim());
if (key.Length != 64)
throw new CryptographicException("Invalid secure key length. Expected 64 bytes (base64-encoded).");

var pkXml = DecryptWithPwd(cnt, secretKey);
try
{
_rsa = new RSACryptoServiceProvider();
_rsa.FromXmlString(pkXml); // init
}
catch (CryptographicException)
{
ConsoleWriter.WriteErrLine("The cryptography provider could not be acquired.");
throw new ArgumentException("Try forcing a new key pair.");
}
catch (ArgumentNullException)
{
ConsoleWriter.WriteErrLine("No valid private key were provided for cryptography.");
throw new ArgumentException("Try forcing a new key pair generation.");
}
_aesKey = new byte[32];
_hmacKey = new byte[32];
Buffer.BlockCopy(key, 0, _aesKey, 0, 32);
Buffer.BlockCopy(key, 32, _hmacKey, 0, 32);
}

// =========================================================
// 🔒 PUBLIC STATIC API (callers are unchanged)
// =========================================================

/// <summary>
/// Decrypts the data with the node's private key.
/// Encrypts using AES-256-CBC + HMAC-SHA256 when a secure key was provided,
/// otherwise falls back to legacy AES-128-CBC.
/// </summary>
/// <param name="textToDecrypt"></param>
/// <returns></returns>
public static string Decrypt(string textToDecrypt)
public static string Encrypt(string plainText)
{
if (_rsa == null)
return textToDecrypt;
if (_instance == null || _instance._aesKey == null)
throw new CryptographicException("No secure key was provided. Use --use-stdin-key to provide a key via stdin.");

var encryptedBytes = Convert.FromBase64String(textToDecrypt);
byte[] text;
try
{
text = _rsa.Decrypt(encryptedBytes, false);
}
catch (CryptographicException)
{
ConsoleWriter.WriteErrLine("Failed to decrypt data using private key, try forcing a new public-private key pair.");
throw new ArgumentException("Decryption failed using private key.");
}
return _instance.EncryptSecure(plainText);
}

public static string Decrypt(string cipherText)
{
#if DEBUG
return cipherText; // used for troubleshooting and testing without needing to set up keys
#endif
if (cipherText.IsNullOrWhiteSpace())
return cipherText;
if (_instance == null || _instance._aesKey == null)
throw new CryptographicException("No secure key was provided. Use --use-stdin-key to provide a key via stdin.");

return Encoding.UTF8.GetString(text);
return _instance.DecryptSecure(cipherText);
}

/// <summary>
/// Internal usage only, used for private key decryption.
/// </summary>
/// <param name="textToDecrypt"></param>
/// <returns></returns>
private static string DecryptWithPwd(string textToDecrypt, string secretKey)
// =========================================================
// 🔐 SECURE MODE (AES-256-CBC + HMAC)
// =========================================================

private string EncryptSecure(string plainText)
{
var rijndaelCipher = new RijndaelManaged
using (Aes aes = Aes.Create())
{
BlockSize = 0x80,
KeySize = 0x100,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7
};
aes.Key = _aesKey;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.GenerateIV();
byte[] iv = aes.IV; // 16 bytes

var encryptedData = Convert.FromBase64String(textToDecrypt);
var pwdBytes = Encoding.UTF8.GetBytes(secretKey);
using (ICryptoTransform encryptor = aes.CreateEncryptor())
{
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] ciphertext = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);

var ivBytes = new byte[0x10];
Array.Copy(encryptedData, ivBytes, 16);
var cntBytes = new byte[encryptedData.Length - 16];
Array.Copy(encryptedData, 16, cntBytes, 0, cntBytes.Length);
// Layout: [ IV (16) | ciphertext | HMAC (32) ]
byte[] data = new byte[16 + ciphertext.Length];
Buffer.BlockCopy(iv, 0, data, 0, 16);
Buffer.BlockCopy(ciphertext, 0, data, 16, ciphertext.Length);

rijndaelCipher.Key = pwdBytes;
rijndaelCipher.IV = ivBytes;
using (HMACSHA256 h = new HMACSHA256(_hmacKey))
{
byte[] hmac = h.ComputeHash(data);

byte[] plainText;
try
{
plainText = rijndaelCipher.CreateDecryptor().TransformFinalBlock(cntBytes, 0, cntBytes.Length);
byte[] result = new byte[data.Length + 32];
Buffer.BlockCopy(data, 0, result, 0, data.Length);
Buffer.BlockCopy(hmac, 0, result, data.Length, 32);

return Convert.ToBase64String(result);
}
}
}
catch (CryptographicException)
}

private string DecryptSecure(string input)
{
byte[] buffer = Convert.FromBase64String(input);
// minimum: 16 (IV) + 1 block (16) + 32 (HMAC) = 64
if (buffer.Length < 64)
throw new CryptographicException("Invalid encrypted payload.");

int ciphertextLen = buffer.Length - 16 - 32;

byte[] iv = new byte[16];
byte[] ciphertext = new byte[ciphertextLen];
byte[] hmac = new byte[32];

Buffer.BlockCopy(buffer, 0, iv, 0, 16);
Buffer.BlockCopy(buffer, 16, ciphertext, 0, ciphertextLen);
Buffer.BlockCopy(buffer, buffer.Length - 32, hmac, 0, 32);

using (HMACSHA256 h = new HMACSHA256(_hmacKey))
{
ConsoleWriter.WriteErrLine("Failed to decrypt using AES, possibly master key have changed since the encryption.");
throw new ArgumentException(
"Try forcing a new public-private key pair on this node, by deselecting encryption in Node configurations.");
byte[] expected = h.ComputeHash(buffer, 0, buffer.Length - 32);

if (!ConstantTimeEquals(expected, hmac))
throw new CryptographicException("HMAC validation failed.");

using (Aes aes = Aes.Create())
{
aes.Key = _aesKey;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (ICryptoTransform decryptor = aes.CreateDecryptor())
{
byte[] plain = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
return Encoding.UTF8.GetString(plain);
}
}
}
}

// =========================================================
// 🛠 HELPERS
// =========================================================

return Encoding.UTF8.GetString(plainText);
private static bool ConstantTimeEquals(byte[] a, byte[] b)
{
if (a.Length != b.Length) return false;
int diff = 0;
for (int i = 0; i < a.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
}
}
Loading