Skip to content

Instantly share code, notes, and snippets.

@matkatmusic
Created June 9, 2022 04:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matkatmusic/bd7091ebcbd539aa5d1a5e0a79acf480 to your computer and use it in GitHub Desktop.
Save matkatmusic/bd7091ebcbd539aa5d1a5e0a79acf480 to your computer and use it in GitHub Desktop.
OpenSSL <-> juce::RSAKey
/*
Code that allows conversion of OpenSSL public keys into the juce::RSAKey format.
This may or may not work with private keys.
I have not tested it with private keys from the server, only public keys from the server.
*/
struct PEMHelpers
{
using PEMMemoryBlock = juce::MemoryBlock;
using PEMDataType = juce::uint8;
static PEMMemoryBlock convertPEMStringToPEMMemoryBlock(juce::String pemString)
{
PEMMemoryBlock mb;
{
juce::MemoryOutputStream mos(mb, false);
auto ok = juce::Base64::convertFromBase64(mos, pemString);
jassert(ok);
juce::ignoreUnused(ok);
}
return mb;
}
static juce::String convertPEMMemoryBlockToPEMString(PEMMemoryBlock byteArray)
{
PEMMemoryBlock resultBlock;
juce::MemoryOutputStream resultMOS(resultBlock, false);
juce::Base64::convertToBase64(resultMOS, byteArray.getData(), byteArray.getSize());
auto result = resultBlock.toString();
return result;
}
static juce::String convertPEMPublicKeyToString(juce::String pubKey)
{
jassert( pubKey.contains("-----BEGIN PUBLIC KEY-----"));
jassert( pubKey.contains("-----END PUBLIC KEY-----"));
if( !pubKey.contains("MII") )
{
DBG( "your key has less than 2048 bits! You should increase the key size" );
}
auto keyDataArr = juce::StringArray::fromLines(pubKey);
keyDataArr.remove(keyDataArr.indexOf("-----END PUBLIC KEY-----"));
keyDataArr.remove(0);
keyDataArr.removeEmptyStrings();
auto pemData = keyDataArr.joinIntoString("");
DBG( "pemData: " );
DBG( pemData );
return pemData;
}
static juce::String toHex(juce::uint8 value)
{
static const char* hexChars = "0123456789abcdef";
juce::String str;
auto v = value;
while( v > 0 )
{
auto idx = v & 0xf;
str = hexChars[ idx ] + str;
v >>= 4;
}
//insert a zero at the front.
if( value < 16 )
{
str = "0" + str;
}
return str;
}
static juce::uint8 fromHex(juce::String str)
{
jassert( str.length() == 2 );
juce::uint8 value = 0;
static juce::String hexChars { "0123456789abcdef" };
for( int i = 0; i < str.length(); ++i )
{
value += hexChars.indexOf( str.substring(i, i+1) );
if( i == 0 )
value <<= 4;
}
return value;
}
//ported from: https://github.com/lapo-luchini/asn1js/blob/trunk/int10.js
struct Int10
{
static constexpr juce::int64 max = 10'000'000'000'000;
std::vector<juce::int64> buf;
Int10(juce::int64 val = 0)
{
buf.push_back(val);
}
void mulAdd(juce::int64 m, juce::int64 c)
{
auto& b = buf;
auto l = b.size();
size_t i = 0;
juce::int64 t;
for (; i < l; ++i)
{
t = b[i] * m + c;
if (t < max)
{
c = 0;
}
else
{
//
c = std::floor(static_cast<double>(t) / static_cast<double>(max));
t -= c * max;
}
b[i] = t;
}
if (c > 0)
{
b[i] = c;
}
}
auto simplify() const
{
return buf.front();
}
};
//ported from: https://github.com/lapo-luchini/asn1js/blob/trunk/asn1.js#L508
struct ASN1Tag
{
int tagClass = 0;
bool tagConstructed = false;
juce::int64 tagNumber = 0;
ASN1Tag(juce::InputStream* stream = nullptr)
{
jassert(stream != nullptr);
jassert(!stream->isExhausted());
auto buf = stream->readByte();
tagClass = buf >> 6;
tagConstructed = (buf & 0x20) != 0;
tagNumber = buf & 0x1f;
if( tagNumber == 0x1f ) //long tag
{
auto n = Int10();
do
{
buf = stream->readByte();
n.mulAdd(128, buf & 0x7F);
}
while (buf & 0x80 && !stream->isExhausted() );
tagNumber = n.simplify();
}
}
bool isEOC() const
{
return tagClass == 0x00 && tagNumber == 0x00;
}
bool isUniversal() const
{
return tagClass == 0x00;
}
};
//ported from: https://github.com/lapo-luchini/asn1js/blob/trunk/asn1.js#L324
struct ASN1 : juce::ReferenceCountedObject
{
using Ptr = juce::ReferenceCountedObjectPtr<ASN1>;
std::unique_ptr<juce::MemoryInputStream> stream;
juce::int64 header = 0;
juce::int64 length = 0;
ASN1Tag tag;
juce::int64 tagLen = 0;
std::vector<Ptr> sub;
ASN1() = default;
ASN1(std::unique_ptr<juce::MemoryInputStream>&& stream_,
juce::int64 header_,
juce::int64 length_,
ASN1Tag tag_,
juce::int64 tagLen_,
std::vector<Ptr> sub_) :
stream(std::move(stream_)),
header(header_),
length(length_),
tag(tag_),
tagLen(tagLen_),
sub(sub_)
{
}
};
struct ASN1Decoder
{
//ported from: https://github.com/lapo-luchini/asn1js/blob/trunk/asn1.js#L494
static juce::int64 decodeLength(juce::InputStream& stream)
{
juce::uint8 byte = stream.readByte();
juce::uint64 buf = byte; //allows for 48-bit lengths
auto len = buf & 0x7f;
if( len == buf )
return len;
if( len == 0 )
return -1;
if( len > 6 )
{
//JS: throw "Length over 48 bits not supported at position " + (stream.pos - 1);
jassertfalse;
return -1;
}
buf = 0;
for (int i = 0; i < len; ++i)
{
juce::uint8 val = stream.readByte();
buf = (buf * 256) + val;
}
return buf;
}
//ported from: https://github.com/lapo-luchini/asn1js/blob/trunk/asn1.js#L528
static ASN1::Ptr decode(juce::MemoryInputStream& stream, int offset = 0)
{
auto streamStart = std::make_unique<juce::MemoryInputStream>(stream.getData(), stream.getDataSize(), true);
streamStart->setPosition(stream.getPosition());
auto tag = ASN1Tag(&stream);
auto tagLen = stream.getPosition() - streamStart->getPosition();
auto len = decodeLength(stream);
auto start = stream.getPosition();
auto header = start - streamStart->getPosition();
auto sub = std::vector<ASN1::Ptr>();
auto getSub = [&]()
{
if( len != -1 )
{
auto end = start + len;
if( end > stream.getTotalLength() )
{
// JS: throw 'Container at offset ' + start + ' has a length of ' + len + ', which is past the end of the stream';
jassertfalse;
return;
}
while( stream.getPosition() < end )
{
sub.push_back(decode(stream));
}
if( stream.getPosition() != end )
{
// JS: throw 'Content size is not correct for container at offset ' + start;
jassertfalse;
return;
}
}
else
{
// undefined length
for (;;)
{
auto s = decode(stream);
if( s == nullptr )
{
jassertfalse;
break;
}
if (s->tag.isEOC())
{
break;
}
sub.push_back(s);
}
len = start - stream.getPosition();
}
};
if (tag.tagConstructed)
{
getSub();
}
else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04)))
{
// sometimes BitString and OctetString are used to encapsulate ASN.1
if (tag.tagNumber == 0x03)
{
if( stream.readByte() != 0 )
{
//JS: throw "BIT STRINGs with unused bits cannot encapsulate.";
jassertfalse;
}
}
getSub();
for( size_t i = 0; i < sub.size(); ++i )
{
if( sub[i]->tag.isEOC() )
{
//JS: throw 'EOC is not supposed to be actual content.';
jassertfalse;
sub.clear();
break;
}
}
}
if( sub.empty() )
{
if( len == -1 )
{
// JS throw "We can't skip over an invalid tag with undefined length at offset " + start;
jassertfalse;
return {};
}
stream.setPosition(start + std::abs(len));
}
return new ASN1(std::move(streamStart), header, len, tag, tagLen, sub);
}
};
};
struct PEMFormatKey : juce::RSAKey
{
void loadFromPEMFormattedString(juce::String str);
juce::String decryptBase64String(juce::String base64);
};
void PEMFormatKey::loadFromPEMFormattedString(juce::String pubKey)
{
/*
extract the base64 data from the key
*/
auto pemString = PEMHelpers::convertPEMPublicKeyToString(pubKey);
if( ! pemString.contains("MII") && !pemString.contains("MIG") )
{
jassertfalse;
//it's not a PEM key. abort!
DBG( "invalid key!" );
return;
}
/*
convert it into a MemoryBlock
*/
auto pemData = PEMHelpers::convertPEMStringToPEMMemoryBlock(pemString);
/*
convert the MemoryBlock into an ASN1-formatted object with nested hierarchy
*/
juce::MemoryInputStream mis(pemData, false);
auto asn1 = PEMHelpers::ASN1Decoder::decode(mis);
/*
navigate the ASN1 hierarchy and find the modulus and exponent.
read the exponent and modulus
*/
jassert( asn1->sub.size() == 2 );
if( asn1->sub.size() != 2 )
{
jassertfalse;
//it's not a PEM key. abort!
DBG( "invalid key!" );
return;
}
auto bitString = asn1->sub.back();
jassert(bitString->sub.size() == 1);
if( bitString->sub.size() != 1 )
{
jassertfalse;
//it's not a PEM key. abort!
DBG( "invalid key!" );
return;
}
auto sequence = bitString->sub.front();
jassert(sequence->sub.size() == 2);
if( sequence->sub.size() != 2 )
{
jassertfalse;
//it's not a PEM key. abort!
DBG( "invalid key!" );
return;
}
/*
the modulus is the 1st element in the sequence's sub
read the data into a memory block.
convert it to a hex string
parse that hex string into a BigInteger.
*/
auto modulus = sequence->sub.front();
juce::MemoryBlock modulusBlock;
modulusBlock.setSize(modulus->length);
modulus->stream->setPosition(modulus->stream->getPosition() + modulus->header);
modulus->stream->read(modulusBlock.getData(),
static_cast<int>(modulus->length));
auto modulusHexStr = juce::String::toHexString(modulusBlock.getData(),
static_cast<int>(modulus->length));
/*
String::toHexString adds spaces between each pair of hex characters.
those spaces need to be removed.
*/
modulusHexStr = modulusHexStr.removeCharacters(" ");
auto modulusBigInteger = juce::BigInteger();
/*
load the modulus hex data into a BigInteger instance.
*/
modulusBigInteger.parseString(modulusHexStr, 16);
/*
the exponent is the 2nd element in the sequence's sub.
repeat the same process as above.
*/
auto exponent = sequence->sub.back();
juce::MemoryBlock exponentBlock;
exponentBlock.setSize(exponent->length);
exponent->stream->setPosition(exponent->stream->getPosition() + exponent->header);
exponent->stream->read(exponentBlock.getData(), static_cast<int>(exponent->length));
auto exponentHexStr = juce::String::toHexString(exponentBlock.getData(),
static_cast<int>(exponent->length));
exponentHexStr = exponentHexStr.removeCharacters(" ");
auto exponentBigInteger = juce::BigInteger();
exponentBigInteger.parseString(exponentHexStr, 16);
/*
now that you're finished parsing, assign the exponent and modulus appropriately.
*/
part1 = exponentBigInteger;
part2 = modulusBigInteger;
}
juce::String PEMFormatKey::decryptBase64String(juce::String base64)
{
auto confirmationBlock = PEMHelpers::convertPEMStringToPEMMemoryBlock(base64);
auto confirmationHex = juce::String::toHexString(confirmationBlock.getData(),
confirmationBlock.getSize());
// jassertfalse;
juce::BigInteger confirmationBigInt;
confirmationBigInt.parseString(confirmationHex, 16);
applyToValue(confirmationBigInt);
auto decrypted = confirmationBigInt.toMemoryBlock();
auto decryptedString = juce::String::createStringFromData(decrypted.getData(), decrypted.getSize());
//see https://forum.juce.com/t/string-reverse-method/23582/20?u=matkatmusic
auto stringReverser = [](const juce::String& in)
{
auto inBegin = in.getCharPointer();
auto inPtr = inBegin.findTerminatingNull();
juce::String out;
if (inPtr != inBegin)
{
out.preallocateBytes(inPtr - inBegin);
auto outPtr = out.getCharPointer();
while (inPtr != inBegin)
{
--inPtr;
outPtr.write(*inPtr);
}
outPtr.writeNull();
}
return out;
};
decryptedString = stringReverser(decryptedString);
return decryptedString;
}
//usage:
void exampleFunc()
{
/*
Assuming you have a juce::var with the following properties:
"pubkey" - the public key from the server
"confirmation" - the encrypted message from the server, encrypted with server's private key
"expected" - the expected result of decrypting:
*/
jassert( resultVar["pubkey"] != var() );
auto pubKey = resultVar["pubkey"].toString();
DBG( "pubkey: ");
DBG( pubKey );
jassert( resultVar["confirmation"] != var() );
PEMFormatKey rsaKey;
rsaKey.loadFromPEMFormattedString(pubKey);
jassert(rsaKey.isValid());
if( !rsaKey.isValid() )
{
jassertfalse;
return;
}
auto confirmation = resultVar["confirmation"].toString();
auto decryptedString = rsaKey.decryptBase64String(confirmation);
DBG( "encrypted: " << confirmation );
DBG("result: " << decryptedString );
DBG("expect: " << resultVar["expected"].toString() );
jassert( resultVar["expected"].toString() == decryptedString );
}
<?php
/**
* it is the caller's responsibility to close the handle that is opened
*/
function getKeyString($filePath, &$handle) : string | false
{
$handle = fopen($filePath, "r");
if( $handle === false )
{
echo( "failed to open file!" );
return false;
}
$size = filesize($filePath);
if( $size === false )
{
echo( "failed to get size of file!" );
return false;
}
$str = fread($handle, $size);
if( $str === false )
{
echo( "failed to read file!!!" );
return false;
}
return $str;
}
function getKeyDetails(string $PEMkey, bool $usePrivate) : array | false
{
$asymKey = false;
if( $usePrivate === true )
{
$asymKey = openssl_pkey_get_private($PEMkey);
}
else
{
$asymKey = openssl_pkey_get_public($PEMkey);
}
if( $asymKey === false )
{
echo( "failed to get AsymKey from PEMKey string!" );
return false;
}
$details = openssl_pkey_get_details($asymKey);
if( $details === false )
{
echo( "failed to get details from asymKey" );
return false;
}
return $details;
}
function encryptRSA(string $plainData, string $PEMkey, &$encrypted, bool $usePrivate) : bool
{
$keyDetails = getKeyDetails($PEMkey, $usePrivate);
if( $keyDetails === false )
{
echo("Failed to get key details!" );
return false;
}
$NUM_BITS = $keyDetails['bits'];
$ENCRYPT_BLOCK_SIZE = $NUM_BITS / 8 - 11;
$temp = '';
$plainData = str_split($plainData, $ENCRYPT_BLOCK_SIZE);
foreach($plainData as $chunk)
{
$partialEncrypted = '';
//using for example OPENSSL_PKCS1_PADDING as padding
$encryptionOk = false;
if( $usePrivate === true )
{
$encryptionOk = openssl_private_encrypt($chunk, $partialEncrypted, $PEMkey);
}
else
{
$encryptionOk = openssl_public_encrypt($chunk, $partialEncrypted, $PEMkey);
}
if($encryptionOk === false)
{
echo( "encryption failed!" );
return false;
}
$temp .= $partialEncrypted;
}
$encrypted = base64_encode($temp);//encoding the whole binary String as MIME base 64
return true;
}
function decryptRSA(string $PEMkey, $data, &$decrypted, bool $usePrivate) : bool
{
$decrypted = '';
$keyDetails = getKeyDetails($PEMkey, $usePrivate);
if( $keyDetails === false )
{
echo("failed to get key details!" );
return false;
}
//TODO: block size should be based on $PEMkey size
$DECRYPT_BLOCK_SIZE = $keyDetails["bits"] / 8;
//decode must be done before spliting for getting the binary String
$data = str_split(base64_decode($data), $DECRYPT_BLOCK_SIZE);
foreach($data as $chunk)
{
$partial = '';
//be sure to match padding
// openssl_private_decrypt($chunk, $partial, $PEMkey, OPENSSL_PKCS1_PADDING);
$decryptionOK = false;
if( $usePrivate === true )
{
$decryptionOK = openssl_private_decrypt($chunk, $partial, $PEMkey);
}
else
{
$decryptionOK = openssl_public_decrypt($chunk, $partial, $PEMkey);
}
if($decryptionOK === false)
{
//here also processed errors in decryption. If too big this will be false
echo("decryption failed!" );
return false;
}
$decrypted .= $partial;
}
return true;
}
function performEncryptionTest(string $priKeyStr, string $pubKeyStr, string $message) : bool
{
$encrypted = "";
if( encryptRSA($message, $priKeyStr, $encrypted, true) === false )
{
echo( "failed to encrypt with the private key!" );
return false;
} //encode some message with the pubkey
//try to decrypt
$decrypted = "";
if( decryptRSA($pubKeyStr, $encrypted, $decrypted, false) === false )
{
echo( "failed to decrypt with public key!" );
return false;
}
if( $decrypted !== $message )
{
echo( "decrypted result doesn't match original input!" );
return false;
}
if( encryptRSA($message, $pubKeyStr, $encrypted, false) === false )
{
echo( "failed to encrypt with the public key!" );
return false;
} //encode some message with the pubkey
//try to decrypt
$decrypted = "";
if( decryptRSA($priKeyStr, $encrypted, $decrypted, true) === false )
{
echo( "failed to decrypt with private key!" );
return false;
}
if( $decrypted !== $message )
{
echo( "decrypted result doesn't match original input!");
return false;
}
return true;
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment