nim-eth-p2p/ethp2p/ecies.nim

394 lines
15 KiB
Nim

#
# Ethereum P2P
# (c) Copyright 2018
# Status Research & Development GmbH
#
# See the file "LICENSE", included in this
# distribution, for details about the copyright.
#
## This module implements ECIES method encryption/decryption.
import ecc, nimcrypto/sha2, nimcrypto/hash, nimcrypto/hmac
import nimcrypto/rijndael, nimcrypto/utils, nimcrypto/sysrand
import nimcrypto/bcmode, nimcrypto/utils
type
EciesException* = object of Exception
EciesStatus* = enum
Success, ## Operation was successful
BufferOverrun, ## Output buffer size is too small
EmptyMessage, ## Attempt to encrypt/decrypt empty message
RandomError, ## Could not obtain random data
EcdhError, ## ECDH shared secret could not be calculated
IncorrectSize, ## ECIES data has incorrect size (size is too low)
WrongHeader, ## ECIES header is incorrect
IncorrectKey, ## Recovered public key is invalid
IncorrectTag ## ECIES tag verification failed
# when false:
# # REVIEW(zah):
# # Why do we work with arrays and known fixed offsets (such sa eciesIvPos)
# # instead of defining object types with named fields:
# type
# EciesPrefix = object
# leadingByte: byte
# pubKey: PublicKey
# iv: array[aes128.sizeBlock]
# # You can then write to these fields by doing:
# var eciesPrefix = cast[ptr EciesPrefix](addr array[0])
# eciesPrefix.pubKey = ...
# eciesPrefix.iv = ...
# # This will make the code slightly easier to read and review for correctness
template eciesOverheadLength*(): int =
## Return data overhead size for ECIES encrypted message
1 + sizeof(PublicKey) + aes128.sizeBlock + sha256.sizeDigest
template eciesEncryptedLength*(size: int): int =
## Return size of encrypted message for message with size `size`.
size + eciesOverheadLength()
template eciesDecryptedLength*(size: int): int =
## Return size of decrypted message for encrypted message with size `size`.
size - eciesOverheadLength()
template eciesMacLength(size: int): int =
## Return size of authenticated data
size + aes128.sizeBlock
template eciesMacPos(size: int): int =
## Return position of MAC code in encrypted block
size - sha256.sizeDigest
template eciesIvPos(): int =
## Return position of IV in encrypted block
sizeof(PublicKey) + 1
template eciesDataPos(): int =
## Return position of encrypted data in block
sizeof(PublicKey) + 1 + aes128.sizeBlock
proc kdf*(data: openarray[byte]): array[KeyLength, byte] {.noInit.} =
## NIST SP 800-56a Concatenation Key Derivation Function (see section 5.8.1)
var ctx: sha256
var counter: uint32
var counterLe: uint32
let reps = ((KeyLength + 7) * 8) div (int(ctx.sizeDigest) * 8)
var offset = 0
# REVIEW: There is a relationship between KeyLength and sha256.sizeDigest here
# that could be expressed in the code with a static assert.
var storage = newSeq[byte](KeyLength * (reps + 1))
while counter <= uint32(reps):
counter = counter + 1
counterLe = LSWAP(counter)
ctx.init()
ctx.update(cast[ptr byte](addr counterLe), uint(sizeof(uint32)))
ctx.update(unsafeAddr data[0], uint(len(data)))
# REVIEW: unnecessary copy here
var hash = ctx.finish().data
copyMem(addr storage[offset], addr hash[0], ctx.sizeDigest)
offset += int(ctx.sizeDigest)
ctx.init() # clean ctx
copyMem(addr result[0], addr storage[0], KeyLength)
# REVIEW(zah): We can make Araq happy by using the new openarray
# for these input and output parameters
proc eciesEncrypt*(inp, oup: ptr byte, inl, oul: int, pubkey: PublicKey,
shmac: ptr byte = nil, shlen: int = 0): EciesStatus =
## Encrypt data with ECIES method to the given public key `pubkey`.
##
## `inp` - [INPUT] pointer to input data
## `oup` - [INPUT] pointer to output data
## `inl` - [INPUT] input data size
## `oul` - [INPUT] output data size
## `pubkey` - [INPUT] Ecc secp256k1 public key
## `shmac` - [INPUT] additional mac data
## `shlen` - [INPUT] additional mac data size
var
encKey: array[KeyLength div 2, byte]
macKey: array[KeyLength, byte]
cipher: CTR[aes128]
ctx: HMAC[sha256]
iv: array[aes128.sizeBlock, byte]
tag: array[sha256.sizeDigest, byte]
secret: SharedSecret
material: array[KeyLength, byte]
assert(not isNil(inp) and not isNil(oup))
assert(inl > 0 and oul > 0)
if oul < eciesEncryptedLength(inl):
return(BufferOverrun)
if randomBytes(addr iv[0], len(iv)) != len(iv):
return(RandomError)
var ephemeral = newKeyPair()
var output = cast[ptr UncheckedArray[byte]](oup)
var epub = ephemeral.pubkey.getRaw()
if ecdhAgree(ephemeral.seckey, pubkey, secret) != EccStatus.Success:
return(EcdhError)
material = kdf(secret)
when false:
# REVIEW: Please try to write the code in a way that's easy to review
# only by looking at the current line. For example, the zeroMem call
# below could have been written:
zeroMem(addr secret[0], sizeof(secret))
# or even better:
zeroArray(secret)
# where `zeroArray` is a template that does the right thing:
template zeroArray(a: array) = zeroMem(unsafeAddr a[0], sizeof(a))
# When constants are used, sometimes errors will slip through the
# cracks after copy/pasting code and it's harder to notice the problem
# in a code review.
zeroMem(addr secret[0], sizeof(SharedSecret)) # clean shared secret
copyMem(addr encKey[0], addr material[0], KeyLength div 2)
# REVIEW: The line below will introduce an array copy. Is this intentional?
# If you store the result MDigest value on the stack and use the `data` field
# in `ctx.init` below, there won't be copies. I've also noticed that you are
# trying to zero out the `macKey` variable at the end of the function, which
# I assume is done as a security measure. The temporary MDigest here will
# store the same bytes and won't be zeroed out.
macKey = sha256.digest(material, KeyLength div 2).data
zeroMem(addr material[0], KeyLength) # clean material
cipher.init(addr encKey[0], addr iv[0])
cipher.encrypt(inp, cast[ptr byte](addr output[eciesDataPos()]), uint(inl))
zeroMem(addr encKey[0], KeyLength div 2) # clean encKey
zeroMem(addr cipher, sizeof(CTR[aes128])) # clean cipher context
output[0] = 0x04
copyMem(addr output[1], addr epub.data[0], sizeof(PublicKey))
copyMem(addr output[eciesIvPos()], addr iv[0], aes128.sizeBlock)
ctx.init(addr macKey[0], uint(len(macKey)))
ctx.update(addr output[eciesIvPos()], uint(eciesMacLength(inl)))
if not isNil(shmac) and shlen > 0:
ctx.update(shmac, uint(shlen))
tag = ctx.finish().data
# REVIEW: If this is an important step after creating a HMAC, perhaps
# it could be provided as an alternative way to call `finish` or
# at least it could be a proc like `ctx.clear()`
zeroMem(addr ctx, sizeof(HMAC[sha256])) # clean hmac context
zeroMem(addr macKey[0], KeyLength) # clean macKey
copyMem(addr output[eciesDataPos() + inl], addr tag[0], sha256.sizeDigest)
result = Success
proc eciesDecrypt*(inp, oup: ptr byte, inl, oul: int, seckey: PrivateKey,
shmac: ptr byte = nil, shlen: int = 0): EciesStatus =
## Decrypt data with ECIES method using the given private key `seckey`.
##
## `inp` - [INPUT] pointer to input data
## `oup` - [INPUT] pointer to output data
## `inl` - [INPUT] input data size
## `oul` - [INPUT] output data size
## `seckey` - [INPUT] Ecc secp256k1 private key
## `shmac` - [INPUT] additional mac data (default = nil)
## `shlen` - [INPUT] additional mac data size (default = 0)
var
pubkey: PublicKey
encKey: array[KeyLength div 2, byte]
macKey: array[KeyLength, byte]
tag: array[sha256.sizeDigest, byte]
cipher: CTR[aes128]
ctx: HMAC[sha256]
secret: SharedSecret
assert(not isNil(inp) and not isNil(oup))
assert(inl > 0 and oul > 0)
var input = cast[ptr UncheckedArray[byte]](inp)
if inl <= eciesOverheadLength():
return(IncorrectSize)
if inl - eciesOverheadLength() > oul:
return(BufferOverrun)
if input[0] != 0x04:
return(WrongHeader)
if recoverPublicKey(addr input[1], KeyLength * 2,
pubkey) != EccStatus.Success:
return(IncorrectKey)
if ecdhAgree(seckey, pubkey, secret) != EccStatus.Success:
return(EcdhError)
var material = kdf(secret)
zeroMem(addr secret[0], sizeof(SharedSecret)) # clean shared secret
copyMem(addr encKey[0], addr material[0], KeyLength div 2)
# REVIEW: unnecessary copy
macKey = sha256.digest(material, KeyLength div 2).data
zeroMem(addr material[0], KeyLength) # clean material
let macsize = eciesMacLength(inl - eciesOverheadLength())
ctx.init(addr macKey[0], uint(len(macKey)))
ctx.update(addr input[eciesIvPos()], uint(macsize))
if not isNil(shmac) and shlen > 0:
ctx.update(shmac, uint(shlen))
tag = ctx.finish().data
zeroMem(addr ctx, sizeof(HMAC[sha256])) # clean hmac context
zeroMem(addr macKey[0], KeyLength) # clean macKey
if not equalMem(addr tag[0], addr input[eciesMacPos(inl)], sha256.sizeDigest):
return(IncorrectTag)
cipher.init(addr encKey[0], addr input[eciesIvPos()])
cipher.decrypt(cast[ptr byte](addr input[eciesDataPos()]),
cast[ptr byte](oup), uint(inl - eciesOverheadLength()))
zeroMem(addr encKey[0], KeyLength div 2) # clean encKey
zeroMem(addr cipher, sizeof(CTR[aes128])) # clean cipher context
result = Success
proc eciesEncrypt*[A, B](input: openarray[A],
pubkey: PublicKey,
output: var openarray[B],
outlen: var int,
ostart: int = 0,
ofinish: int = -1): EciesStatus =
## Encrypt data with ECIES method to the given public key `pubkey`.
##
## `input` - [INPUT] input data
## `pubkey` - [INPUT] Ecc secp256k1 public key
## `output` - [OUTPUT] output data
## `outlen` - [OUTPUT] output data size
## `ostart` - [INPUT] starting index in `data` (default = -1, start of input)
## `ofinish` - [INPUT] ending index in `data` (default = -1, whole input)
##
## Encryption is done on `data` with inclusive range [ostart, ofinish]
## Negative values of `ostart` and `ofinish` are treated as index with value
## (len(data) + `ostart/ofinish`).
let so = if ostart < 0: (len(input) + ostart) else: ostart
let eo = if ofinish < 0: (len(input) + ofinish) else: ofinish
let length = (eo - so + 1) * sizeof(A)
# We don't need to check `so` because compiler will do it for `data[so]`.
if eo >= len(input):
return(BufferOverrun)
if len(input) == 0:
return(EmptyMessage)
let esize = eciesEncryptedLength(length)
if (len(output) * sizeof(B)) < esize:
return(BufferOverrun)
outlen = esize
result = eciesEncrypt(cast[ptr byte](unsafeAddr input[so]), addr output[0],
length, esize, pubkey)
proc eciesEncrypt*[A, B, C](input: openarray[A],
pubkey: PublicKey,
output: var openarray[B],
outlen: var int,
shmac: openarray[C],
ostart: int = 0,
ofinish: int = -1): EciesStatus =
## Encrypt data with ECIES method to the given public key `pubkey`.
##
## `input` - [INPUT] input data
## `pubkey` - [INPUT] Ecc secp256k1 public key
## `output` - [OUTPUT] output data
## `outlen` - [OUTPUT] output data size
## `shmac` - [INPUT] additional mac data
## `ostart` - [INPUT] starting index in `data` (default = -1, start of input)
## `ofinish` - [INPUT] ending index in `data` (default = -1, whole input)
##
## Encryption is done on `data` with inclusive range [ostart, ofinish]
## Negative values of `ostart` and `ofinish` are treated as index with value
## (len(data) + `ostart/ofinish`).
let so = if ostart < 0: (len(input) + ostart) else: ostart
let eo = if ofinish < 0: (len(input) + ofinish) else: ofinish
let length = (eo - so + 1) * sizeof(A)
# We don't need to check `so` because compiler will do it for `data[so]`.
if eo >= len(input):
return(BufferOverrun)
if len(input) == 0:
return(EmptyMessage)
let esize = eciesEncryptedLength(length)
if len(output) * sizeof(B) < esize:
return(BufferOverrun)
outlen = esize
result = eciesEncrypt(cast[ptr byte](unsafeAddr input[so]), addr output[0],
length, esize, pubkey,
cast[ptr byte](unsafeAddr shmac[0]),
len(shmac) * sizeof(C))
proc eciesDecrypt*[A, B](input: openarray[A],
seckey: PrivateKey,
output: var openarray[B],
outlen: var int,
ostart: int = 0,
ofinish: int = -1): EciesStatus =
## Decrypt data with ECIES method using given private key `seckey`.
##
## `input` - [INPUT] input data
## `seckey` - [INPUT] Ecc secp256k1 private key
## `output` - [OUTPUT] output data
## `outlen` - [OUTPUT] output data size
## `ostart` - [INPUT] starting index in `data` (default = -1, start of input)
## `ofinish` - [INPUT] ending index in `data` (default = -1, whole input)
##
## Decryption is done on `data` with inclusive range [ostart, ofinish]
let so = if ostart < 0: (len(input) + ostart) else: ostart
let eo = if ofinish < 0: (len(input) + ofinish) else: ofinish
let length = (eo - so + 1) * sizeof(A)
# We don't need to check `so` because compiler will do it for `data[so]`.
if eo >= len(input):
return(BufferOverrun)
if len(input) == 0:
return(EmptyMessage)
let dsize = eciesDecryptedLength(length)
if len(output) * sizeof(B) < dsize:
return(BufferOverrun)
outlen = dsize
result = eciesDecrypt(cast[ptr byte](unsafeAddr input[so]), addr output[0],
length, dsize, seckey)
proc eciesDecrypt*[A, B, C](input: openarray[A],
seckey: PrivateKey,
output: var openarray[B],
outlen: var int,
shmac: openarray[C],
ostart: int = 0,
ofinish: int = -1): EciesStatus =
## Decrypt data with ECIES method using given private key `seckey`.
##
## `input` - [INPUT] input data
## `seckey` - [INPUT] Ecc secp256k1 private key
## `output` - [OUTPUT] output data
## `outlen` - [OUTPUT] output data size
## `shmac` - additional mac data
## `ostart` - starting index in `data` (default = -1, data[0])
## `ofinish` - ending index in `data` (default = -1, data[len(data) - 1])
##
## Decryption is done on `data` with inclusive range [ostart, ofinish]
let so = if ostart < 0: (len(input) + ostart) else: ostart
let eo = if ofinish < 0: (len(input) + ofinish) else: ofinish
let length = (eo - so + 1) * sizeof(A)
# We don't need to check `so` because compiler will do it for `data[so]`.
if eo >= len(input):
return(BufferOverrun)
if len(input) == 0:
return(EmptyMessage)
let dsize = eciesDecryptedLength(length)
if len(output) * sizeof(B) < dsize:
return(BufferOverrun)
outlen = dsize
result = eciesDecrypt(cast[ptr byte](unsafeAddr input[so]), addr output[0],
length, dsize, seckey,
cast[ptr byte](unsafeAddr shmac[0]),
len(shmac) * sizeof(C))