Skip to content

Encrypting and Decrypting Messages

✌️ edited this page Apr 27, 2022 · 12 revisions

Messages and KVS records are encrypted using Curve25519, Salsa20, and Poly1305 NaCl box and NaCL.secretbox cipher algorithms respectively, then packed in transactions, which are signed and broadcasted to ADAMANT network.

Encrypting a message

An encrypted message uses the following flow:

  • Messages are encrypted using curve25519xsalsa20poly1305 (NaCl box) algorithm. Box is created using sender’s privateKey and recipient’s publicKey.
  • Recipient’s publicKey is retrieved from ADAMANT network
  • privateKey and publicKey of account are instances of Ed25519 signing keys, so they must be converted into Curve25519 Diffie-Hellman keys first

Example of encrypting a message:

import nacl from 'tweetnacl/nacl-fast'
import ed2curve from 'ed2curve'
import sodium from 'sodium-browserify-tweetnacl'

/**
 * Converts a bytes array to the respective string representation
 * @param {Array<number>|Uint8Array} bytes bytes array
 * @returns {string}
 */
export function bytesToHex (bytes = []) {
  const hex = []

  bytes.forEach(b => {
    hex.push((b >>> 4).toString(16))
    hex.push((b & 0xF).toString(16))
  })

  return hex.join('')
}

/**
 * Encodes a text message for sending to ADM
 * @param {string} msg message to encode
 * @param {*} recipientPublicKey recipient's public key
 * @param {*} privateKey our private key
 * @returns {{message: string, nonce: string}}
 */
adamant.encodeMessage = function (msg, recipientPublicKey, privateKey) {
  const nonce = Buffer.allocUnsafe(24)
  sodium.randombytes(nonce)

  if (typeof recipientPublicKey === 'string') {
    recipientPublicKey = hexToBytes(recipientPublicKey)
  }

  const plainText = Buffer.from(msg)
  const DHPublicKey = ed2curve.convertPublicKey(recipientPublicKey)
  const DHSecretKey = ed2curve.convertSecretKey(privateKey)

  const encrypted = nacl.box(plainText, nonce, DHPublicKey, DHSecretKey)

  return {
    message: bytesToHex(encrypted),
    nonce: bytesToHex(nonce)
  }
}

Example of decrypting a message:

import nacl from 'tweetnacl/nacl-fast'
import ed2curve from 'ed2curve'
import { decode } from '@stablelib/utf8'

/**
 * Converts a hex string representation (e.g. `0xdeadbeef`) to the respective byte array
 * @param {string} hexString hex string
 * @returns {Uint8Array}
 */
export function hexToBytes (hexString = '') {
  const bytes = []

  for (let c = 0; c < hexString.length; c += 2) {
    bytes.push(parseInt(hexString.substr(c, 2), 16))
  }

  return Uint8Array.from(bytes)
}

**
 * Decodes the incoming message
 * @param {any} msg encoded message
 * @param {string} senderPublicKey sender public key
 * @param {string} privateKey our private key
 * @param {any} nonce nonce
 * @returns {string}
 */
adamant.decodeMessage = function (msg, senderPublicKey, privateKey, nonce) {
  if (typeof msg === 'string') {
    msg = hexToBytes(msg)
  }

  if (typeof nonce === 'string') {
    nonce = hexToBytes(nonce)
  }

  if (typeof senderPublicKey === 'string') {
    senderPublicKey = hexToBytes(senderPublicKey)
  }

  if (typeof privateKey === 'string') {
    privateKey = hexToBytes(privateKey)
  }

  const DHPublicKey = ed2curve.convertPublicKey(senderPublicKey)
  const DHSecretKey = ed2curve.convertSecretKey(privateKey)
  const decrypted = nacl.box.open(msg, nonce, DHPublicKey, DHSecretKey)

  return decrypted ? decode(decrypted) : ''
}

Encrypting a KVS record

If value needs to be encrypted, the following flow applied:

  • Wrap content to be encrypted into JSON: { "payload": <your value> }
  • Convert the above JSON to string; prefix and suffix the stringified JSON with a random string (alphanumeric ASCII-chars recommended; do not use { or })
  • Encrypt the resulting string using NaCL.secretbox. Secret key is a SHA-256 hash of the ADAMANT privateKey
  • Resulting nonce and encrypted message are wrapped into JSON: { "message": <encrypted message>, "nonce": <nonce> }, which is then saved into the KVS.

Example of encrypting a KVS value:

/
/**
 * Encodes a secret value (available for the owner only)
 * @param {string} value value to encode
 * @param {Uint8Array} privateKey private key
 * @returns {{message: string, nonce: string}} encoded value and nonce (both as HEX-strings)
 */
adamant.encodeValue = function (value, privateKey) {
  const randomString = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, Math.ceil(Math.random() * 10))

  const nonce = Buffer.allocUnsafe(24)
  sodium.randombytes(nonce)

  const padded = randomString() + JSON['stringify']({ payload: value }) + randomString()

  const plainText = Buffer.from(padded)
  const secretKey = ed2curve.convertSecretKey(sodium.crypto_hash_sha256(privateKey))

  const encrypted = nacl.secretbox(plainText, nonce, secretKey)

  return {
    message: bytesToHex(encrypted),
    nonce: bytesToHex(nonce)
  }
}

Example of decrypting a KVS value:

/**
 * Decodes a secret value
 * @param {string|Uint8Array} source source to decrypt
 * @param {Uint8Array} privateKey private key
 * @param {string|Uint8Array} nonce nonce
 * @returns {string} decoded value
 */
adamant.decodeValue = function (source, privateKey, nonce) {
  if (typeof source === 'string') {
    source = hexToBytes(source)
  }

  if (typeof nonce === 'string') {
    nonce = hexToBytes(nonce)
  }

  const secretKey = ed2curve.convertSecretKey(sodium.crypto_hash_sha256(privateKey))
  const decrypted = nacl.secretbox.open(source, nonce, secretKey)

  const strValue = decrypted ? decode(decrypted) : ''
  if (!strValue) return null

  const from = strValue.indexOf('{')
  const to = strValue.lastIndexOf('}')

  if (from < 0 || to < 0) {
    throw new Error('Could not determine JSON boundaries in the encoded value')
  }

  const json = JSON.parse(strValue.substr(from, to - from + 1))
  return json.payload
}