meld-cp / obsidian-encrypt

Hide secrets in your Obsidian.md vault
MIT License
544 stars 32 forks source link

Is it possible to port this to C#? #158

Closed RonSijm closed 4 weeks ago

RonSijm commented 3 months ago

Hey there,

I'm using a C# service that's generating documentation regarding certain services - in an obsidian format.

So my goal is to allow my C# service to embed encrypted secrets by the format of this meld-encrypt plugin.

I was trying to make an attempt to convert it:

    using System.Security.Cryptography;
    using System.Text;

    public class CryptoHelper2304(int vectorSize, int saltSize, int iterations)
    {
        private async Task<byte[]> DeriveKey(string password, byte[] salt)
        {
            using var rfc2898 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA512);

            return rfc2898.GetBytes(256 / 8); // 256 bits key size
        }

        private async Task<byte[]> EncryptToBytes(string text, string password)
        {
            var salt = new byte[saltSize];
            using (var rng = new RNGCryptoServiceProvider())
            {
                rng.GetBytes(salt);
            }

            var key = await DeriveKey(password, salt);

            var iv = new byte[vectorSize];
            using (var rng = new RNGCryptoServiceProvider())
            {
                rng.GetBytes(iv);
            }

            using var aesGcm = new AesGcm(key);

            var textBytes = Encoding.UTF8.GetBytes(text);
            var cipherText = new byte[textBytes.Length];

            var tag = new byte[16]; // AES-GCM tag size is 128 bits (16 bytes)
            aesGcm.Encrypt(iv, textBytes, cipherText, tag);

            var finalBytes = new byte[iv.Length + salt.Length + cipherText.Length];
            Buffer.BlockCopy(iv, 0, finalBytes, 0, iv.Length);
            Buffer.BlockCopy(salt, 0, finalBytes, iv.Length, salt.Length);
            Buffer.BlockCopy(cipherText, 0, finalBytes, iv.Length + salt.Length, cipherText.Length);

            return finalBytes;
        }

        private string ConvertToString(byte[] bytes)
        {
            return Encoding.UTF8.GetString(bytes);
        }

        public async Task<string> EncryptToBase64(string text, string password)
        {
            var finalBytes = await EncryptToBytes(text, password);
            return Convert.ToBase64String(finalBytes);
        }

        private async Task<string> DecryptFromBytes(byte[] encryptedBytes, string password)
        {
            var iv = new byte[vectorSize];
            Buffer.BlockCopy(encryptedBytes, 0, iv, 0, vectorSize);

            var salt = new byte[saltSize];
            Buffer.BlockCopy(encryptedBytes, vectorSize, salt, 0, saltSize);

            var cipherText = new byte[encryptedBytes.Length - vectorSize - saltSize];
            Buffer.BlockCopy(encryptedBytes, vectorSize + saltSize, cipherText, 0, encryptedBytes.Length - vectorSize - saltSize);

            var key = await DeriveKey(password, salt);

            using var aesGcm = new AesGcm(key);

            var tag = new byte[16]; // AES-GCM tag size is 128 bits (16 bytes)
            var decryptedBytes = new byte[cipherText.Length];

            aesGcm.Decrypt(iv, cipherText, tag, decryptedBytes);

            return Encoding.UTF8.GetString(decryptedBytes);
        }

        public async Task<string> DecryptFromBase64(string base64Encoded, string password)
        {
            var bytesToDecode = Convert.FromBase64String(base64Encoded);
            return await DecryptFromBytes(bytesToDecode, password);
        }
    }

Which now lets me encrypt and decrypt I think, using the same ES-GCM as the CryptoHelper2304.ts - however my encryption seems to be different from the typescript implementation, and I can't decrypt anything encrypted with this plugin, or visa versa, add my encrypted text into Obsidian and have it decrypt it.

Another strange thing that I found was that you're invoking the service with new CryptoHelper2304( 16, 16, 210000 );

However, debugging my own code, I eventually get into

I get into this method:

if (!nonce.Length.IsLegalSize(NonceByteSizes))
{
    throw new ArgumentException(System.SR.Cryptography_InvalidNonceLength, "nonce");
}

And unless the nonce size is 12, AesGcm rejects the input and throws that exception. So I also can't really even create a new CryptoHelper2304 with a vector size of 16.

Has anyone tried anything similar, or has any idea how to fix this, and make a c# function that's compatible with the function in this library? Thanks

meld-cp commented 3 months ago

I haven't tried js <=> c#. But it is interesting to know if it's possible.

I'm not sure if it helps, but I came across this: https://github.com/smartinmedia/Net-Core-JS-Encryption-Decryption

and

https://pilabor.com/series/dotnet/js-gcm-encrypt-dotnet-decrypt/

RonSijm commented 3 months ago

I tried the first one: https://github.com/smartinmedia/Net-Core-JS-Encryption-Decryption

But it works a little different. For example, when I try to encrypt something, the resulting string becomes: {"DerivationType":"scrypt","Salt":"51b919a3a2d45b68817a4f8ff61426706f34ab2f94235f41f7a75167ebaba9ff","Cost":16384,"BlockSize":16,"Parallel":1,"KeySizeInBytes":16,"DerivationIterations":210000,"AesRijndaelIv":"z2kdcROxxV9EvGfWV6Jc8A==","CipherOutputText":"Fm7ycLD6oeLEZQlD7D44epoCfUHQI1MWs+z0zO3RXx4="}

So it creates a json with metadata. It looks like your plugin appends all the metadata to the string itself, and then slices it during decrypt, right?

I tried to recreate that in C#, but so far I don't really know which value goes where, and what parameters to use, haha. So it's a bit of a trial and error process now

RonSijm commented 3 months ago

I was able to port V0 to C#: https://github.com/RonSijm/RonSijm.ObsidianEncrypt/blob/main/src/RonSijm.ObsidianEncrypt/V0/ObsidianEncryptV0.cs

I was trying to get V2 to work: https://github.com/RonSijm/RonSijm.ObsidianEncrypt/blob/main/src/RonSijm.ObsidianEncrypt/V2/ObsidianEncryptV2.cs

But I can't seem to get it to work.

The PREFIX_OBSOLETE in typescript does not have a "Visible" mode, so it's a bit annoying to use.

Would you be willing to implement a PREFIX_OBSOLETE_VISIBLE? Or do you intend to remove the obsolete version at some point?

meld-cp commented 1 month ago

Hi @RonSijm ,

Sorry I haven't been able to really help with the C# side of things... did you end up getting it to work?

Anyway, 2.4.0-beta.3 implements the missing 'PREFIX_OBSOLETE_VISIBLE'