I'm trying to encrypt text in Android and decrypt it in a C# application. My problem is that the title's error appears when I'm decrypting.
Android (encryption) side:
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Security;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.SecretKeySpec;
import org.spongycastle.jce.provider.BouncyCastleProvider;
public class AesHelper {
private SecretKeySpec key;
private byte[] input;
private ByteArrayOutputStream output;
private CipherOutputStream cipherOutput;
private Cipher encrypt;
public AesHelper(byte[] chosenKey, String plaintext) {
Security.addProvider(new BouncyCastleProvider());
key = new SecretKeySpec(chosenKey, "AES");
try {
encrypt = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
encrypt.init(Cipher.ENCRYPT_MODE, key);
input = plaintext.getBytes();
} catch (Exception e) {
Log.d("testclient", e.getMessage());
}
}
public byte[] encrypt() {
output = new ByteArrayOutputStream();
cipherOutput = new CipherOutputStream(output, encrypt);
try {
cipherOutput.write(input);
cipherOutput.close();
} catch (IOException e) {
Log.d("testclient", e.getMessage());
}
return output.toByteArray();
}
}
Invoked as follows:
String message = "TEST-TEST-TEST-TEST-TEST-TEST-TEST-TEST";
cryptHelper = new AesHelper(key, message);
byte[] cipherMessage = cryptHelper.encrypt();
String finalMessage = bytesToHex(cipherMessage);
C# (decryption) side:
public class AesHelper
{
private readonly Encoding _encoding;
private readonly IBlockCipher _blockCipher;
private PaddedBufferedBlockCipher _cipher;
public IBlockCipherPadding Padding { get; set; }
public AesHelper()
{
_blockCipher = new AesEngine();
_encoding = Encoding.UTF8;
}
public string Encrypt(string plain, byte[] key)
{
var result = BouncyCastleCrypto(true, _encoding.GetBytes(plain), key);
return result.AsHex();
}
public string Decrypt(string cipher, byte[] key)
{
var result = BouncyCastleCrypto(false, cipher.AsByteArray(), key);
return _encoding.GetString(result);
}
private byte[] BouncyCastleCrypto(bool forEncrypt, byte[] input, byte[] key)
{
try
{
_cipher = Padding == null
? new PaddedBufferedBlockCipher(new CbcBlockCipher(_blockCipher))
: new PaddedBufferedBlockCipher(new CbcBlockCipher(_blockCipher), Padding);
_cipher.Init(forEncrypt, new KeyParameter(key));
return _cipher.DoFinal(input);
}
catch (CryptoException ex)
{
throw new CryptoException(ex.Message);
}
}
}
Invoked as follows:
cryptoHelper = new AesHelper { Padding = new Pkcs7Padding() };
var decryptedMessage = cryptoHelper.Decrypt(message, _aesKey);
The thing is, if I encrypt the string
"TEST-TEST-TEST-TEST-TEST-TEST-TEST-TEST"
the decrypted string is
"�\a��{ٳ�]%'Ts�EST-TEST-TEST-TEST-TEST"
I really can't figure out what I'm doing wrong.
It appears to be the first block of output (16 bytes) that is wrong, which for CBC mode implies a different "initialization vector" (IV) was used for encryption and decryption. The Android code is actually generating a (random) IV automatically (when you call Cipher.init() with just a key), which you could retrieve using Cipher.getIV(). Alternatively you can explicitly specify an IvParameterSpec using a different init method.
However you do it, that same IV needs to be available to the decryption code, and you would then init _cipher like this in the C# code:
_cipher.Init(forEncrypt, new ParametersWithIV(new KeyParameter(key), iv));
Also please note that in the Android code:
plaintext.getBytes()
might cause you problems later. It's better to explicitly say UTF8:
plaintext.getBytes("UTF8")
Related
I have limited knowledge with PGP but came across code to use BouncyCastle to take care of it. I'm able to Encrypt/Decrypt a file but was asked to send an Encrypted and Signed file to an external vendor. When I use the following code I've found it does encrypt the file but when the vendor tries to decrypt they are getting an error gpg: Can't check signature: No public key. I don't know enough about PGP to know how to troubleshoot this properly. Looking for any suggestions to try.
Here is the code I'm using. The source and encrypted file path seem to be fine. The Public Key is pointing to the vendor's Public Key they provided me. The Private Key and Private Key Phrase are the Private Key file and password from the Key that I made for my company.
PGPEncryptDecrypt.EncryptAndSign(sourceFileFullPath,
encryptedFileFullPath,
publicKeyFileName,
privateKeyFileName,
privateKeyPhrase,
true);
This is the code in the PGPEncryptDecrypt class I pulled from an example.
#region Encrypt and Sign
public static void EncryptAndSign(string inputFile, string outputFile, string publicKeyFile, string privateKeyFile, string passPhrase, bool armor)
{
PGPEncryptionKeys encryptionKeys = new PGPEncryptionKeys(publicKeyFile, privateKeyFile, passPhrase);
if (!File.Exists(inputFile))
throw new FileNotFoundException(String.Format("Input file [{0}] does not exist.", inputFile));
if (!File.Exists(publicKeyFile))
throw new FileNotFoundException(String.Format("Public Key file [{0}] does not exist.", publicKeyFile));
if (!File.Exists(privateKeyFile))
throw new FileNotFoundException(String.Format("Private Key file [{0}] does not exist.", privateKeyFile));
if (String.IsNullOrEmpty(passPhrase))
throw new ArgumentNullException("Invalid Pass Phrase.");
if (encryptionKeys == null)
throw new ArgumentNullException("Encryption Key not found.");
using (Stream outputStream = File.Create(outputFile))
{
if (armor)
using (ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(outputStream))
{
OutputEncrypted(inputFile, armoredOutputStream, encryptionKeys);
}
else
OutputEncrypted(inputFile, outputStream, encryptionKeys);
}
}
private static void OutputEncrypted(string inputFile, Stream outputStream, PGPEncryptionKeys encryptionKeys)
{
using (Stream encryptedOut = ChainEncryptedOut(outputStream, encryptionKeys))
{
FileInfo unencryptedFileInfo = new FileInfo(inputFile);
using (Stream compressedOut = ChainCompressedOut(encryptedOut))
{
PgpSignatureGenerator signatureGenerator = InitSignatureGenerator(compressedOut, encryptionKeys);
using (Stream literalOut = ChainLiteralOut(compressedOut, unencryptedFileInfo))
{
using (FileStream inputFileStream = unencryptedFileInfo.OpenRead())
{
WriteOutputAndSign(compressedOut, literalOut, inputFileStream, signatureGenerator);
inputFileStream.Close();
}
}
}
}
}
private static void WriteOutputAndSign(Stream compressedOut, Stream literalOut, FileStream inputFile, PgpSignatureGenerator signatureGenerator)
{
int length = 0;
byte[] buf = new byte[BufferSize];
while ((length = inputFile.Read(buf, 0, buf.Length)) > 0)
{
literalOut.Write(buf, 0, length);
signatureGenerator.Update(buf, 0, length);
}
signatureGenerator.Generate().Encode(compressedOut);
}
private static Stream ChainEncryptedOut(Stream outputStream, PGPEncryptionKeys m_encryptionKeys)
{
PgpEncryptedDataGenerator encryptedDataGenerator;
encryptedDataGenerator = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.TripleDes, new SecureRandom());
encryptedDataGenerator.AddMethod(m_encryptionKeys.PublicKey);
return encryptedDataGenerator.Open(outputStream, new byte[BufferSize]);
}
private static Stream ChainCompressedOut(Stream encryptedOut)
{
PgpCompressedDataGenerator compressedDataGenerator = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip);
return compressedDataGenerator.Open(encryptedOut);
}
private static Stream ChainLiteralOut(Stream compressedOut, FileInfo file)
{
PgpLiteralDataGenerator pgpLiteralDataGenerator = new PgpLiteralDataGenerator();
return pgpLiteralDataGenerator.Open(compressedOut, PgpLiteralData.Binary, file);
}
private static PgpSignatureGenerator InitSignatureGenerator(Stream compressedOut, PGPEncryptionKeys m_encryptionKeys)
{
const bool IsCritical = false;
const bool IsNested = false;
PublicKeyAlgorithmTag tag = m_encryptionKeys.SecretKey.PublicKey.Algorithm;
PgpSignatureGenerator pgpSignatureGenerator = new PgpSignatureGenerator(tag, HashAlgorithmTag.Sha1);
pgpSignatureGenerator.InitSign(PgpSignature.BinaryDocument, m_encryptionKeys.PrivateKey);
foreach (string userId in m_encryptionKeys.SecretKey.PublicKey.GetUserIds())
{
PgpSignatureSubpacketGenerator subPacketGenerator = new PgpSignatureSubpacketGenerator();
subPacketGenerator.SetSignerUserId(IsCritical, userId);
pgpSignatureGenerator.SetHashedSubpackets(subPacketGenerator.Generate());
// Just the first one!
break;
}
pgpSignatureGenerator.GenerateOnePassVersion(IsNested).Encode(compressedOut);
return pgpSignatureGenerator;
}
#endregion Encrypt and Sign
NEW ERROR AFTER INITIAL FIX
I was able to get by the initial error of the signature check (we had the wrong keys) but now the vendor is getting the following error. Keep in mind that just Encrypting the file works but EncryptAndSign is what is giving the error. Anyone have another suggestions?
gpg: WARNING: message was not integrity protected
gpg: Hint: If this message was created before the year 2003 it is
likely that this message is legitimate. This is because back
then integrity protection was not widely used.
gpg: Use the option '--ignore-mdc-error' to decrypt anyway.
gpg: decryption forced to fail!
I am using Android and I am trying to decrypt a message encrypted in a C Sharp Server.
Below is the code for the C# Cryptor, that uses 256 bit long Keys, 128 bit long IV, 5000 Iterations. It uses Rfc2898DeriveBytes Class, so that is the same as PBKDF2WithHmacSHA1 in Android.
The decrypt function of the C# Cryptor takes as its IV the (reversed) first 128 bits of the 256 bit long key.
namespace CompanyName.Framework.Encryption
{
internal class SymmetricCryptor : ISymmetricCryptor
{
internal static int KeyLengthInBytes = 32;
internal int Iterations = 5000;
#region Private Fields
// RijndaelManaged aes; old version
AesManaged aes;
int IVLength = KeyLengthInBytes >> 1;
#endregion Private Fields
#region Internal Constructors
internal SymmetricCryptor( )
{
aes = new AesManaged
{
Mode = CipherMode.CBC,
KeySize= KeyLengthInBytes<<3,
Padding = PaddingMode.PKCS7,
};
//aes.KeySize = KeyLengthInBytes << 3;
//aes.Padding = PaddingMode.Zeros; //PKCS7 can not be used with stream
}
#endregion Internal Constructors
#region Public Methods
public byte[] Decrypt(byte[] cryptedData, string password, IVMode ivmode)
{
using (MemoryStream ms = new MemoryStream(cryptedData))
{
using (MemoryStream data = new MemoryStream())
{
Decrypt(ms, data, password,ivmode);
return data.ToArray();
}
}
}
public void Encrypt(Stream data, Stream trgStream, string password, IVMode ivmode)
{
try
{
var key = GetKey(password);
var iv = (ivmode == IVMode.Auto)
?key.GetBytes(IVLength).Reverse().ToArray()
: new byte[IVLength];
var dc = aes.CreateEncryptor(key.GetBytes(KeyLengthInBytes), iv);
using (CryptoStream cryptor = new CryptoStream(trgStream, dc, CryptoStreamMode.Write))
{
data.CopyTo(cryptor);
cryptor.FlushFinalBlock();
cryptor.Close();
}
}
catch (Exception)
{
throw new InvalidOperationException("Invalid password.");
}
}
public void Decrypt(Stream cryptedData, Stream trgStream, string password, IVMode ivmode)
{
try
{
var key= GetKey(password);
var iv = (ivmode == IVMode.Auto)
? key.GetBytes(IVLength).Reverse().ToArray()
: new byte[IVLength];
var dc = aes.CreateDecryptor(key.GetBytes(KeyLengthInBytes),iv);
using (CryptoStream cryptor = new CryptoStream(cryptedData, dc, CryptoStreamMode.Read))
{
cryptor.CopyTo(trgStream);
cryptor.Close();
}
}
catch (Exception)
{
throw new InvalidOperationException("Invalid password.");
}
}
public byte[] Encrypt(byte[] data, string password, IVMode ivmode)
{
using (MemoryStream ms = new MemoryStream(data))
{
using (MemoryStream cData = new MemoryStream())
{
Encrypt(ms, cData, password,ivmode);
return cData.ToArray();
}
}
}
#endregion Public Methods
#region Private Methods
private Rfc2898DeriveBytes GetKey(string password)
{
try
{
var iv =
CompanyName.Framework.Cryptography.Digest.SHA1.Compute(password);
return new Rfc2898DeriveBytes(password, iv, Iterations);
}
catch (Exception)
{
throw;
}
}
#endregion Private Methods
}
}
My Android Cryptor, which tries to decrypt a message encrypted by the above C Sharp Cryptor looks like this, I tried to copy the Decrypt method of the C Sharp Cryptor:
public class Cryptor {
private static final String TRANSFORMATION = "AES/CBC/PKCS7Padding";
private static final String AES = "AES";
private static final String RANDOM_ALGO = "SHA1PRNG";
private static final int KEY_LENGTH_IN_BITS = 256;
private static final int IV_LENGTH = 16;
private static final int PBE_ITERATION_COUNT = 5000;
private static final int PBE_SALT_LENGTH_INT_BITS = 128;
private static final String PBE_ALGO = "PBKDF2WithHmacSHA1";
public static byte[] generateKeyFromPassword(String password, int Size) throws GeneralSecurityException {
byte[] salt = generateSalt();
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, PBE_ITERATION_COUNT, Size);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBE_ALGO);
byte[] data = keyFactory.generateSecret(keySpec).getEncoded();
return data;
}
private static byte[] generateSalt() throws GeneralSecurityException {
return randomBytes(PBE_SALT_LENGTH_INT_BITS);
}
private static byte[] randomBytes(int length) throws GeneralSecurityException {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGO);
byte[] b = new byte[length];
random.nextBytes(b);
return b;
}
public static byte[] decrypt(byte[] cipherText, String password) throws GeneralSecurityException {
byte[] keyBytes = generateKeyFromPassword(password, 256);
byte[] ivBytes = generateKeyFromPassword(password, 128);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
ivBytes = reverse(ivBytes);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, AES);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decrypted = cipher.doFinal(cipherText);
return decrypted;
}
public static byte[] reverse(byte[] array) {
if (array == null) {
return null;
}
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array;
}
But it is not working, When do final is called I get a
javax.crypto.BadPaddingException: error:1e06b065:Cipher functions:EVP_DecryptFinal_ex:BAD_DECRYPT
Exception. I am not sure what I am doing wrong, because my Decrypt Method in Android is doing the exact same thing as the Decrypt Method in C Sharp: First I generate a Key from the password, which is shared by the Csharp Server and me. Then I generate a random 128 bit IV, reversing it is not necessary, but C Sharp implementation reverses it, so I do it as well. Can anyone tell me what I am doing wrong? Here is the context where I use the Cryptor:
//open the client channel, read and return the response as byte[]
Channel clientChannel = new Channel(serverAddress);
byte[] result = clientChannel.execute(serviceID.toString(), data);
//result[] is encrypted data. firstTen is the shared Password
byte[] decrypted = Cryptor.decrypt(result, firstTen);
Server returns the result as Base64 encrypted, before passing it for decryption I get the result[] array through:
It comes as a Base64 String. I get the result[] array through:
Base64.decode(result, Base64.NO_WRAP);
You need to generate random salt and IV on Server side and sent it with ciphreText to Android side. Android need to use exactly the same salt and IV to derive key for decryption that that was used to derive encryption key on Server side.
I need to encrypt a password on a web site using a public key. The public key is delivered through a Java web service with the information: The key is a RSA key to be used with RSA/ECB/PKCS1Padding algorithm. The public key is delivered as JSON in the form:
{
"kid":"PWD",
"kty":"RSA",
"use":"enc",
"n":"MTA0OTgzNjg0OTMxMzE2NjkwNTU4Mjg3NDIwMDg1NTY0ODEyMjg1MDk2NTcwNzU5NDIzNzM0O
DA3OTA2MzA0MDczNTU0NDQ2Njg3ODY2ODk2NTk0NjYzNTAxMzg0NzE1OTExMjA0MjU1MzMzOTIzMjA
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
zcwMjA3MzQxOTcwNzc4NDAwNzM3MTY2NDMyNzIwNjMwMDQwOTMwOTQ0MTA2OTE1NDEzMDAwNTMyMTE
5ODM0MTA2MjAzMDIyODEwMjYyMDM3MDQ0NzkxNzIzNTU1MjQyNjYxMzE2ODc4OTc5NzY1OTgzMjg4M
zQ0NDc3OTYwNTg3MzE2NTUwMDgx",
"e":"NjU1Mzc"
}
I tried to encrypt the password using the public key but the key generated is not correct. Here is my code:
encryptedPassword = EncrypIt("Password01", "MTA1MzQxNzIwODA3NjUwNzg5ND
Y4ODU2Mjc0NDA3ODIwMjQ1ODQ5NDE1MDk1MDIzMTM3MjM0NzAwNzYzNjc2MTgwNjg3ODMxMjA3
NTY0NTcxMjg2MzM4NjQ1NzEwMDcyMjY2MTQyNDIzMTczMjkwMDk0MTc0MTA5MTc5MzI0NjYwMjQ4NzI3NzM0MTQ5NDY0MjUwODkwO
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
c2OTAzNjc3NzQzODM3NzM0MjE2ODM0NjY4MjM4MTQ0OTA3MDQ3MTk1Njc1NzU3OTE2NjEyNzkzMzM2MzI3MDUyNjg0NDI5NDExNjI
2MzA0MzM5", "NjU1Mzc");
public static string EncrypIt(string password, string publicKey, string exponent)
{
UnicodeEncoding ByteConverter = new UnicodeEncoding();
byte[] publicKeyByte = ByteConverter.GetBytes(publicKey);
byte[] passwordByte = ByteConverter.GetBytes(password);
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
RSAParameters RSAKeyInfo = new RSAParameters();
RSAKeyInfo = RSA.ExportParameters(false); //Export only public key
//Set RSAKeyInfo to the public key values.
RSAKeyInfo.Modulus = publicKeyByte;
//RSAKeyInfo.Exponent = exponentBytes; //I tried to set exponent but I have an error
RSA.ImportParameters(RSAKeyInfo);
byte[] encryptedPassword = RSA.Encrypt(passwordByte, false);
return Convert.ToBase64String(encryptedPassword);
}
(The public key is different between the JSON and my code but don't pay attention to it, I just copied information from different sources)
The encrypted password I obtained is far too long: The encrypted password should be 172 characters (I know because I have a small Java program that allows me to encrypt passwords correctly) but I get 1100 characters.
I do not use the exponent: Should I ?
Could I not use the JSON to directly configure the RSACryptoServiceProvider correctly ?
The answer from owlstead helped me to get an encrypted password with the right string size but the service using the encrypted string reject it with the message: javax.crypto.IllegalBlockSizeException: Data must not be longer than 128 bytes
I otained the code of the java program wich is doing the correct encryption (see below). I need to achieve the same encryption using .NET.
public class EncryptionServiceImpl
{
private static final Charset UTF8 = Charset.forName("UTF-8");
#Resource(name = "briqueAuthentificationClient")
private BriqueAuthentificationClientImpl briqueAuthentificationClient;
protected static final String ALGORITHM_RSA = "RSA";
protected static final String TRANSFORMATION_RSA_ECB_PKCS1PADDING = "RSA/ECB/PKCS1Padding";
private static final Logger LOG = LoggerFactory.getLogger(EncryptionServiceImpl.class);
public EncryptionServiceImpl() {
LOG.info("constructeur EncryptionServiceImpl");
}
/**
* #param briqueAuthentificationClient the briqueAuthentificationClient to set
*/
public void setBriqueAuthentificationClient(final BriqueAuthentificationClientImpl briqueAuthentificationClient) {
this.briqueAuthentificationClient = briqueAuthentificationClient;
}
public String encrypt(final String input) throws GeneralSecurityException {
if (StringUtils.isNotBlank(input)) {
final CertificateDto certificate = this.briqueAuthentificationClient.getCurrentCertificate();
if (certificate != null) {
return new String(this.encryptAndEncode(input.getBytes(), certificate), EncryptionServiceImpl.UTF8);
} else {
throw new RuntimeException("Certificate is null");
}
}
return null;
}
protected byte[] encryptAndEncode(final byte[] input, final CertificateDto currentCertificate)
throws GeneralSecurityException {
// Création de la clé publique
final PublicKey publicKey = this.buildPublicKey(currentCertificate);
// Chiffre
final byte[] inputEncrypted = this.encrypte(input, publicKey);
// Encode
return this.encodeBase64Url(inputEncrypted);
}
protected PublicKey buildPublicKey(final CertificateDto currentCertificate) throws GeneralSecurityException {
if ("RSA".equals(currentCertificate.getKeyType())) {
return this.buildRSAPublicKey(currentCertificate);
}
LOG.error(String.format("Tentative de création d'une clé publique avec un algorithme non connu [%s]",
currentCertificate.getKeyType()));
return null;
}
protected PublicKey buildRSAPublicKey(final CertificateDto currentCertificate) throws GeneralSecurityException {
final BigInteger modulus = new BigInteger(new String(Base64.decodeBase64(currentCertificate.getModulus()),
EncryptionServiceImpl.UTF8));
final BigInteger publicExponent = new BigInteger(new String(Base64.decodeBase64(currentCertificate
.getPublicExponent()), EncryptionServiceImpl.UTF8));
try {
return KeyFactory.getInstance(ALGORITHM_RSA).generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
} catch (InvalidKeySpecException e) {
throw e;
} catch (NoSuchAlgorithmException e) {
throw e;
}
}
protected byte[] encrypte(final byte[] input, final RSAPublicKeySpec rsaPublicKeySpec)
throws GeneralSecurityException {
PublicKey publicKey;
try {
publicKey = KeyFactory.getInstance(ALGORITHM_RSA).generatePublic(
new RSAPublicKeySpec(rsaPublicKeySpec.getModulus(), rsaPublicKeySpec.getPublicExponent()));
} catch (InvalidKeySpecException e) {
throw e;
} catch (NoSuchAlgorithmException e) {
throw e;
}
return this.encrypte(input, publicKey);
}
protected byte[] encrypte(final byte[] input, final PublicKey publicKey) throws GeneralSecurityException {
try {
final Cipher cipher = Cipher.getInstance(TRANSFORMATION_RSA_ECB_PKCS1PADDING);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(input);
} catch (NoSuchAlgorithmException e) {
throw e;
} catch (NoSuchPaddingException e) {
throw e;
} catch (IllegalBlockSizeException e) {
throw e;
} catch (BadPaddingException e) {
throw e;
}
}
protected byte[] decrypte(final byte[] input, final RSAPrivateKeySpec rsaPrivateKeySpec)
throws GeneralSecurityException {
final BigInteger modulus = rsaPrivateKeySpec.getModulus();
final BigInteger privateExponent = rsaPrivateKeySpec.getPrivateExponent();
try {
final PrivateKey privateKey = KeyFactory.getInstance(ALGORITHM_RSA).generatePrivate(
new RSAPrivateKeySpec(modulus, privateExponent));
final Cipher cipher = Cipher.getInstance(TRANSFORMATION_RSA_ECB_PKCS1PADDING);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(input);
} catch (NoSuchAlgorithmException e) {
throw e;
} catch (NoSuchPaddingException e) {
throw e;
} catch (IllegalBlockSizeException e) {
throw e;
} catch (BadPaddingException e) {
throw e;
} catch (InvalidKeySpecException e) {
throw e;
} catch (InvalidKeyException e) {
throw e;
}
}
protected byte[] encodeBase64Url(final byte[] input) {
return Base64.encodeBase64(input, false);
}
protected byte[] decodeBase64Url(final byte[] input) {
return Base64.decodeBase64(input);
}
/**
* Method to connect to an url
*
* #param httpclient the http connection
* #return the response GetMethod
* #throws OAuthException in cas of connection error
*/
private GetMethod connect(final HttpClient httpclient, final String url) {
final GetMethod httpget = new GetMethod(url);
try {
httpclient.executeMethod(httpget);
} catch (final UnknownHostException e) {
throw new RuntimeException("Connection ERROR - Host could not be determined.", e);
} catch (final IOException e) {
throw new RuntimeException("Connection ERROR - Input/Output error.", e);
}
return httpget;
}
}
The steps I accomplished with the help of owlstead are in the answer below.
When I use this Java program to encode the string Password01 I obtain a string like:
sy5/XElHvuYA4Rbq8OBydLymT82R+z77jy6MU2sNal7VenzPEbARgy7p3zWgYgG13Cypk+zbnnB5L37fVUhgOgCqCyLtikzxJBNmPyzUK9+beiHJKyONZwifDzQ44hXTeXcZ0bmF9b5dLFy9QS/N5m28vIyBSGY8K2B7EB2FF38=
This encrypted password can be decrypted on Java side
When I use the .NET code the string is like:
ACZXYj/KudyxKBB510SxSouKaVwssmEUM6Jpreh8jTtrIH9eGb18GIdkBU7rXzMuLYbAhyREbFLbR87zW/2DNa4tN97FOlqr6k1JppJ/SSS/9fGdMvSOAQbWjsxksDH7fRu9dCvK0m0exFtGfXG7Yua9bB1m0dTNiwCZUZM0LnEM
This encrypted password failed to be decrypted on Java side. Failed with the error message:
javax.crypto.IllegalBlockSizeException: Data must not be longer than 128 bytes
You first need to perform base 64 decoding using Convert.FromBase64String of n and e, convert the result from ASCII encoding to a string and then parse the result using BigInteger.parse. Then you can convert using toByteArray to bytes, but beware that BigInteger is little endian, and RSAParameters expects big endian, so you have to reverse the bytes.
Your .NET RSA ciphertext (which you probably shouldn't reverse) is preceded by a 00h valued byte, which makes it an invalid ciphertext. RSA ciphertext must be of the exact length in bytes of the key.
Thank you sirs for the answers and comments, it help me to finally solve my issue:
For exponent, I got an error message: Invalid length for a Base-64 char array or string
This is because a base 64 value should be a multiple of 4. If this is not the case we should append equal sign (=) to reach a multiple of 4.
Thus after changing the exponent string from "NjU1Mzc" to "NjU1Mzc=" the value can be decoded.
Then I applied the solution provided by owlstead. Here is the final code which works fine:
//Decode from base 64
byte[] publicKeyByte = Convert.FromBase64String(rsaPublicKey.modulo);
byte[] exponentByte = Convert.FromBase64String(rsaPublicKey.exponent);
//Convert to ASCII string
UTF8Encoding ByteConverter = new UTF8Encoding();
string publicKeyString = System.Text.Encoding.Default.GetString(publicKeyByte);
string exponentString = System.Text.Encoding.Default.GetString(exponentByte);
//Convert to BigInteger
BigInteger publicKeyBI = BigInteger.Parse(publicKeyString);
BigInteger exponentBI = BigInteger.Parse(exponentString);
//Convert back to byte array
byte[] publicKeyByte2 = publicKeyBI.ToByteArray();
byte[] exponentByte2 = exponentBI.ToByteArray();
//We must remove the 129th sign byte which is added when converting to BigInteger
if (publicKeyByte2.Length == 129) Array.Resize(ref publicKeyByte2, 128);
//Create crypto service
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
RSAParameters RSAKeyInfo = new RSAParameters();
//Assign RSA key modulus/exponent reversing from little endian to big endian
RSAKeyInfo.Modulus = publicKeyByte2.Reverse().ToArray();
RSAKeyInfo.Exponent = exponentByte2.Reverse().ToArray();
RSA.ImportParameters(RSAKeyInfo);
//Convert password string to byte array
byte[] passwordByte = ByteConverter.GetBytes(clearPassword);
//Encrypt the password and encode 64
encryptedPassword = Convert.ToBase64String(RSA.Encrypt(passwordByte, false));
The missing point from owlstead is that: the method returns a byte array with an extra element whose value is zero to disambiguate positive values
See Microsoft documentation for more information on this point: BigInteger.ToByteArray Method
This code encrypts the password as a 172 characters string which ends with a = sign which is what I expected and it is correctly decrypted on Java side with the private key.
I tried this and it encrypts correctly
var input = "String to Encode.";
var mod = "MTA1MzQxNzIwODA3NjUwNzg5NDY4ODU2Mjc0NDA3ODIwMjQ1ODQ5NDE1MDk1MDIzMTM3MjM0NzAwNzYzNjc2MTgwNjg3ODMxMjA3NTY0NTcxMjg2MzM4NjQ1NzEwMDcyMjY2MTQyNDIzMTczMjkwMDk0MTc0MTA5MTc5MzI0NjYwMjQ4NzI3NzM0MTQ5NDY0MjUwODkwOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXc2OTAzNjc3NzQzODM3NzM0MjE2ODM0NjY4MjM4MTQ0OTA3MDQ3MTk1Njc1NzU3OTE2NjEyNzkzMzM2MzI3MDUyNjg0NDI5NDExNjI2MzA0MzM5==";
var exp = "NjU1Mzc=";
var intValue = int.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(exp)));
var rsaParam = new RSAParameters();
rsaParam.Modulus = Convert.FromBase64String(mod);
rsaParam.Exponent = BitConverter.GetBytes(intValue);
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaParam);
Console.WriteLine(Convert.ToBase64String(rsa.Encrypt(Encoding.UTF8.GetBytes(input), false)));
}
Console.ReadLine();
I think the issue is the exponent is strange. Exp = 65537;
This is my code for decryption process:
private RSACryptoServiceProvider _rsa;
private string _privateKey;
private string _publicKey;
public RsaLibrary()
{
//initialsing the RSA object taking the option of a 1024 key size
_rsa = new RSACryptoServiceProvider(1024);
_privateKey = _rsa.ToXmlString(true);
_publicKey = _rsa.ToXmlString(false);
}
public string Decrypt(string ciphertext, string privateKey_ = null)
{
if (String.IsNullOrEmpty(privateKey_))
{
return DecryptToBytes(ciphertext, _privateKey);
}
else
{
return DecryptToBytes(ciphertext, privateKey_);
}
}
private string DecryptToBytes(string ciphertext, string privateKey)
{
if (String.IsNullOrEmpty(privateKey))
{
throw new ArgumentNullException("Error: No key provided.");
}
if (ciphertext.Length<=0)
{
throw new ArgumentNullException("Error: No message to decrypt.");
}
byte[] plaintext;
byte[] ciphertext_Bytes = Encoding.Unicode.GetBytes(ciphertext);
_rsa.FromXmlString(privateKey);
plaintext = _rsa.Decrypt(ciphertext_Bytes, false);
return Encoding.Unicode.GetString(plaintext);
}
The encryption code:
private string EncryptToByte(string plaintext, string publicKey)
{
if (String.IsNullOrEmpty(publicKey))
{
throw new ArgumentNullException("Error: No key provided.");
}
if (plaintext.Length<=0)
{
throw new ArgumentNullException("Error: No message to incrypt");
}
byte[] ciphertext;
byte[] plaintext_Bytes = Encoding.Unicode.GetBytes(plaintext);
_rsa.FromXmlString(publicKey);
ciphertext = _rsa.Encrypt(plaintext_Bytes, false);
return Convert.ToBase64String(ciphertext);
}
I can not see where I am going wrong. I have made sure that the keys are correct. The public one which i extracted using this line in the constructor:
_publicKey = _rsa.ToXmlString(false);
This public key is displayed on the form that I created. The private i used the "true" instead of false.
Any ideas?
Ciphertext is very unlikely to be genuinely UTF-16-encoded text. Assuming that the encryption side had something like:
string encryptedText = Encoding.Unicode.GetString(encryptedBytes);
you've basically lost data. The result of encryption is not text - it's arbitrary binary data. If you want to convert that to text for some transport reason, you should use Base64, e.g.
string base64EncryptedText = Convert.ToBase64String(encryptedBytes);
Then use Convert.FromBase64String to recover the original encrypted binary data which is ready to decrypt.
I am doing this application and it depends on settings that are stored in an xml file. This file should be encrypted and the values inside it are provided by the guy responsible for creating the application setup and is used to determine available functionality options depending on the version the user installed.
I need a way to store the password hard-coded in my software to be able to decrypt that file at runtime and read the values in it to see which features of the app the user has access to.
Bear in mind that this file should not be edited and is provided as part of the software.
I haven't provided any code, because its more of a design issue than a coding issue.
I know that hard-coding a password is stupid yet I am out of options.
If you're giving the application to untrustworthy users (i.e. this is a desktop app, rather than code running on an [ASP] server that users can't access directly) then there's nothing that you can do.
If you are giving the code to the user that will decrypt a configuration file, at some point, they will be able to access that file themselves. You could make it harder, possibly even a lot harder if you put in the time/effort/money, but you can't make it impossible. Here are some things that they could do:
Decompile your program and look for the password = "12345" line of code.
Monitor the program's memory; see when it loads the XML file, and try to find the decrypted version of it in memory.
Find the section of code where you read the decrypted XML file and do some action accordingly and change the code so that it always does whatever they want regardless of what's in the file (essentially just commenting out the if check).
Some things you can do to make the above steps harder (but not impossible) include:
Obfuscating your code.
Signing your code.
Doing random pointless stuff to try to confuse would be code sniffers (for example play shell games by having 3 files, reading them all, decrypting them all, and then having 2 of them not actually be used.
Send the config file to a web service of yours to be decrypted, rather than decrypting it locally. (This can be defeated by sniffing the network for the decrypted result).
Have a web service that you query to see if the user has permissions to do what you want (again, this can be defeated by sniffing/spoofing the network connection).
Now, it might be possible to actually prevent the user from doing "something", depending on what the "something" is, by not giving them the code that does it in the first place. These would be (potentially; if coded correctly) unbreakable:
Do the work on a server.
Have a web service that does some of the sensitive work. The desktop app only manages the UI or other non-sensitive tasks. If you do this the user can only break the code you've given them.
Make the whole app a website, or other server based application (i.e. think of MMORPGS) where it simply doesn't function at all without a server; it does almost all of the sensitive (and non-sensitive) work.
Note that the only true solutions require an internet connection being available for all users when using the application; they can't be offline.
internal const string XmlPassword = "This is more like security through "+
+"obfuscation than real security. If it fits your purposes, cool, but you "+
+"might want to consider using real encryption, like public key encryption";
If I'm reading the question right - the OP needs to decrypt a file provided (already encrypted) with a 3rd party software package of some type and he has obtained the decryption password (key) from the original publisher of the software.
I would do something similar to what itsmatt suggests, only instead of encrypting the file, encrypt the provided password, store that encrypted password in your config file, then read and decrypt the password at runtime, and use it to decrypt the file.
This way you are not hardcoding a plaintext password in your source to be easily sniffed out. By keeping it in your config file, you can easily change it in the future if needed.
Answer to comment:
I use AES256.
I use this class to AES256 encrypt/decrypt:
#region Preprocessor Directives
# if __FRAMEWORK_V35 || __FRAMEWORK_V4
#define __DotNet35Plus
#endif
#if !__DotNet35Plus
#warning AES implementation used by this compile of the library is not NIST certified as FIPS 140-2 compliant
#endif
#endregion
#region Namespaces
using System;
using System.Security.Cryptography;
using System.IO;
using System.Text;
using System.Threading;
using System.Runtime.Serialization.Formatters.Binary;
#endregion
namespace Simple
{
public static class AES256Encryption
{
private static readonly object _lock = new object();
private const Int32 KeySize = 256;
#if __DotNet35Plus
private static AesCryptoServiceProvider thisCSP = new AesCryptoServiceProvider();
#else
private static RijndaelManaged thisCSP = new RijndaelManaged();
#endif
private static MemoryStream msEncrypt = new MemoryStream();
private static CryptoStream csEncrypt;
private static MemoryStream msDecrypt = new MemoryStream();
private static CryptoStream csDecrypt;
public enum stringIOType
{
base64EncodedString = 0,
HexEncodedString = 1
}
public static bool NISTCertified()
{
#if __DotNet35Plus
return true;
#else
return false;
#endif
}
#region Encryption Methods
public static byte[] encryptBytes(byte[] Value, string PassPhrase, Encoding PassPhraseEncoding)
{
try
{
Monitor.Enter(_lock);
return encryptBytes(Value, getKeyFromPassPhrase(PassPhrase, PassPhraseEncoding), getIVFromPassPhrase(PassPhrase, PassPhraseEncoding));
}
finally
{
Monitor.Exit(_lock);
}
}
public static byte[] encryptBytes(byte[] Value, byte[] Key, byte[] IV)
{
try
{
Monitor.Enter(_lock);
#if __DotNet35Plus
thisCSP = new AesCryptoServiceProvider();
#else
thisCSP = new RijndaelManaged();
#endif
thisCSP.KeySize = KeySize;
Int32 bitLength = Key.Length * 8;
if (bitLength != thisCSP.KeySize)
{
throw new ArgumentException("The supplied key's length [" + bitLength.ToString() + " bits] is not a valid key size for the AES-256 algorithm.", "Key");
}
bitLength = IV.Length * 8;
if (bitLength != thisCSP.BlockSize)
{
throw new ArgumentException("The supplied IV's length [" + bitLength.ToString() + " bits] is not a valid IV size for the AES-256 algorithm.", "IV");
}
ICryptoTransform Encryptor = thisCSP.CreateEncryptor(Key, IV);
msEncrypt = new MemoryStream();
csEncrypt = new CryptoStream(msEncrypt, Encryptor, CryptoStreamMode.Write);
csEncrypt.Write(Value, 0, Value.Length);
csEncrypt.FlushFinalBlock();
Encryptor.Dispose();
Encryptor = null;
msEncrypt.Close();
return msEncrypt.ToArray();
}
finally
{
thisCSP = null;
Monitor.Exit(_lock);
}
}
public static string encryptString(string Value, string PassPhrase, Encoding PassPhraseEncoding, Encoding inputEncoding, stringIOType outputType)
{
try
{
Monitor.Enter(_lock);
return encryptString(Value, getKeyFromPassPhrase(PassPhrase, PassPhraseEncoding), getIVFromPassPhrase(PassPhrase, PassPhraseEncoding), inputEncoding, outputType);
}
finally
{
Monitor.Exit(_lock);
}
}
public static string encryptString(string Value, byte[] Key, byte[] IV, Encoding inputEncoding, stringIOType outputType)
{
try
{
Monitor.Enter(_lock);
byte[] baseValue = (byte[])Array.CreateInstance(typeof(byte), inputEncoding.GetByteCount(Value));
baseValue = inputEncoding.GetBytes(Value);
switch(outputType)
{
case stringIOType.base64EncodedString:
return Convert.ToBase64String(encryptBytes(baseValue, Key, IV));
case stringIOType.HexEncodedString:
return ByteArrayToHexString(encryptBytes(baseValue, Key, IV));
default:
return Convert.ToBase64String(encryptBytes(baseValue, Key, IV));
}
}
finally
{
Monitor.Exit(_lock);
}
}
#endregion
#region Decryption Methods
public static byte[] decryptBytes(byte[] Value, string PassPhrase, Encoding PassPhraseEncoding)
{
try
{
Monitor.Enter(_lock);
return decryptBytes(Value, getKeyFromPassPhrase(PassPhrase, PassPhraseEncoding), getIVFromPassPhrase(PassPhrase, PassPhraseEncoding));
}
finally
{
Monitor.Exit(_lock);
}
}
public static byte[] decryptBytes(byte[] Value, byte[] Key, byte[] IV)
{
try
{
Monitor.Enter(_lock);
#if __DotNet35Plus
thisCSP = new AesCryptoServiceProvider();
#else
thisCSP = new RijndaelManaged();
#endif
thisCSP.KeySize = KeySize;
Int32 bitLength = Key.Length * 8;
if (bitLength != thisCSP.KeySize)
{
throw new ArgumentException("The supplied key's length [" + bitLength.ToString() + " bits] is not a valid key size for the AES-256 algorithm.", "Key");
}
bitLength = IV.Length * 8;
if (bitLength != thisCSP.BlockSize)
{
throw new ArgumentException("The supplied IV's length [" + bitLength.ToString() + " bits] is not a valid IV size for the AES-256 algorithm.", "IV");
}
try
{
byte[] Decrypted;
ICryptoTransform Decryptor = thisCSP.CreateDecryptor(Key, IV);
msDecrypt = new MemoryStream(Value);
csDecrypt = new CryptoStream(msDecrypt, Decryptor, CryptoStreamMode.Read);
Decrypted = (byte[])Array.CreateInstance(typeof(byte), msDecrypt.Length);
csDecrypt.Read(Decrypted, 0, Decrypted.Length);
Decryptor.Dispose();
Decryptor = null;
msDecrypt.Close();
Int32 trimCount = 0;
// Remove any block padding left over from encryption algorithm before returning
for (Int32 i = Decrypted.Length - 1; i >= 0; i--)
{
if (Decrypted[i] == 0) { trimCount++; } else { break; }
}
if (trimCount > 0)
{
byte[] buffer = (byte[])Array.CreateInstance(typeof(byte), Decrypted.Length - trimCount);
Array.ConstrainedCopy(Decrypted, 0, buffer, 0, buffer.Length);
Array.Clear(Decrypted, 0, Decrypted.Length);
Array.Resize<byte>(ref Decrypted, buffer.Length);
Array.Copy(buffer, Decrypted, buffer.Length);
buffer = null;
}
return Decrypted;
}
finally
{
thisCSP = null;
}
}
finally
{
Monitor.Exit(_lock);
}
}
public static string decryptString(string Value, string PassPhrase, Encoding PassPhraseEncoding, stringIOType inputType, Encoding outputEncoding)
{
try
{
Monitor.Enter(_lock);
return decryptString(Value, getKeyFromPassPhrase(PassPhrase, PassPhraseEncoding), getIVFromPassPhrase(PassPhrase, PassPhraseEncoding), inputType, outputEncoding);
}
finally
{
Monitor.Exit(_lock);
}
}
public static string decryptString(string Value, byte[] Key, byte[] IV, stringIOType inputType, Encoding outputEncoding)
{
try
{
Monitor.Enter(_lock);
byte[] baseValue;
switch (inputType)
{
case stringIOType.base64EncodedString:
baseValue = Convert.FromBase64String(Value);
break;
case stringIOType.HexEncodedString:
baseValue = HexStringToByteArray(Value);
break;
default:
baseValue = Convert.FromBase64String(Value);
break;
}
return outputEncoding.GetString(decryptBytes(baseValue, Key, IV));
}
finally
{
Monitor.Exit(_lock);
}
}
#endregion
#region Key/Digest Generation Methods
public static byte[] getKeyFromPassPhrase(string PassPhrase, Encoding encoder)
{
Monitor.Enter(_lock);
try
{
return getDigest(PassPhrase, encoder, 32);
}
finally
{
Monitor.Exit(_lock);
}
}
public static byte[] getIVFromPassPhrase(string PassPhrase, Encoding encoder)
{
Monitor.Enter(_lock);
try
{
byte[] buffer = (byte[])Array.CreateInstance(typeof(byte), encoder.GetByteCount(PassPhrase));
byte[] reverseBuffer = (byte[])Array.CreateInstance(typeof(byte), encoder.GetByteCount(PassPhrase));
buffer = encoder.GetBytes(PassPhrase);
for (Int32 i = 0; i <= buffer.Length - 1; i++)
{
reverseBuffer[i] = buffer[buffer.Length - i - 1];
}
return getDigest(reverseBuffer, 16);
}
finally
{
Monitor.Exit(_lock);
}
}
public static byte[] getDigest(string value, Encoding encoder, Int32 digestLength)
{
Monitor.Enter(_lock);
try
{
return getDigest(encoder.GetBytes(value), digestLength);
}
finally
{
Monitor.Exit(_lock);
}
}
public static byte[] getDigest(object value, Int32 digestLength)
{
Monitor.Enter(_lock);
try
{
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, value);
return getDigest(ms.ToArray(), digestLength);
}
finally
{
Monitor.Exit(_lock);
}
}
public static byte[] getDigest(byte[] value, Int32 digestLength)
{
Monitor.Enter(_lock);
try
{
Int32 iterations = 0;
// Find first non-zero byte value to use to calculate iterations
for (Int32 i = 0; i < value.Length; i++)
{
if (value[i] != 0) { iterations = (Int32)(value[i] * 10); break; }
}
// There were no non-zero byte values use the max for iterations
if (iterations == 0) { iterations = (Int32)(byte.MaxValue * 10); }
Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(value, new SHA256Managed().ComputeHash(value), iterations);
return deriveBytes.GetBytes(digestLength);
}
finally
{
Monitor.Exit(_lock);
}
}
#endregion
#region HexArray/String String/HexArray Converters
public static string ByteArrayToHexString(byte[] ba)
{
try
{
Monitor.Enter(_lock);
StringBuilder hex = new StringBuilder(ba.Length * 2);
foreach (byte b in ba)
hex.AppendFormat("{0:x2}", b);
return hex.ToString();
}
finally
{
Monitor.Exit(_lock);
}
}
public static byte[] HexStringToByteArray(String hex)
{
try
{
Monitor.Enter(_lock);
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
finally
{
Monitor.Exit(_lock);
}
}
#endregion
}
}
If you are using .Net Framework 3.5, or 4.0 define either __FRAMEWORK_V35 or __FRAMEWORK_V40 as a value for the preprocessor. The class used is not available for framework versions less than 3.5. If you are using an earlier framework version, just do not define the preprocessor values and an earlier class will be used.
First get the name of the file.
Then use the following to encrypt the OEM password (I'd suggest writing a little tool to do this):
string fileName = "Whatever The Filename is";
string password = "Whatever the OEM supplied password is";
string encryptedValue = Simple.AES256Encryption.encryptString(password, fileName, new UTF8Encoding(), new UTF8Encoding(), stringIOType.base64EncodedString);
Save the resulting base64 encoded string from encryptString to your config file.
To recover the OEM password:
string encryptedPassword = "This is the base64 encoded string you read from your config file";
string decrytptedPassword = Simple.AES256Encryption.decryptString(encryptedPassword, fileName, new UTF8Encoding(), stringIOType.base64EncodedString, new UTF8Encoding());
An approach would be to always encrypt the settings file based on some hash of the file. Then, your software could use the same hash as the key to decrypt the file.
So you'd basically do the following:
Get the file name (and/or some other attribute of the file - size, perhaps)
Compute the hash of the file name
Use that hash to decrypt the file
Not saying this is a good approach or an approach I'd personally use in any sort of production software but at least you're not relying upon either a plaintext or ofuscated string inside the code and it gives you the flexibility to change the settings filename and rely upon a convention for the encryption.
Again, this is only maybe a step above the "store the key in the source" solution.