Writing secure code is hard. Even in traditional client-server applications, it's difficult to defend against the vast array of possible threats and attacks. Security considerations stretch across every area of programming, from design to deployment, and include everything from hiding sensitive information to restricting the abilities of different classes of users. Code that runs smoothly, requires user credentials, and uses encryption can still be filled with exploitable security holes. Most often, the professional developers who created it won't have any idea of the risk until these weaknesses are exploited.
In peer-to-peer programming, security considerations are multiplied. Communication is usually over the public Internet, but peers communicate with a wide array of different devices that are often anonymous, and there may not be any central authority for authenticating users. However, a little work can go a long way toward improving the security of any system. This chapter won't tell you how to make a bulletproof security infrastructure—for that you need highly complex protocols such as Kerberos and Secure Sockets Layer (SSL), which can't be easily applied to a peer-to-peer system. However, this chapter shows the security fundamentals that you need to prevent casual hacking, data tampering, and eavesdropping. In other words, if you apply the fundamentals in this chapter, you can change a wide-open application into one that requires significant effort to breach—and that's a worthwhile change.
Part of the challenge of security is that it's an immense field that covers everything from the way users jot down passwords and lock server-room doors to advanced cryptography. This chapter focuses on two basic types of security issues:
Authentication and authorization. How do you verify that a user is who he or she claims to be and how do you grant or restrict application privileges based on this identity?
Encryption and cryptography. How can you ensure that sensitive data cannot be read by an eavesdropper or tampered with by an attacker?
This certainly isn't all there is to security in a peer-to-peer application. For example, you've already seen in Chapter 6 how you can use .NET code-access security to restrict the permissions you give to dynamically executed code. In addition, you might need to make decisions about how you enforce nonrepudiation, which is how transactional systems ensure that user actions are nonreversible, even if compromised (generally using a combination of logging techniques). You also might want to create an incident response plan for dealing with security problems as well as an auditing system that logs user behavior and alerts users or administrators if a suspicious pattern of behavior emerges.
The security discussion in this chapter takes the peer-to-peer perspective. The security issues with distributed applications are inherently more complex than in stand-alone applications, and the security considerations for peer-to-peer systems are some of the most complex of all. Unfortunately, the source of peer-to-peer flexibility—loosely defined networks and a lack of server control—can also be the source of endless security headaches.
Some of the challenges in secure peer-to-peer programming include the following:
How can two peers validate each other's identity if they don't have access to a centralized user database or any authentication information?
Once two peers validate each other's identity, how do they make trust decisions to determine what interactions are safe?
As messages are sent over the network, how can a peer be certain that they aren't being tampered with?
How can a peer hide sensitive data so a hacker can't sniff it out as it travels over the Internet?
What happens if a malicious user tries to impersonate another user or computer? What happens if a hacker tries to capture the network packets you use for authentication and interaction, and use them later?
This chapter looks at all these considerations, but it won't directly deal with one of the most important details—trying to limit the damage of an attack by coding defensively. Secure programming isn't just about authentication and cryptography; it's also about making sensible coding choices and using basic validation and error-handling logic to close security holes. For example, a file-sharing application should check that it can't be tricked into returning or overwriting a system file. It should also include failsafes that allow it to stop writing a file if the hard drive is out of space or the size of the file seems grossly out of proportion. (For example, if you attempt to download a song and the end of the file still hasn't been reached after 100 MB.)
These common-sense measures can prevent serious security problems. For best results, review your code frequently with other programmers. Spend time in the design, testing, and review stage looking exclusively for security flaws. Take the perspective of a hacker trying to decide what features could be exploited to gain privileged access, steal data, or even just cripple the computer by wasting its CPU or hard-drive resources (a common and often overlooked tactic known as a denial of service attack). Unfortunately, you can't find the security problems in a piece of code until they are exploited—and it's far better for you to exploit them in the testing phase than for a hacker to discover them in a real-world environment.
In enterprise development, the best security choice is to rely on third-party security services whenever possible. For example, if you use integrated Windows authentication and SSL encryption, you gain a relatively well-protected system without needing to write a single line of code. Unfortunately, in peer-to-peer applications, your environment probably won't support these features. For example, Windows authentication won't work in its most secure forms between networks. SSL can't be accessed outside of the Internet Information Server (IIS), unless you want to deal with extremely complicated low-level Windows API code.
Fortunately, .NET provides a reasonable alternative: the rich set of classes in the System.Security.Cryptography namespace. These classes allow your code to manually perform various cryptography tasks such as encrypting and decrypting data, signing messages, and so on. However, these features come at a price. Typically, you'll find that the more cryptography code you write, the more tightly your solution becomes bound to a particular platform and implementation. You'll also need to manage a slew of additional details, such as keys, block sizes, .NET-to-binary data-type conversions, and so on. Lastly, although the System.Security.Cryptography namespace contains robust, professional-level classes, it's easy to use these classes incorrectly. In other words, by writing your own cryptography code you increase the chances of leaving security holes. That doesn't mean that it's better to avoid custom cryptography altogether, but it does mean that you should have your cryptography code reviewed by a security expert before a mission-critical application is deployed.