Discv5: Specifically handle decryption errors

This commit is contained in:
kdeme 2020-03-10 16:01:04 +01:00
parent 19a8c6ee0c
commit 596f148974
No known key found for this signature in database
GPG Key ID: 4E8DD21420AF43F5
5 changed files with 95 additions and 26 deletions

View File

@ -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

View File

@ -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:

View File

@ -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")

View File

@ -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()

View File

@ -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()