mirror of https://github.com/status-im/nim-eth.git
Discv5: Specifically handle decryption errors
This commit is contained in:
parent
19a8c6ee0c
commit
596f148974
|
@ -50,3 +50,10 @@ method loadKeys*(db: DiscoveryDB, id: NodeId, address: Address, r, w: var AesKey
|
||||||
except CatchableError:
|
except CatchableError:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
method deleteKeys*(db: DiscoveryDB, id: NodeId, address: Address):
|
||||||
|
bool {.raises: [Defect].} =
|
||||||
|
try:
|
||||||
|
db.backend.del(makeKey(id, address))
|
||||||
|
return true
|
||||||
|
except CatchableError:
|
||||||
|
return false
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import
|
import
|
||||||
std/tables, nimcrypto, stint, chronicles,
|
std/[tables, options], nimcrypto, stint, chronicles,
|
||||||
types, node, enr, hkdf, ../enode, eth/[rlp, keys]
|
types, node, enr, hkdf, ../enode, eth/[rlp, keys]
|
||||||
|
|
||||||
const
|
const
|
||||||
|
@ -7,7 +7,7 @@ const
|
||||||
keyAgreementPrefix = "discovery v5 key agreement"
|
keyAgreementPrefix = "discovery v5 key agreement"
|
||||||
authSchemeName* = "gcm"
|
authSchemeName* = "gcm"
|
||||||
gcmNonceSize* = 12
|
gcmNonceSize* = 12
|
||||||
gcmTagSize = 16
|
gcmTagSize* = 16
|
||||||
tagSize* = 32 ## size of the tag where each message (except whoareyou) starts
|
tagSize* = 32 ## size of the tag where each message (except whoareyou) starts
|
||||||
## with
|
## with
|
||||||
|
|
||||||
|
@ -43,7 +43,8 @@ type
|
||||||
DecodeStatus* = enum
|
DecodeStatus* = enum
|
||||||
Success,
|
Success,
|
||||||
HandshakeError,
|
HandshakeError,
|
||||||
PacketError
|
PacketError,
|
||||||
|
DecryptError
|
||||||
|
|
||||||
proc randomBytes*(v: var openarray[byte]) =
|
proc randomBytes*(v: var openarray[byte]) =
|
||||||
if nimcrypto.randomBytes(v) != v.len:
|
if nimcrypto.randomBytes(v) != v.len:
|
||||||
|
@ -159,17 +160,25 @@ proc encodeEncrypted*(c: Codec,
|
||||||
headBuf.add(encryptGCM(writeKey, nonce, body, tag))
|
headBuf.add(encryptGCM(writeKey, nonce, body, tag))
|
||||||
return (headBuf, nonce)
|
return (headBuf, nonce)
|
||||||
|
|
||||||
proc decryptGCM(key: AesKey, nonce, ct, authData: openarray[byte]): seq[byte] =
|
proc decryptGCM*(key: AesKey, nonce, ct, authData: openarray[byte]):
|
||||||
|
Option[seq[byte]] =
|
||||||
|
if ct.len <= gcmTagSize:
|
||||||
|
debug "cipher is missing tag", len = ct.len
|
||||||
|
return
|
||||||
|
|
||||||
var dctx: GCM[aes128]
|
var dctx: GCM[aes128]
|
||||||
dctx.init(key, nonce, authData)
|
dctx.init(key, nonce, authData)
|
||||||
result = newSeq[byte](ct.len - gcmTagSize)
|
var res = newSeq[byte](ct.len - gcmTagSize)
|
||||||
var tag: array[gcmTagSize, byte]
|
var tag: array[gcmTagSize, byte]
|
||||||
dctx.decrypt(ct.toOpenArray(0, ct.high - gcmTagSize), result)
|
dctx.decrypt(ct.toOpenArray(0, ct.high - gcmTagSize), res)
|
||||||
dctx.getTag(tag)
|
dctx.getTag(tag)
|
||||||
if tag != ct.toOpenArray(ct.len - gcmTagSize, ct.high):
|
|
||||||
result = @[]
|
|
||||||
dctx.clear()
|
dctx.clear()
|
||||||
|
|
||||||
|
if tag != ct.toOpenArray(ct.len - gcmTagSize, ct.high):
|
||||||
|
return
|
||||||
|
|
||||||
|
return some(res)
|
||||||
|
|
||||||
type
|
type
|
||||||
DecodePacketResult = enum
|
DecodePacketResult = enum
|
||||||
decodingSuccessful
|
decodingSuccessful
|
||||||
|
@ -222,7 +231,10 @@ proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader,
|
||||||
|
|
||||||
var zeroNonce: array[gcmNonceSize, byte]
|
var zeroNonce: array[gcmNonceSize, byte]
|
||||||
let respData = decryptGCM(secrets.authRespKey, zeroNonce, head.response, [])
|
let respData = decryptGCM(secrets.authRespKey, zeroNonce, head.response, [])
|
||||||
let authResp = rlp.decode(respData, AuthResponse)
|
if respData.isNone():
|
||||||
|
return false
|
||||||
|
|
||||||
|
let authResp = rlp.decode(respData.get(), AuthResponse)
|
||||||
|
|
||||||
newNode = newNode(authResp.record)
|
newNode = newNode(authResp.record)
|
||||||
return true
|
return true
|
||||||
|
@ -275,7 +287,7 @@ proc decodeEncrypted*(c: var Codec,
|
||||||
var writeKey: AesKey
|
var writeKey: AesKey
|
||||||
if not c.db.loadKeys(fromId, fromAddr, readKey, writeKey):
|
if not c.db.loadKeys(fromId, fromAddr, readKey, writeKey):
|
||||||
trace "Decoding failed (no keys)"
|
trace "Decoding failed (no keys)"
|
||||||
return PacketError
|
return DecryptError
|
||||||
# doAssert(false, "TODO: HANDLE ME!")
|
# doAssert(false, "TODO: HANDLE ME!")
|
||||||
|
|
||||||
let headSize = tagSize + r.position
|
let headSize = tagSize + r.position
|
||||||
|
@ -283,8 +295,14 @@ proc decodeEncrypted*(c: var Codec,
|
||||||
|
|
||||||
let body = decryptGCM(readKey, auth.auth, bodyEnc.toOpenArray,
|
let body = decryptGCM(readKey, auth.auth, bodyEnc.toOpenArray,
|
||||||
input[0 .. tagSize - 1].toOpenArray)
|
input[0 .. tagSize - 1].toOpenArray)
|
||||||
if body.len > 1:
|
if body.isNone():
|
||||||
let status = decodePacketBody(body[0], body.toOpenArray(1, body.high), packet)
|
discard c.db.deleteKeys(fromId, fromAddr)
|
||||||
|
return DecryptError
|
||||||
|
|
||||||
|
let packetData = body.get()
|
||||||
|
if packetData.len > 1:
|
||||||
|
let status = decodePacketBody(packetData[0],
|
||||||
|
packetData.toOpenArray(1, packetData.high), packet)
|
||||||
if status == decodingSuccessful:
|
if status == decodingSuccessful:
|
||||||
return Success
|
return Success
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -211,11 +211,17 @@ proc receive*(d: Protocol, a: Address, msg: Bytes) {.gcsafe,
|
||||||
waiter.complete(packet.some)
|
waiter.complete(packet.some)
|
||||||
else:
|
else:
|
||||||
debug "TODO: handle packet: ", packet = packet.kind, origin = $node
|
debug "TODO: handle packet: ", packet = packet.kind, origin = $node
|
||||||
elif decoded == DecodeStatus.PacketError:
|
elif decoded == DecodeStatus.DecryptError:
|
||||||
debug "Could not decode packet, respond with whoareyou",
|
debug "Could not decrypt packet, respond with whoareyou",
|
||||||
localNode = $d.localNode, address = a
|
localNode = $d.localNode, address = a
|
||||||
|
# only sendingWhoareyou in case it is a decryption failure
|
||||||
d.sendWhoareyou(a, sender, authTag)
|
d.sendWhoareyou(a, sender, authTag)
|
||||||
# No Whoareyou in case it is a Handshake Failure
|
elif decoded == DecodeStatus.PacketError:
|
||||||
|
# Still adding the node in case there is a packet error (could be
|
||||||
|
# unsupported packet)
|
||||||
|
if not node.isNil:
|
||||||
|
debug "Adding new node to routing table", node = $node, localNode = $d.localNode
|
||||||
|
discard d.routingTable.addNode(node)
|
||||||
|
|
||||||
proc waitPacket(d: Protocol, fromNode: Node, reqId: RequestId): Future[Option[Packet]] =
|
proc waitPacket(d: Protocol, fromNode: Node, reqId: RequestId): Future[Option[Packet]] =
|
||||||
result = newFuture[Option[Packet]]("waitPacket")
|
result = newFuture[Option[Packet]]("waitPacket")
|
||||||
|
|
|
@ -87,6 +87,9 @@ method storeKeys*(db: Database, id: NodeId, address: Address, r, w: AesKey):
|
||||||
method loadKeys*(db: Database, id: NodeId, address: Address, r, w: var AesKey):
|
method loadKeys*(db: Database, id: NodeId, address: Address, r, w: var AesKey):
|
||||||
bool {.base, raises: [Defect].} = discard
|
bool {.base, raises: [Defect].} = discard
|
||||||
|
|
||||||
|
method deleteKeys*(db: Database, id: NodeId, address: Address):
|
||||||
|
bool {.raises: [Defect].} = discard
|
||||||
|
|
||||||
proc toBytes*(id: NodeId): array[32, byte] {.inline.} =
|
proc toBytes*(id: NodeId): array[32, byte] {.inline.} =
|
||||||
id.toByteArrayBE()
|
id.toByteArrayBE()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import
|
import
|
||||||
unittest, stew/byteutils, stint,
|
unittest, options, sequtils, stew/byteutils, stint,
|
||||||
eth/[rlp, keys] , eth/p2p/discoveryv5/[types, encoding, enr]
|
eth/[rlp, keys] , eth/p2p/discoveryv5/[types, encoding, enr]
|
||||||
|
|
||||||
# According to test vectors:
|
# According to test vectors:
|
||||||
|
@ -141,16 +141,16 @@ suite "Discovery v5 Cryptographic Primitives":
|
||||||
eph.data == hexToSeqByte(sharedSecret)
|
eph.data == hexToSeqByte(sharedSecret)
|
||||||
|
|
||||||
test "Key Derivation":
|
test "Key Derivation":
|
||||||
const
|
# const
|
||||||
# input
|
# # input
|
||||||
secretKey = "0x02a77e3aa0c144ae7c0a3af73692b7d6e5b7a2fdc0eda16e8d5e6cb0d08e88dd04"
|
# secretKey = "0x02a77e3aa0c144ae7c0a3af73692b7d6e5b7a2fdc0eda16e8d5e6cb0d08e88dd04"
|
||||||
nodeIdA = "0xa448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"
|
# nodeIdA = "0xa448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"
|
||||||
nodeIdB = "0x885bba8dfeddd49855459df852ad5b63d13a3fae593f3f9fa7e317fd43651409"
|
# nodeIdB = "0x885bba8dfeddd49855459df852ad5b63d13a3fae593f3f9fa7e317fd43651409"
|
||||||
idNonce = "0x0101010101010101010101010101010101010101010101010101010101010101"
|
# idNonce = "0x0101010101010101010101010101010101010101010101010101010101010101"
|
||||||
# expected output
|
# # expected output
|
||||||
initiatorKey = "0x238d8b50e4363cf603a48c6cc3542967"
|
# initiatorKey = "0x238d8b50e4363cf603a48c6cc3542967"
|
||||||
recipientKey = "0xbebc0183484f7e7ca2ac32e3d72c8891"
|
# recipientKey = "0xbebc0183484f7e7ca2ac32e3d72c8891"
|
||||||
authRespKey = "0xe987ad9e414d5b4f9bfe4ff1e52f2fae"
|
# authRespKey = "0xe987ad9e414d5b4f9bfe4ff1e52f2fae"
|
||||||
|
|
||||||
# Code doesn't allow to start from shared `secretKey`, but only from the
|
# Code doesn't allow to start from shared `secretKey`, but only from the
|
||||||
# public and private key. Would require pulling `ecdhAgree` out of
|
# public and private key. Would require pulling `ecdhAgree` out of
|
||||||
|
@ -194,3 +194,38 @@ suite "Discovery v5 Cryptographic Primitives":
|
||||||
# The encryption of the auth-resp-pt uses one of these keys, as does the
|
# The encryption of the auth-resp-pt uses one of these keys, as does the
|
||||||
# encryption of the message itself. So the whole test depends on this.
|
# encryption of the message itself. So the whole test depends on this.
|
||||||
skip()
|
skip()
|
||||||
|
|
||||||
|
suite "Discovery v5 Additional":
|
||||||
|
test "Encryption/Decryption":
|
||||||
|
let
|
||||||
|
encryptionKey = hexToByteArray[aesKeySize]("0x9f2d77db7004bf8a1a85107ac686990b")
|
||||||
|
nonce = hexToByteArray[authTagSize]("0x27b5af763c446acd2749fe8e")
|
||||||
|
ad = hexToByteArray[tagSize]("0x93a7400fa0d6a694ebc24d5cf570f65d04215b6ac00757875e3f3a5f42107903")
|
||||||
|
pt = hexToSeqByte("0xa1")
|
||||||
|
|
||||||
|
let ct = encryptGCM(encryptionKey, nonce, pt, ad)
|
||||||
|
let decrypted = decryptGCM(encryptionKey, nonce, ct, ad)
|
||||||
|
|
||||||
|
check decrypted.get() == pt
|
||||||
|
|
||||||
|
test "Decryption":
|
||||||
|
let
|
||||||
|
encryptionKey = hexToByteArray[aesKeySize]("0x9f2d77db7004bf8a1a85107ac686990b")
|
||||||
|
nonce = hexToByteArray[authTagSize]("0x27b5af763c446acd2749fe8e")
|
||||||
|
ad = hexToByteArray[tagSize]("0x93a7400fa0d6a694ebc24d5cf570f65d04215b6ac00757875e3f3a5f42107903")
|
||||||
|
pt = hexToSeqByte("0x01c20101")
|
||||||
|
ct = hexToSeqByte("0xa5d12a2d94b8ccb3ba55558229867dc13bfa3648")
|
||||||
|
|
||||||
|
# valid case
|
||||||
|
check decryptGCM(encryptionKey, nonce, ct, ad).get() == pt
|
||||||
|
|
||||||
|
# invalid tag/data sizes
|
||||||
|
var invalidCipher: seq[byte] = @[]
|
||||||
|
check decryptGCM(encryptionKey, nonce, invalidCipher, ad).isNone()
|
||||||
|
|
||||||
|
invalidCipher = repeat(byte(4), gcmTagSize)
|
||||||
|
check decryptGCM(encryptionKey, nonce, invalidCipher, ad).isNone()
|
||||||
|
|
||||||
|
# invalid tag/data itself
|
||||||
|
invalidCipher = repeat(byte(4), gcmTagSize + 1)
|
||||||
|
check decryptGCM(encryptionKey, nonce, invalidCipher, ad).isNone()
|
||||||
|
|
Loading…
Reference in New Issue