Created
June 9, 2022 04:12
-
-
Save matkatmusic/bd7091ebcbd539aa5d1a5e0a79acf480 to your computer and use it in GitHub Desktop.
OpenSSL <-> juce::RSAKey
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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 ); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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