Contribution to dhive : Added memo encryption and decrypt feature

in #hive-1395313 months ago (edited)

Here is the merge request : https://gitlab.syncad.com/hive/dhive/-/merge_requests/12/diffs

Wow I certainly didn't expect developing such a feature would take me up to 2 week to finish it. Embarking on this journey of developing it made me learn about basic of cryptography and how encrypted message can be transfer securely between two parties. Hereby I wish to give a more thorough tutorial on how you can play with encryption on blockchain.

In cryptography there are two major types of encryption schemes are widely used: symmetric encryption (where a single secret key is used to encrypt and decrypt data) and asymmetric encryption (where a public key cryptosystem is used and encryption and decryption is done using a pair of public and corresponding private key).

In this case, hivejs and dhive is using ecies (Elliptic curve integrated encryption scheme). ECIES standard is a hybrid between symmetric and asymmetric encryption.

It consists of :

  1. ECC- cryptography (public key cryptosystem)
  2. Key-derivation function
  3. symmetric encryption algorithm (for example AES encryption)
  4. MAC algorithm (Message authentication code)

So what each of them do?

The main function here is the AES encryption. It outputs ciphertext as its final result by using randomly generated IV + MAC code + input message.
IV = initial vector (or random salt)
MAC code = Message authentication code exists as an indicator if the encrypted message is being tampered with
input message = plain text message that you wish to encrypt

So what we are doing is just feeding the aes algorithm to do the work.

Library that we used to implement the above are: secp256k1 or P-521 elliptic curve for the public-key calculations + HKDF for KDF function + AES-CBC for symmetric cipher and authentication tag + HMAC-SHA512 for MAC algorithm (in case of unauthenticated encryption).


Source from : Nakov cryptobook

Let's look into the code to have a step-by-step understanding of what's really happening..

The encryption process

export function encode(private_key: PrivateKey | string, public_key: PublicKey | string, memo: string, testNonce?: number) {
    if (!/^#/.test(memo)) return memo
    memo = memo.substring(1)

    private_key = toPrivateObj(private_key) as PrivateKey
    public_key = toPublicObj(public_key) as PublicKey

    const { nonce, message, checksum } = Aes.encrypt(private_key, public_key, memo, testNonce);

    let mbuf = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN)
    Types.EncryptedMemo(mbuf, {
        from: private_key.createPublic(),
        to: public_key,
        nonce,
        check: checksum,
        encrypted: message
    });
    mbuf.flip();
    const data = Buffer.from(mbuf.toBuffer());
    return '#' + bs58.encode(data);
}

As you can see encryption function requires sender's privatekey and recepient's publickey, memo as plain text message that you wish to encrypt.

In comparison with hivejs, dhive written in typescript allow type checking straight from the function paramaters. The code is cleaner and much readable.

As you can see we firstly convert the privatekey of sender and publickey of recipient into a workable object. We then use those keys to derive a shared key that can be embedded to our message, this is to indicate if our message is ever tampered with. If the message content is changed, shared key won't be the same and decryption process will be unsuccessful.

Let's have a look how aes work under the hood :

export function encrypt(private_key: PrivateKey, public_key: PublicKey, message: any, nonce) {
    // Change message to varint32 prefixed encoded string
    let mbuf = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN)
    mbuf.writeVString(message)
    message = Buffer.from(mbuf.flip().toBinary());

    let aesKey = private_key.encapsulate(public_key);
    return crypt(aesKey, nonce = uniqueNonce(), message)
}

export class PrivateKey {
  public secret: Buffer;

  constructor(private key: Buffer) {
    this.secret = key;
    assert(secp256k1.privateKeyVerify(key), "invalid private key");
  }

  /**
   * HMAC based key derivation function
   * @param pub recipient publickey
   */
  public encapsulate(pub: PublicKey): Buffer {
    const master = Buffer.concat([
      pub.uncompressed,
      this.multiply(pub),
    ]);
    return hkdf(master, 64, {
      hash: "SHA-512",
    });
  }

  public multiply(pub: any): Buffer {
    return Buffer.from(secp256k1.publicKeyTweakMul(pub.key, this.secret, false));
  }
}

So at the end you will get ciphered data that looks like this :

+-------------------------------+----------+----------+-----------------+
| 65 Bytes                      | 16 Bytes | 32 Bytes | == data size    |
+-------------------------------+----------+----------+-----------------+
| Sender Public Key (ephemeral) | Nonce/IV | Tag/MAC  | Encrypted data  |
+-------------------------------+----------+----------+-----------------+
|           Secp256k1           |              AES-256-CBC              |
+-------------------------------+---------------------------------------+

Decryption process

Decryption of encrypted message requires unbundle of this packed Buffer and then using privatekey of recipient to unlock the data.

export function decode(private_key: PrivateKey | string, memo: any) {
    if (!/^#/.test(memo)) return memo
    memo = memo.substring(1)
    // checkEncryption()

    private_key = toPrivateObj(private_key) as PrivateKey

    memo = bs58.decode(memo)
    memo = types.EncryptedMemoD(Buffer.from(memo, 'binary'))

    const { from, to, nonce, check, encrypted } = memo
    const pubkey = private_key.createPublic().toString()
    const otherpub = pubkey === new PublicKey(from.key).toString() ? new PublicKey(to.key) : new PublicKey(from.key)
    memo = Aes.decrypt(private_key, otherpub, nonce, encrypted, check)

    // remove varint length prefix
    const mbuf = ByteBuffer.fromBinary(memo.toString('binary'), ByteBuffer.LITTLE_ENDIAN)
    try {
        mbuf.mark()
        return '#' + mbuf.readVString()
    } catch (e) {
        mbuf.reset()
        // Sender did not length-prefix the memo
        memo = Buffer.from(mbuf.toString('binary'), 'binary').toString('utf-8')
        return '#' + memo
    }
}

As dhive itself didn't provide any buffer deserializer. I made one myself as close to serialization function that was written by previous author before.

import * as ByteBuffer from 'bytebuffer'
import { PublicKey } from '../crypto';

export type Deserializer = (buffer: ByteBuffer) => void

const PublicKeyDeserializer = (
        buf: ByteBuffer
) => {
        let c: ByteBuffer = fixed_buf(buf, 33)
        return PublicKey.fromBuffer(c)
}

const UInt64Deserializer = (b: ByteBuffer) => {
        return b.readUint64();
}

const UInt32Deserializer = (b: ByteBuffer) => {
        return b.readUint32();
}

const BinaryDeserializer = (b: ByteBuffer) => {
        let b_copy: ByteBuffer;
        var len = b.readVarint32();
        b_copy = b.copy(b.offset, b.offset + len), b.skip(len);
        return Buffer.from(b_copy.toBinary(), 'binary');
}

const BufferDeserializer = (keyDeserializers: [string, Deserializer][]) => (
        buf: ByteBuffer | Buffer
) => {
        let obj = {};
        for (const [key, deserializer] of keyDeserializers) {
                try {
                        //Decodes a binary encoded string to a ByteBuffer.
                        buf = ByteBuffer.fromBinary(buf.toString("binary"), ByteBuffer.LITTLE_ENDIAN);
                        obj[key] = deserializer(buf)
                } catch (error) {
                        error.message = `${key}: ${error.message}`
                        throw error
                }
        }
        return obj;
}

function fixed_buf(b: ByteBuffer, len: number): Buffer | any {
        if (!b) {
                throw Error('No buffer found on first parameter')
        } else {
                let b_copy = b.copy(b.offset, b.offset + len);
                b.skip(len);
                return Buffer.from(b_copy.toBinary(), 'binary');
        }
}

const EncryptedMemoDeserializer = BufferDeserializer([
        ['from', PublicKeyDeserializer],
        ['to', PublicKeyDeserializer],
        ['nonce', UInt64Deserializer],
        ['check', UInt32Deserializer],
        ['encrypted', BinaryDeserializer]
]);

export const types = {
        EncryptedMemoD: EncryptedMemoDeserializer
}

By running the mocha test, you'll get back the string that was encrypted before! Yeay!

image.png

If you love to see more features added to hivejs or dhive, do upvote this content as a token of encouragement!

Sort:  

Thanks! there's so much to develop on hive!

A part time geek :) Passionate with blockchain tech

Ok, I'm a blockchain passionate too and songwriter. Maybe we could try to share our dream around it

I thought that the ability to encrypt and decrypt memos already exists. Or did you add a different implementation?

This is for dhive/dsteem feature. It was not available in dhive before.

Nice work! I never tried typescript...

I have picked your post for my daily hive voting initiative, Keep it up and Hive On!!

Appreciate your contribution to our Hive

Appreciate your upvote and comment too!

Great job, will sure try it out. Can be a use case for private chatting.

Hmm I believe @kingswisdom did such an app and openseed is also working on such a solution. Perhaps you can come out with something with this library :)