Skip to content

Add support of encrypted payload for Cloud Device API#1715

Open
gcatanese wants to merge 6 commits into
mainfrom
cloud-device-add-encryption
Open

Add support of encrypted payload for Cloud Device API#1715
gcatanese wants to merge 6 commits into
mainfrom
cloud-device-add-encryption

Conversation

@gcatanese

Copy link
Copy Markdown
Contributor

Summary

This PR adds support for encrypted payloads in the Cloud Device API, enabling end-to-end encryption for terminal communication.

Changes

New service

  • EncryptedCloudDeviceApi — support sending and receiving encrypted payloads

New models

  • CloudDeviceApiSecuredRequest / CloudDeviceApiSecuredResponse — wrappers for encrypted request/response payloads
  • SaleToPOISecuredMessage — represents a secured (encrypted) Sale-to-POI message
  • clouddevice/security/NexoDerivedKey — derived key model used in the encryption scheme
  • clouddevice/security/SecurityTrailer — security trailer attached to encrypted messages

New security classes

  • EncryptionCredentialDetails — holds the encryption credential configuration
  • NexoCryptoPrimitives — low-level crypto primitives (AES, HMAC, etc.)
  • NexoDerivedKeyGenerator — derives session keys from a master key
  • NexoSecurityException — dedicated exception for security/encryption errors
  • NexoSecurityManager — orchestrates encryption and decryption of Cloud Device API messages

Tests

  • NexoSecurityManagerTest — unit tests for the encryption/decryption logic
  • EncryptedCloudDeviceApiTest — integration-style tests for the full encrypted request/response flow, backed by fixture JSON files

@gcatanese gcatanese requested a review from a team as a code owner July 6, 2026 12:23
@gcatanese gcatanese added the Feature New feature or request label Jul 6, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the EncryptedCloudDeviceApi service along with cryptographic utilities (NexoSecurityManager, nexoCryptoPrimitives, and nexoDerivedKeyGenerator) to support encrypted payloads (AES-256-CBC and HMAC-SHA256) for the Cloud Device API. It also adds comprehensive unit tests and updates terminal specs to verify encrypted requests. The code review identified several important issues, including a security vulnerability in crypt due to missing ivNonce length validation, side effects from directly mutating request parameters in the API methods, a potential runtime crash in decryptNotification when parsing non-object JSON payloads, and performance concerns regarding synchronous key derivation blocking the event loop.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/security/clouddevice/nexoCryptoPrimitives.ts
Comment thread src/services/clouddevice/encryptedCloudDeviceApi.ts
Comment thread src/services/clouddevice/encryptedCloudDeviceApi.ts
Comment thread src/services/clouddevice/encryptedCloudDeviceApi.ts
Comment thread src/security/clouddevice/nexoDerivedKeyGenerator.ts
@gcatanese gcatanese force-pushed the cloud-device-add-encryption branch from a3f1efb to 419f085 Compare July 6, 2026 12:42
@gcatanese gcatanese enabled auto-merge July 6, 2026 12:43
"SaleID": "406aa21f8b",
"ServiceID": "406aa21f8b"
},
"NexoBlob": "MnqvlI21WRfLyhkHUz3WkoKtn2+jq2zlb8ACsEj59vDSQmMMw5QHCyknuqP85scrhU6DsHkyjq3LWN7XxB9lADzfIIO\/6IklAfVP+aqz7zADlT6erP2Kq+\/uDKILQEAg6jikpCkCEhHQmQ+E2P7rswKyYU5I\/wfcGs46AkZ9iaBq\/DkvMDkv+3WGfN6qf\/sYmom\/zJWcWXArvsRub1\/sBsAI5QyxE+2Thsl4wfNBslrHFiIWi3akLXpTnU8wnUAykCxEjYH6kqTu2NI00iN1mNU6lhEbFyC6Nm7byjHSNkVGuHve4yHE0HMV080pyCD3ZkHGuzle54ktumLh+UdlXhZt6jNHvKZzMu4MuHjhUJuLtN1Mw\/bIEtUu5uevGyZHWZzR59PuyjBiyLF1XluNG342lyjZwTXQ\/x9OP+mx7dmINQGsNp0RKk1gC1wpKRIIHkwrmbmqSoPo6oBwg3t0V\/\/IsHLETCPXF4Ekvy\/XEw+4noMtcQMiA2x6DZK0t9Y3c+QfAYTGCWLRwXqxom+6xucZqfTtjosJXDrLTWcfHgnpwlplNv+wfzJN1RWxEY2ZV\/6gXXkuZymdlJEMtP6hF70Db2uD9MetM\/FCWxGvGQTrRA4AErPJgpA4+YnMA6K41e7DszfZNRMGs+6IJPsyvEBby5T1El1z4v1nEkPeFASDmuWICYxc6Ki8i7jdYgxmD9ztZSiKY8zrtGvIna4nxZoMbgxp6UFN0ws+Rqg++mJUkvQY0S9e1A\/CLhrxIICpOaqGPMPAWzf6nhz5HOKpdMPs2ZUrjgN7KSW28QsAtIcBfQEjZYKCvn3f68ERTEEU68vp+FfQUnXNKFQ7vH4ytuDzEqkgzD+bCbkh7VVzrJ1GdzuH+N1aBi+Hh73Qnk06\/LVdSvCp7L9CI\/8gG3oZDTk2t5BVNuHzbTsV5NWamgzJ0wtRqu6ya3qohtQNIvCMiYfNpT936hbiUyudbqAtakrgGBLo9xKWGLjjOALRMM8+URzK9kazCYg5ENFR7A8DGKpfVZEEwonOK2ISKT1\/9UbUHMrFS4UrVrginXmakwvVmCBXHgOrhMWQWFU1pDGZFaCMRMaa+2M87SGVMh0rIH+jihbfBmmTpu\/iMbjP2Qc5ZqgCnTYAr4ZECzR4cbxYxKDtvL0yWc5FxZGGxQuM6Hk+K7IfOfbUitVeyUFyGAA3t3SYbv8CCHQGCXeE6OZI92tP0T09HpbJB70s8rFYmAHR6k\/hcGNojOuuWj1hbRIdIVvdaAnazYvizQEGr\/cfZvP0Fu6WfPcggYHbGVdmVE+s61yaqJTKoFEpnrvcwaHLhIrdFD62KErll9Of46bH1s3FQokRnN\/\/lCQwjjFjP6cHWfZTVaXZLhwJ1QVXk4+567+e4mtnUk1Zx0WOQNjqfjorLPfsafmfrFLUALZYHvNYdPjtshON8Mo1QiQLtMMqzW6RmMl5VDmIlDE5m0L6QiV9SwVbo15hu0ga+ZzjffvglZzQXJbdmxq9orZspsk4NeSjE5rwQHpIamE5AtIjd+oFZwM4x13fCmECQTExY5FuUcBbRT7t0SIHRGnlFJUdzzFCPJqqvotBiVQKmx34K6+kdS5BRe2JMmyQcWd3uO11k8QWIPQMa5TzKhsDnvYeOy\/IxPPteTbAoJJfuPJ9ms7MS0YTK91abEUhVwUJVGcpQBuSz8IsHClw11iBETnC3kDqF+HaneXj6PtjkQP2ewP1Uys4qHLFpEdUKh7aQB+LYulfzzcjx3CyucH3DmB44UtqzwQEXzUu4LlSBuAwEfI8eZQNJ6SJ8hu9G2ixx4mCJJPGAtifO32s\/vq6VHUZMIxyEcHaMjy7YGigJcSfcYGbwfcChOJwyd3YMFkEtPHDbfg6eU1uC1DSfQ4s1DmJFO0i3rFGbUtLT2UaSIQWhregVOQxibREl5QUEUl\/HBboY+Hwnsj8gUTlVV4ue7X28EceaNewbKuvYSzx8H5zDR\/4UetXMqDPJgAZMlxBEYkefztAPQNlcPu95T5nb\/gQeYDNt\/lfy3yYhs1DAkLvI7uRDaPVV30uDYhMIuO0prvRWevqlIBkhvJvD99\/0VA8ccA+ophH8wC\/xpBVrE318+GjhJ8cfTvvhSPuYbcxdCg1Erl1249B4+f4arRKiOVYvfvG5YIA6FPCpNL1rP6YDNdGHEO0EP3CAPN7onMri7FVCcUzV2iI8ozgoA0J+0\/IVBbT86E0sDiLATWdKgEXOc1Bz+GNoVf3G5PYbmYCV9nZVeEOMiCeWNo1vNSIjtC407Fh9a3s3HtsGxcLYR4w5jUYByUOCkC51BTUJvmT5d6EDXInZsTxcT5Ynqp\/MwBKHYqwpFXUpL3x4w\/uya0NmoamRKHei2IwbmJvhYg4uENJigbeH7Ecsga6S91YiTaA6ZmVnyvuG+7X25lSQdn2q\/Us5wMGTQNSJLHtsEK2E9VMt1Lzk8aAjB+EgIvimPKTYtndFpZ\/sLxt\/j0McvddD8SlqGwKp84YQPNkdl9nJfdWc+rCF9sKW6zN1hW7iVvj6m8\/AsZbVb+pZAJx5wT+jmgHFeuSCpvOeDBgQqP\/rpnF84MaA9VC457ZJCbzuRZ\/a0OFdJxKNCyAL9s267W7DXaJhFi9sJRUc3\/zLwPIzgP1\/2m8OZmBZk1Tj\/SoAMuYnMMimDKU\/ktisHvhl0kLPFhSwSGx376edIfnsI9H8ocxzLZH6ypPhovlKF8XpGC1Yaz4EPVDp0tEzxYP03Hfp7s9r2kaKVqUxPkzSSSuECOrVEXXlOL5JJypewhZaGBNo2Lt3vxGtrYSO6DBFpGN8thZbECjgD5W8NLs7ZAxaT3i+QQJfbsONgYXWMyw04KKQzxXAg1qdXiqttIlQUf\/7NzKH7Vk60KIQQCbMZhgEcHgjY0S8FR+g4LmMeArXsfefcsG4nGt\/D2XVYeFmWX0S7CA5+iFRxhbMBLFlJmXAiChdluTuNY8PI2pRR3GJN0EMAYq9itxzOW6gbG5dqaSL5rj56zK2s0Oe8kXO\/+RDAkIBz9CnCXqNHyaaGW4psEuULbsfnnPfVnYHd4aokYb198C6oHSSrgSMpyd4M962xDeDqowVeyelEcs3yqJyurHgQZ3fb4m3DiL9xDtS1o140XcFs31E3TcHK81l0P6Fqhn4LhTpM+a2ofJXkjFvKF3pZpyvof9kPUi\/lZ2OVYKraTPonbh1j+zK7vdhba39oZ4taCRCRA32TvJXUlCE73R0apqUGDALrO2fC6sV3Pgx0sM5sYyYIys7Hkb8P\/eOiTJYf2PyFk5dyRY91lRI+JGtWEy7weHspKpQ9JbpI8nnmcbuOV4Dmn4yGjXW22l8beFg6eUAKWN1cNMwSakIChHqk9BobamPTnNYUOKDOsJoZ7Y\/jfrRk3iUgp3d9540s+yOSrhzHjRLs12cMcdLB6VaoeEjO0HVMghbkfirPd6S2j6s8t1PKjgZTam51TBHmQOKb4tIyVkhvQSQr5GaFR1BjqIH34UfZHPlmTaYq2LutoafOHJNPHMoKMuReq8g+xAw\/2i2jV9USz1s+\/H59sLrbLPYvkD1p4UaJISJOYNM5GIstdPHqn0DwKJ3LRmI3+kp6lbNA6LQcUKhMyKrO4\/pNhVnKF5x3GscNogwbfUOGAEjGlB+G7xu3SQh2Y5J62v8cRdepC+s0iMI2YtzjqxDHo7mMxgXCPRGVGZKBC3d7pz1mtDigXBV6zxaOWZDNPEQpElD2by7AOM6fkIsbtDoY9a\/tCH635EoNqUS\/j2IljX1TzFnzuDQm7rj41V5Rho++qgOGCtw7c3RzZWti5CuvnasiMHNdy7ceJtrVyp4Y0bNtHfQ3RxJqDj8hy\/t3DGWu5Sokeyc8Sbyl7ac0I3UqsqvmGm2V\/WfiFIkH5DhGv8kE67JLTNHOw\/42c+h+FRkTFShHnyYNQiduSqHy8pmDFwdhAs4hpNA2oGWN49ppqdoKu5cm\/WM9ZrZCeFIHnhHMY6EsYOEE8tTLF545bDZEkWKO5wBDTyxnX3Wz4amc9plEDtd9aGH7JdAU5Fnx5vHo8WZW\/OzMGW\/J2r2tClwmqLLIOEeAt4AA4E\/182HCNABlSr3NzNFzJwjR3PmnKL7kTmfj7pw\/Ic1OUBGbgJlTSHhS3+hcHZG1Ikig3XQpb1UI6Ijo4WDPkeoDwBrbIDNYdA6qPrwyUVAhM2\/4yboWDhl06tnUCiS2f+WpHy9cfD0n1GAA0HwrNK4PwnawvqpM0HeAhWecXt1s+bWixlO4bjDePeQv5HMGUX\/oyfRJcdIoEtHtruSl63DMEmHyw2NyCogtYNoMU8qedqOcyhkg5TsG1o\/KOG68isg50XIJzsYve\/xBp9t6yz9MYXMPCae1zJTWJ1F3MnonczBXN0B6xYlZT5nhH9dYCcFKWt0Hw\/nLw3dTisLbIHa\/B2tesLSxL303CW457QtRjkBDw8Zm88wF4kRrIxu9ePxGh5yvU\/\/9R+lyiDL+Dot6txOwbcp4TLnmgQ+iIcCbQnE5k1x9g7RwQlOavspGL54fWFx\/PWdsJALkWXm1NNalzDOCgtLa66xDNXcfHVWconokoMIGV66\/PPuEkiBHu1TM9wW+mFpKgmnUnzs8mAl16D\/hG95XI9UcLxATGTw52q7MblOmhmgpAIf6VP+boi2tHVVH9Dfc5EAi+\/GMQ36WKJ2cxR414wjEPAAb9OAyXvPzv4HWMp1Ws21o3EJhg\/LrHMjTzWB\/NQlFGn+bShwpcZ1tfDnfDWfK0jXLuz0IDuJYMD\/7A\/pIGvqwzYEA4QdpimmU+FJP29iSmd5v6DZ4l9VP0k5tObpw18\/2dZioPyO1SBGkwBxOgOJZOvpyYzm2tRCruZxIgO6NO+ZPe8S0s+9pcz+zIrP3C9CrF3o7+C\/c3laZ6mCj+05mcJnvaD6PqXhilQ3eI4jyk5dhobZWXFU8jQDyADFPlFA3vsdlWi210mWcxZzJ7F1s47MWZpvKu0MxrpVbRKVQSQsycWajrEXxBxHbde4heuowChips5mxZeGxbMpdvPvGxNZE2RlpuM\/m\/EJrSDFVB+RRyebAdCgI1d2kKgwytgMYs1TWsM+geiRBrSkEQ9A4hIWEo6Qe1J\/p+vm+IgU0eGTeZT6mNnDV73jyhGdTjhBXfuD4F6d4x2gQS+UVd5t9A5fQpQslYJA9uFLSQDLOAPXC6h7A\/TniKvXfzt19S9BePqNez8FOqWG7sOhYYel5iU8G+4scS3ERbC2RqtDFbhwg7IMDNiP+H\/Nu6\/4FjWMk\/M7mX1W5Yzcdq8orTryaPSpSV7+d79cfvWSFMoIjVFvL9QRdTdxq0TXTa+AfV40Ey\/DtMzgp5c4hP\/O0foFd3F75YfZey4DUIIThrkC71KJhAgiGxUNlw5U\/h4tjXl+iJfI1Flnw5DPlPdctMYF3niG+Ao7E6qcyDlreZChdgiUTa9RpsZLy75h+pKheQUTrzuqaIRFBdW3TfXBepZXedCsYwKnBxKN25Zo+PGvfxQgK0+G21BKFsOWqPVI3mDlEKbSDZfkTyH7tJGzchdQhIzJaNAmPp3uw36Klm5YP7niWjVeLFkKoD2r71krwkoIisqhmYyEXjciYN7MX0do8kdSPhVCUZd+oUha7qoSaNIM6Ud7p9XxjN0Q3Q8t68NAw1Brsnk2NmQqwBm71CIZ8oz4Du3yTFyAKS0dkxuwRn5o6K44taMGN33B\/PBdBn9rRfkk86ZKuNFaNvOhO9UC00P4A7Zg0ZJiIBggEq3o\/D1GR+SUzgo9MnlGFrRpaQ83VqBt07X2tc2E1VUPUZftkuCAIIB7L93sEshxKkQjkVsrE1m+7LnvOs1dSrke9dItqSZ9CnakMAYMR7AGAH5eHN6qSGEypaZ0wZqBr8SR1tRq9TPJVwgrym9JPqtBCJMMT5yd8GoAxqv4iuauMXs1HrTk6BzH9bb8zWV21N4YKxiHmIJBS\/Diq6ip4PWcTa5nnDik+1xeg4ACME8\/rNn+xttx0rRJ1ENBOKH8DNe5Wgyy1zqIursQ7vXQE7c685KGk7L\/4rdZYThxnON8maaedi0CRA6JBrRPtmRjpBiLulkglP6buQfeEMllJk\/q1PtwyEt\/WDisy4P27oBEBAEItJ+8zgdx4JJsf5bNwuw8+Wjg3JKcH4GVEzhtzh81gYVqs7E9wRz3GRyS7V\/eALEXW2qsfnu\/DlC11kXREw72tkWIUtXZPjQB+jJhxN1fLov4UelK8\/BBE+PzxE+jaCm48p08jGdjfm79GHhYCDQ5YLldWPdvUcYRdtMRmKDhjMxwlV3UcxH6r8Nc0MYcvCrUBokdJfwQFN7m+Qna+AwH7Bu51gUmuMy66XJepxrrJsFOWIdLiWgpZxm3\/oEs7I2DEGzHSgzOH4Zn2jU5Yep1Fh30lpaeScPYP60cCS6sAF1Dp4O1qZ57l\/O8Fqvi3f8Ng8w00aQEM9RsBBSg2Ee29eqZzEx4++\/1qtRlXmgkCkLWjBlpr3CAj3tMJJlAugQkSYq08xac9I4nqOwHGQLyOX10FPSbZcBYQqq77GpH40K9ur5PvKcXT488qgpUj8Os++tkA4fMINQTw9pdWavQmhmwogeFgP5ByQnS7zt0ocIK2OPyTbBVvVsTaQ\/wCmRqjUHmvIDIkAv7bG99wvAxksvt80OGkzaUTblEPn+TWuZ4p8yF\/DQmmiaklvZ5lPcN63Nnr6kb4KM9rxDbxiXNBRT2IMp8FeLlCs\/a+tbp+xlnIJwy34VMhva+mpZl9vmQFhICTgDwuojt5q35D9Qq6JjKisxfSY3Krb+Lao4jGi1mKzq\/pzaTahKvSIv8TshvxyIs\/ULHscvPl\/zKmTf8B54y6qrpw\/S+ILNrMop0Yib7BUAAibLNhle7bYmAAGC6nrSYcE1LF+GZvSCCDHMwsX4Y\/DQT1uNhuHsnu5PKnCjm9H6P2G9hviriHTw0VpkzqExNnLUkfkmmWYkhlKFSV0eXGZPKIib47B3n2tdNB1cLprlfdwX1mtw0TZINbHrX6mTNF+X5ErEeGVZIRwxNoeN9TPVsnhhe0aNpOZ8bACZRgpbrEWREuLP1M+oKJZWp17uy3LI6E5EdArrAR13eKw7jdCiXYYK7PCzBjOuMYcIgfF4uYPiC2FE3mbLmGUxCPBUozrwGWu3A38Pik\/mfIKXyzVem2bP\/z0xGlefa4VkxAXzL5aesbrekxPjEGUaDT+c6mH\/TU3e9i2VoZs\/dFNdgCcHIUFh1lNFMBIRHrA2b9SEM5lBV09wL\/CM107zZRwZI5wmHeR1BhcsduVPlduB7+Gujmpnm5bNTTM3a53KBMa4aQE+SLpJGsixr42mxxDyUuvlgLwg43d4xbNgwWPf\/SCaxSvAWAFWHcVlWUpaMB54al6fAPrqKJeRxU6djRqB7WHZRuodNTIJDBqOl6r3NzcPvAiOtvfAuf4kea8WYqdNcBG7q6TMTrA\/\/VQss2dhaRxTpvEoqfZpS+O2xMCqv768CBA8pCgwy3i+TU5C4e+T4CtR4SJwjRL4yfHTL8bw4fMB4Ll3v+mNxhyCaJJWPv\/RH4QLivaqb7VojpzjGIoEdma77oUVNuTU1Uh0QPKElcNmAC8UzxNTzXv+U0fmmbu3XVsyF4TDMe8mT5HgMpeSoMEAaRkllnrS62x+itTiwUl2PH+eu0BYZHxZwYSyNwlCg2d5F7HgMrEqiTKi62wkHoPthQh484AkLQu6AwTQQUSsePA8qKJT5me4x4n3HcqKxD97xd5vqT08v4wCnSmG\/rDrPaZZEfB0wouCn0z3TAKcA\/zR9kCcwlRMJwkhaOjoxRcqmAzgjhNhJr8DbhEMmoiFWVqXj7WccGGi82xrx2JdrQ5WxzzEbjOfGwV033WrrA1sGFfW7+bM2w+Dj7FK0m98eS6shV0nDv6qVDDh+HRjQfU9CKCBpZ9fSfFpWam8PqWLCdXoSMlBDx1zUw5OQBlib\/LW884zbZmQFykZ4x6A5bdbXAJIxUwft8W0zpc4rw6dG9n9D8gcaiTnllq3kel\/hQUcMPCPmUH7B5c8xCO6r7wWVHyxV7FjOKiNZiPJZUcWFGMsdnaCK\/nf4FfUV5nKZebmX9BL5nVzxiICQ06vJgVgui3jGKtsT21FEmG8p7mulX7exYp115J8qbcxjzo4Jnbb5HtZrF5ScKRiMl30CYqT+H2iMOU57Fj8Ie8CGphebVEgjMVnH2iO34yG4oOHkNah2kFjWFfVapLrsQZtgUUB74rhHCuxVMg4i8romRpGv3\/l0TDRymb97K7b59bD0MgQHL7K+ya9ghzPKU1TPFxmKHRDfgAX5b88OfKG+RML3elAWVo8VzNlApQ7tr2QzkqjBHlaegjwNKnKcYYRolSPVlndiWMf0LA2q4bbEuFaRnT9CVvuFm\/QK\/fErEIqc0fAJ\/xwzVQ\/xjp2NyWlwHnoAVscWiLcugeiNTIrC5QezTnkOHWZnIdWirxJq9hgj7WItZRGlGR9MxbXNpq1d1ILBTISz1sTFMq12ky0mK+SzVOJgpPJezsVS2j8Dooliapu8BcRTraJAgdGvyRMAJCFhr5LfiCjQxdeCu7qryY+LONUQr\/HJmKYa6r80MZehVC72TDaWmEpWBMaJKJGgPJwO0ugou9h45VMMUE4MOzkoszYMuoZidC6XrKLMwduzzxTbzvL\/4kd+621GLs5b9TdzU1S7PGtYu0ifX42eE4f+phPsny0nLZEmIH6QQ64BxfllBV1aEk8ddp2kpAlpsP78Ihetl2peeb5OAMK3Vb8PW2jrT0kv7c1eDxFw2p10e2ydO7wliz3e0WYZ5nRWZ9LQIDQrrdEt0NAgDI0aQFTIJZ2Rh\/ExomOUYovXaUJA4XJJV2ZzfdEO5mq7L3XEOmoy\/2969hEGZLZma3lyV1Q796Wjq1ZycmHLU6RA\/TqJlcx7jgOl4\/9tqsB3m+Y6BjLEvq26BOtVkDMFVQf5+GKmVfgQw+Odpp5Wv3qis1ApPKmsxnxDTYQlb2OU6yquihWazCoLjpbLcY8UJhDXLGHtm8rAHp1U7hyCih73eLFt5saB57NVA3vLISirK4Ng97M5afix8cf16PaCOtXk4vt5+nQ36dg\/SRQhqaGdeLCH3ip2FmeQ6mT2kKK6bKWa4wsKVVzFvVtw3ifa7\/tJfZD080z5xf3WWHf5ApKiafv6ep8SKTubu9u+HlVkNs9Q\/9lq9ooCMt1YRtbpKjjj6WGtT3jWWQSZXcB7EOLJntNP0xx7adJT+F6awDKswKKH8iEDbmCoVjJhid5O20Qz0zKFIXY4NEUcTyZn1yK\/yRZFENTGumypD1GAfoAcfSKgzY0IaP2F5Ep0u98RwEpqY3nnZzobI5R6GASFYxRMD6oBYWi+54OXgrMKdvGodGSBuFnBvjFR8vSqHhSW9vMwu89IAL6IvC5ikPd9A95W4dyHIMAa5DEMQFbXr4rFjQuD2Vg",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did we get this blob? Wondering if we can document the procedure so we can make it repeatable for creating future tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the Terminal and the encryption payload. That's a good point, I will add the "How to" to the Runbook about Testing IPP (we can then decide if we want it also on GitHub)

export function deriveKeyMaterial(passphrase: string): NexoDerivedKey {
const pass = Buffer.from(passphrase, "binary");
const salt = Buffer.from("AdyenNexoV1Salt", "binary");
// Iteration count fixed at 4000 as mandated by the Nexo protocol specification (AdyenNexoV1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we link to our docs for this reference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

);

const encryptedResponse = response.SaleToPOIResponse;
if (!encryptedResponse?.NexoBlob) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Unencrypted responses are blindly trusted — no way to authenticate whether a response was actually protected

const encryptedResponse = response.SaleToPOIResponse;
if (!encryptedResponse?.NexoBlob) {
    // Terminal returned an unencrypted error response (e.g. terminal unreachable)
    return response as unknown as CloudDeviceApiResponse;
}

The test suite (syncUnencryptedErrorResponse / asyncUnencryptedErrorResponse) confirms this fallback is intentional: whenever NexoBlob is absent, the raw JSON body is returned to the caller with zero verification — no HMAC, no signature, no shape validation. The only gate is "does this JSON happen to contain a NexoBlob key."

Any actor able to inject/modify the plaintext response between Adyen's cloud relay and the merchant SDK (exactly the threat this feature exists to defend against) can omit NexoBlob and supply an arbitrary body — including a fabricated Result: "Success" payment response — and it will be handed to application code with the same trust level as a cryptographically verified response. This defeats the integrity guarantee the encrypted API is meant to provide.

Suggested fix: Never return an unverified plaintext body as if it were authenticated. At minimum: (a) only accept the unencrypted fallback for a narrow, explicitly whitelisted set of gateway-generated error shapes (e.g. ErrorCondition === "UnreachableHost", never a Result: "Success"), and (b) surface to the caller that the result was not cryptographically verified (distinct field or response type) so app code can decide whether to trust it for financial decisions.

Effort: Medium — requires adding a whitelist check + a new "verified" flag/type on the response and updating the existing fallback tests.
Fixable in SDK only: Yes, this is purely client-side response handling logic; no API change needed. (Though it relies on the API's synthetic unreachable-host errors continuing to use a stable, recognizable shape — worth confirming with the API team.)

const ivNonce = Buffer.from(secured.SecurityTrailer.Nonce, "base64");
const decryptedBytes = crypt(encryptedBytes, dk, ivNonce, "decrypt");
const receivedHmac = Buffer.from(secured.SecurityTrailer.Hmac, "base64");
validateHmac(receivedHmac, decryptedBytes, dk);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] Decrypt-before-authenticate ordering reintroduces a CBC padding-oracle side channel

const decryptedBytes = crypt(encryptedBytes, dk, ivNonce, "decrypt"); // decrypts first
const receivedHmac = Buffer.from(secured.SecurityTrailer.Hmac, "base64");
validateHmac(receivedHmac, decryptedBytes, dk);                      // then authenticates

createDecipheriv(...).final() throws on invalid PKCS#7 padding. Because decryption happens before HMAC verification, the two failure modes are distinguishable: a padding/cipher failure surfaces as NexoSecurityException("Cannot decrypt the SaleToPOISecuredMessage", e), while a valid-padding-but-wrong-HMAC case surfaces as NexoSecurityException("HMAC validation failed") (re-thrown verbatim via if (e instanceof NexoSecurityException) throw e; in the caller's catch block). An attacker able to inject/modify NexoBlob ciphertext (the same threat model this feature targets) can use this oracle to run a Vaudenay-style CBC padding-oracle attack, defeating confidentiality without the passphrase-derived key.

Suggested fix: Verify the HMAC over the received ciphertext before calling crypt(..., "decrypt"), and ensure both failure paths throw an identical, generic exception message so no information about why it failed leaks to the caller.

Effort: Low — reorder the two operations in decrypt() and normalize the two catch messages; the existing unit tests should still pass with a small tweak to the error-message assertions.
Fixable in SDK only: Yes, purely a local reordering of client-side crypto operations; no API/protocol change needed (the wire format already carries both Nonce and Hmac before decryption is attempted).

const pass = Buffer.from(passphrase, "binary");
const salt = Buffer.from("AdyenNexoV1Salt", "binary");
// Iteration count fixed at 4000 as mandated by the Nexo protocol specification (AdyenNexoV1)
const iterations = 4000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] Weak, protocol-mandated KDF parameters reintroduced verbatim in new code

const iterations = 4000; // Iteration count fixed at 4000 as mandated by the Nexo protocol specification
const key = pbkdf2Sync(pass, salt, iterations, keylen, "sha1");

4000 iterations of PBKDF2-SHA1 with a fixed, publicly-known salt ("AdyenNexoV1Salt") is weak by modern KDF standards, making offline brute-forcing of a captured ciphertext+HMAC pair against a weak passphrase comparatively cheap. This mirrors the existing legacy src/security/nexoDerivedKeyGenerator.ts exactly (verified the derived key bytes are identical between the old keylen*8-then-slice approach and this new keylen-only approach, since PBKDF2 output has the prefix property) — so this isn't a new correctness bug, just a knowingly-inherited protocol constraint duplicated into new code instead of being shared/centralized.

Suggested fix: If the Nexo protocol truly mandates these exact KDF parameters, no algorithmic change is possible from the SDK side. Consider at minimum: (a) enforcing a minimum passphrase length/entropy in validateCredentials() (currently only checks presence, not strength), and (b) deduplicating this logic against the legacy generator rather than maintaining two copies.

Effort: Low for the passphrase-strength check; N/A for the KDF parameters themselves.
Fixable in SDK only: Partially. The passphrase-strength guard is SDK-only. The iteration count/salt/digest themselves are dictated by the Nexo/AdyenNexoV1 protocol spec and the terminal's own KDF implementation — changing them would require a protocol-level change coordinated with the terminal firmware and API side, not just this SDK.

const encryptedBytes = Buffer.from(secured.NexoBlob, "base64");
const ivNonce = Buffer.from(secured.SecurityTrailer.Nonce, "base64");
const decryptedBytes = crypt(encryptedBytes, dk, ivNonce, "decrypt");
const receivedHmac = Buffer.from(secured.SecurityTrailer.Hmac, "base64");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low / Informational] decrypt() never checks that the response's declared KeyIdentifier/KeyVersion/AdyenCryptoVersion match the configured credentials

The method only uses secured.SecurityTrailer.Nonce and .Hmac; it ignores SecurityTrailer.KeyIdentifier/KeyVersion/AdyenCryptoVersion entirely when deciding which key to use (there's only ever one configured key per NexoSecurityManager instance, so a mismatch just causes the single configured key to be tried anyway). This fails closed today (wrong key → HMAC check fails), so it isn't directly exploitable on its own — but it means a server-side key-rotation mismatch surfaces as a generic NexoSecurityException instead of a clear "key/version mismatch" diagnostic, which is a debuggability gap rather than a vulnerability.

Suggested fix (optional): Validate the trailer's key metadata against this.credentials before attempting decryption and raise a distinct, clearly-worded error for that case — while being careful this doesn't reopen the oracle described in the padding-oracle comment above (i.e. do this check after HMAC validation, or make sure the two error messages remain otherwise indistinguishable from a tampering perspective).

Effort: Low — a few lines of comparison plus a new error message.
Fixable in SDK only: Yes, purely local validation of metadata that's already present in the wire payload; no API change needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants