Tuesday, September 04, 2007 

Non-linearity, Modeling and Correctness by Design

A fascinating topic in software development is the non-linear relationship between requirements and effort. A slight change in requirements can quickly unravel into a large, unexpected implementation effort. This is well-known among developers, and sadly unfamiliar to anyone who has not spent a significant time in the trenches of software development (including, unfortunately, a disproportionate part of upper management).

Early detection of decision points, where an innocent-looking choice can significantly alter the development schedule (or the probability of getting the software right) is even more fascinating. Even if the final decision rules in favor of the additional complexity (hopefully in exchange for greater benefits), it is important to understand the consequences of our choices.

Let's see a real-world example, although on a small scale.
A few weeks ago I was working on a top secret :-)) application, basically a rich client invoking web services on a remote server.
The users must authenticate before they can use the rich client, and it was decided that they will use their email address as the user id (instantly removing collisions), and a PIN instead of a user-chosen password.
The PIN is assigned by a server-side procedure when the user is registered, and users cannot change their PIN. This simplifies a few administrative issues, and also prevents the widely spread problem of password reuse (see, for instance, "The domino effect of password reuse" by Ives, Walsh, and Schneider, Communications of the ACM, April 2004).
At this point, the login screen could look somewhat like this:



Of course, assuming there is some support on the server side, this is so trivial to implement on the client side that you don't need to design anything. There is just no benefit; you can simply code your way through it. Any half-decent widget library natively supports a "password style" edit box which won't show what you're typing, so we're talking about a few lines of code here.

Now, the problem with a machine-generated PIN (everything has a downside!) is that it's harder to remember than a password chosen by the user. However, if they can't remember the PIN, users will write it down, compromising security again. We could argue that, instead of having people write it down, we could as well have the client computer write it down. The "remember me on this computer" checkmark of fame was then proposed (to be used only on computers considered "safe"):



All right, no need to design anything yet. It's just a checkmark, and when it's on, you save the email and PIN. Just remember to clear the storage if it's turned off. Well, ok, there is some chance to put a bug in here, but it's still quite simple, and most people, I guess, will just add some logic inside the form (or "login component", if they feel fancy about reuse), try a few test cases, and declare success.

Of course, storing a human-readable PIN on the client computer is quite lame. It's not much better than a post-it on the user's desk. Can we make the computer a little more helpful? Sure :-). We can store an encrypted PIN on the client computer. The problem is, the encryption key must be inside the rich-client, and this makes it vulnerable. Indeed, if the client computer can decode the encrypted PIN, it's just slightly harder (even for a naive hacker) to get hold of the decrypted PIN itself. Since the same PIN was used for authentication purposes on a related web site, that was deemed too risky.

Technology often comes to the rescue: we decided to use asymmetric encryption. The public key will be hard-coded inside the rich-client, but only the private key (deployed in our secure server), can decrypt the PIN. Good. That means we have to send the encrypted PIN to the server, whether we get it from the edit box (originally unencrypted) or from the local storage (already encrypted).

Now, this may seem like a tiny change upon storing the unencrypted PIN, especially when public key encryption is already available in standard libraries, so it seems that we just need to add a few function calls, and that would be it.
Well, if you think so, think twice :-), as you should catch the glimpse of a non-linear increase in complexity. Let's see why.

When the program opens the authentication screen it must now check if you have the email and [encrypted] PIN on local storage. If you do, it will display the email and... well, it can't just set the encrypted PIN in the corresponding text box, for several reasons, like:
- the encrypted PIN has a different length than the unencrypted PIN
- you usually get the unencrypted PIN from the textbox and crypt it, and you don't want to encrypt it twice.
So you probably want to set a dummy string there.

Now, the user may (for instance) turn off and back on the "remember me" checkmark, at which point you still have to use the encrypted PIN on file. But if the user changes PIN, while leaving the checkmark on (possibly turning it on and off any number of times), you have to read an unencrypted PIN from the textbox, encrypt it, authenticate, and save the new email + encrypted PIN in a local storage. If the checkmark is just turned off, but it was initially on, you have to use the encrypted PIN on file, but clear the local storage. But not if it was initially off. And so on: there are a quite a few different scenarios. Which leads to the second part of this post: modeling and correctness by design.

When faced with a sudden increase in complexity, you can basically follow three paths:
1) [recommended] Negotiate this complexity away. In this case, we didn't want to, as it provided a significant benefit to the user.
2) [suicidal] Ignore it: just work as you would do if that additional complexity wasn't there. In many cases, that means: just code your way through it.
3) [professional] Deal with it. Make it manageable, remove any accidental complexity (as Brooks would say), leave only the essential complexity on the table, shape and represent your problem in a way that is amenable to reasoning.

Now, I know from experience that, faced with the above, quite a few people would go for (2). Maybe they don't see the complexity surge. Maybe they think code is the only important artifact. Maybe they just don't know any better. But they will just add code inside that form, test a little, find bugs, patch it, find more bugs, and so on. In the end, they will release a faulty program, because they didn't try all the scenarios, and by focusing on single cases they missed the big picture.

I'm really curious about how the TDD guys would approach this problem (more on this later), but I can tell you how I did. There is obviously a state machine behind the problem. So my best shot was to define the precise behavior of that state machine. It's not a trivial on-off machine, so I needed a representation which helps me think. I could use a state/event table, or I could use a diagram. I choose a diagram, for reasons I'll discuss shortly.

Now, I must admit that it took me more than I expected to get it right on paper. I can blame my tool (the CASE tool we use on this project is based on UML 1.4, and we had to tweak it more than a bit to adopt some useful UML 2 modeling concepts) but it was largely a learning process. When I started, I felt like in this specific case superstates were the key to keep the diagram simple, so I sketched a model based on superstates. It didn't look nice (read: obviously correct, easy to understand and to implement).
I started moving some concepts from superstates to concurrent (orthogonal) states. In the end I killed the superstates, paying a small price in increased transitions, but the final model was reasonably simple. At the very least, it shows the real complexity of the problem. Here it is: overall, it took me something like two and a half hours to nail it down in this form.



You may want to think a little about the problem and take a look at the diagram (hey, you might even find a bug :-). You'll see that the model is relatively abstract - it doesn't deal with the GUI side at all, and it shouldn't. There is no attempt to capture keystrokes or mouse clicks. What is important at this abstraction level is that the PIN or email has been changed, or that the checkmark has been turned on or off. Everything else belongs elsewhere (in the GUI components, in the authentication form). Creating a model at the right abstraction level is fundamental, as it allows you to concentrate on the important issues, while at the same time shielding the model itself from irrelevant changes (more on this later).

Part of those 2.5 hours went into shaping the diagram. Shaping is not the same as drawing. In a sense, shaping is to drawing as architecture is to structure. Shaping is an intentional activity, directed at revealing the fundamental nature of what we're modeling. Indeed, the biggest difference between a visual model and a state/transition table is that a table won't give you any opportunity for shaping.

Now, look at that diagram again. Can you see the symmetry in shape and the antisymmetry in meaning/behavior? Can you relate that to the problem we're trying to solve? Do the crossing of transitions 6 and 7 further reinforce your understanding of the strict relationship between antisymmetry and behavior? Do you see how information is lost as you move left-to-right in transitions 6 and 7, so that you cannot get back, while it is preserved in transitions 4-5 and 8-9, so you can always get back? Also, see that anomaly on the bottom left side? The shape is no longer symmetric there. Why? Is that right? Can this be avoided? At what cost?
And so on. There is a lot of reasoning that can be suggested and simplified by the right shape. In the end, when the shape of the model closely resembles the shape of the problem, you can basically see that the model is right.
Remember: code is a model too, with the extremely useful property of being executable. However, you just can't apply the same kind of reasoning to code. It is not the right model to think about the global picture.

Ok, time to move to coding. I must confess I usually don't code simple stuff like this. I would usually leave this to someone else, someone who as been working with me on the model. However, having spent so much time on the model, I wanted to see first-hand if my feeling was right: coding should now take very little time, yield relatively few lines, and no bugs, as it was designed for correctness. So I implemented this thing myself.

Now, this may surprise you, but the problem didn't suggest me any fancy implementation. No State pattern, no table-driven state machine implementation. I used only one class, which I named Credentials (no, AuthenticationManager is not a good name :-), so no class diagram this time.
Internally, Credentials uses three enum types to model the orthogonal states. I simply coded the state-dependent methods with an infamous switch/case.
Why? Quite simple! I do not expect the kind of fine-grained changes that more sophisticated implementations can handle gracefully. The State pattern is about extendibility of states. A table-driven implementation makes event re-routing easy. But I do not foresee any need for this.
My estimate is that this code will stay untouched with a 99% probability. The remaining 1% represents a potential for disruptive change (like, we use some authentication device), at which point, in this case, no sophisticated implementation would shield this code from scrapping. Anyway, it's a very small class, so no big deal (excluding braces, empty lines and a few comments, it took me 50 lines of code, including the hard-coded public key and a call to the encryption library). More on this later.

Being so short, it took very little to implement. I didn't keep track, but I would say less than half an hour, including some initial testing. I still managed to put a bug in it. Well, actually not in the state machine itself, but in the glue code inside the form: I called an event when I should have called another (ok ok I'm pretty dumb). Of course, the bug wasn't subtle, and I caught it immediately . No more bugs. Game over.

Now, take a look at the state machine again. All those numbers in red are not strictly part of UML, but they're extremely useful to define scenarios and test cases. For instance, a scenario where the user starts up the application for the first time, types the email and pin, sets the "remember me" checkmark and authenticates successfully can be simply represented as "0-2-1-9-15-22". If, next time, he turns the checkmark off and then back on, but doesn't change a thing and still authenticates successfully, the scenario would be "0-3-5-4-10-20", and so on.
Which leads us to the next subject: testing.

How many interesting test cases do we have here? Quite a few, and you can quickly derive a list of significant case by "navigating" the diagram. Remember to include cases where the user makes some mistake, fails to authenticate, and then corrects himself (or cancel). That is, something like "0-2-13-14-13-23" to get a short one. If you look carefully, you'll find at least a couple of dozen interesting scenarios, possibly more, depending on how much testing you think is enough.

Now, what is the TDD guy supposed to do? Simple: shun the diagram, think about a scenario, make it fail, make it work. Think about the next scenario, and so on. All code-centered. Chance to get it right and clean? Sorry, I think it's quite small. See my previous posts on the XP episode for further evidence.
Time for another confession:I didn't test all the scenarios myself. We have professional testers on this project. Time ago, I trained them on testing state-based software. They can derive a sensible test plan from the state diagram (plus their domain knowledge and understanding of the problem, of course), execute it, report bugs. Sure, it is my responsibility to give them working code, which I did :-), as they didn't find any problem.
We didn't write automatic tests. It could be done - especially because I have a Credentials class decoupled from GUI code. But again, there would be little value on those automatic tests, for the very same reasons that brought me to choose a simple switch/case implementation. Which leads to the final point: change management.

The common tenets of code-centric practices is that if you really want to sketch a diagram you're free to do it (thanks guys :-), but once you get your code working, you must scrap the diagram, as maintenance costs would raise if you don't. Context-unaware suggestions like this are always short-sighted.
You basically touch your code for one of these 3 reasons:
1) you have a bug to fix
2) design (but not requirements) change - let's simplify this to refactoring.
3) requirements change
Let's consider each one in context, as true agility cannot be achieved by applying cookbook recipes.

Say you have a bug to fix. Some scenario is not behaving as intended. Would you rather:
a) play the faulty scenario on the diagram above, understand if it's truly a defect, understand if it is a design defect (the diagram is wrong) or a coding defect (the diagram is right but the code is not behaving properly), at which point you probably know exactly which transition or state is misbehaving, go there and fix it, without losing sight of the global picture.
b) start your debugger and just sift through the code, fix it locally, and rely on a hopefully large set of test cases to guarantee you haven't broken anything.
Dunno about you, but I'll choose (a) in this case - I've seen way too many "simple" systems like this fail for lack of global understanding.
Note that if you expect bugs, you may want to invest on a testing suite. I didn't, so I didn't.

Say you want to refactor your code. Maybe you decided, for some pervert :-) reason, to adopt the State pattern. Fine. Guess what, I choose to draw only a state (not class) diagram exactly because the structural side was too simple to benefit from modeling. Refactor all you want, the state chart above will stay valid. You could even squeeze a little more value out of it, by using it to reason about the new structure you're going to impose on your software - how will you handle concurrent states, and so on. No need to scrap the diagram.

Say requirements change. Good. The change can be of a rather large magnitude, like you're no longer using a PIN but an authentication device. The diagram is now largely useless - although you might want to take some inspiration as you draw a new one. Truth is, considering the abstraction level of this diagram, if the diagram gets useless, you ain't gonna save a single line of code either. So if you scrap the diagram, you scrap the code as well.
But maybe the change is on a smaller scale, like: "when the checkmark is on, you cannot edit fields. You have to turn it off before you can change the email or PIN" (this might even make sense, to prevent accidental modifications). Again, what would you rather do:
a) understand the impact of the change on the diagram; change it to follow the new requirements. Understand correctness on the diagram, as I suggested above. See which states and transitions need to change. Go to your code and change it. Test again.
b) open your editor, look at your test code, change it to follow new requirements, hope you got enough test cases (and removed the ones no longer valid), start patching the old code, refactor as you go to keep it in a decent shape.
Again, dunno about you, but I'll choose (a) in this case - if you benefit from diagrammatic reasoning when you first think about the system, you'll benefit from diagrammatic reasoning when any significant change arise.

Ok, this is a very long post, so I'll cut it short. You can create software expecting bugs, or expecting correctness. Just be careful about the self-fulfilling expectation you choose :-).

Labels: ,

Comments:
Segnalo che al momento in cui scrivo questa entry di blog è accessibile solo tramite indirizzo diretto (archive/2007_09_01_archive.html) e non è visibile dalla main page di "Carlo Pescio - blog" (blog/blog.html)

Saluti
 
ok, a distanza di pochi minuti è andato tutto a posto

(qualche problema di read-consistency/concurrency del blog engine? ;-)
 
Ciao,
avrei qualche commento sulla scelta del checkbox "Remember me". Come dicono qui a Bologna, brisa par criticher... ma non lo trovo una buona idea.
Problema: l'utente si stufa di scrivere la password mille volte. Ha ragione, ed è giusto aiutarlo.
Soluzione migliore: lasciare che il Sistema si preoccupi di conservare la password.
Soluzione peggiore: il checkbox a cura del client. :-)
Ora Windows non ha un sistema centralizzato di gestione password, e se dovesse averlo, immagino ci siano dei problemi perché non l'ho mai visto in funzione. Quindi temo di non poter fornire una alternativa migliore davvero praticabile, ma tanto per parlare, spiego perché non mi piace il checkbox.

1) Il sistema centralizzato funzionerebbe così: gestisce una sorta di "portapassword" criptato, nel quale l'utente mette le proprie password. L'utente può accedere alle proprie password digitando una unica password (tipicamente quella di login).

2) I programmi che fanno uso di password inoltrano una richiesta al sistema e attendono la risposta, con la quale sono liberi di autenticarsi dove devono.

3) Il vantaggio più immediato è che i programmatori non si preoccupano di checkbox, password e affini perché ci pensa il sistema.

4) Il sistema cresce e aggiunge funzionalità, i programmi ne beneficiano automaticamente. Esempio di funzionalità: l'utente scrive una volta nella sua vita la password, poi dice "remember me", che in realtà vuol dire "non mi va di ricordarla", e immediatamente dimentica la password. Il giorno che ha bisogno di riscriverla (prima o poi capita), che fa? Telefona, manda mail, si incavola perché la password è lì dentro a quel cavolo di campo di autenticazione, ma è illeggibile.
Ci vorrebbe la funzionalità di svelare la password. Cosa che il sistema può fare comodamente, il singolo programma... mah: andrebbe tutta pensata da capo.

5) Nel tuo particolare caso, potresti eliminare dei vincoli che sono stati imposti deliberatamente per rendere il tutto più facile.
Le restrizioni sul tipo di password: ben vengano password complicate e lunghe, non sarebbero un problema per l'utente.
L'abbinamento di una sola password per utente: ora sei costretto ad abbinare un utente a una sola password. Ma se si volessero dare due password, per distinguere diversi diritti di accesso per lo stesso utente? Dovresti rivedere buona parte del tuo design, mentre a livello di sistema sarebbe più facile e del tutto trasparente al programma.

6) Flessibilità nel decidere "quanto" ricordare. A seconda delle applicazioni e del gradi di sicurezza che richiedono, si possono stabilire diversi livelli di automatismo. Ovvero la password viene ricordata dal sistema, ma ogni tanto viene comunque chiesta all'utente (per esempio una volta al giorno o dopo tot giorni di inattività). Questo serve a impedire che l'utente la dimentichi subito, e impedire che un impostore esegua operazioni delicate (che richiederebbero sempre una password).
Il sistema gestirebbe tutto facilmente, un programma... è un bel lavoro, no?

Insomma, povero checkbox, cosa mi ha fatto di male? Poco, ma ha al colpa di non essere la soluzione giusta ad un problema giusto.

Ciao
 
Ciao,
non mi e' chiaro quale attacco dovrebbe contrastare l'utilizzo della crittografia.
Se ammettiamo che un utente possa avere accesso al computer (leggere il PIN in chiaro sul disco o il post-it sul monitor), questo utente potra' nella stessa maniera recuperare il PIN crittato. Se pero' diciamo che "we have to send the encrypted PIN to the server", allora chiunque sara' in possesso del PIN crittato potra' usarlo banalmente per autenticarsi inviandolo al server.
Una soluzione puo' essere quella di salvare il PIN sul disco crittato ma utilizzarlo in chiaro per l'autenticazione. Potremo decrittarlo con una chiave privata (password) che puo' coincidere con quella di login del SO, esattamente come dice Citrullo.
 
[curioso come i commenti talvolta prendano una loro strada secondaria rispetto al post :-))]

Rispondo prima a Patrick, perche' e' piu' semplice :-) e mi da' anche l'innesco per rispondere al citrullo.
"L'attacco" che si contrasta e' quello scritto nel post:
Since the same PIN was used for authentication purposes on a related web site, that was deemed too risky.
Ovviamente, nel sito web ci si autentica col PIN in chiaro, digitandolo.
In altri termini:
- se qualcuno "ruba" il PIN cifrato, tutto cio' che puo' fare e' usare il rich client impersonando l'utente a cui lo ha rubato.Nel contesto concreto, e' un problema piuttosto contenuto, in parte anche diagnosticabile.
- rubando invece il PIN in chiaro potrebbe accedere anche al sito web, dove esistono procedure diciamo "amministrative"
(anticipando una parte di risposta al citrullo, il sito web puo' rispedire il PIN all'email registrata). Il sito web e' usato di rado, l'eventuale fastidio di andarsi a recuperare il PIN in chiaro in una email e' ragionevole.

Arriviamo ad una cosa che avete detto entrambi: usare la password di logon per crittare/decrittare in modo simmetrico il PIN [o come vorrebbe il citrullo, un intero parco di password]. Ora, questa cosa si puo' fare in due modi: ad utilizzo manuale o automatico.

Come programma ad utilizzo manuale esiste da tempo, su cellulari, palmari, servizi web, servizio WAP, ecc ecc. L'evidente scomodita' e' che come utente devo accedere manualmente al repository, scrivere la mia (unica) password, trovare nel repository la password/pin che mi serve per il servizio che voglio usare, copiarla (si spera non su carta :-)) e poi usarla. Talmente scomodo che viene usato di rado.

Da come scrive Patrick immagino invece (ma me lo sto inventando) che avesse in mente un accesso programmatico, dove ad es. il client che ha bisogno del PIN parla col repository e se lo fa dare. Non so bene cosa aveva in mente il citrullo. Ora, ci sono N motivi per cui questa cosa non si deve (e spesso non si puo') fare:

- nessun sistema operativo civile offre un accesso programmatico alla password dell'utente loggato. Una funzione accessibile ad un client qualunque (come quello in questione) attraverso la quale possiamo recuperare la password dell'utente loggato sarebbe una falla di sicurezza enorme.

- supponiamo che non ci sia questa funzione, ma un repository citrullesco R, in cui un programma A puo' depositare un token T, questo viene cifrato (diciamo con la password dell'utente, che e' nota al sistema e che cosi' facendo non viene comunque rivelata) e poi memorizzato come T'.
Lo stesso programma A, in un secondo tempo, puo' richiedere indietro il token T, a questo punto il repository citrullesco R usa la password utente (sperando non sia cambiata nel frattempo :-)) per decrittare simmetricamente T' e restituisce T.
Bellissimo. E il repository come fa a sapere che A ha il diritto di recuperare T? Cosa impedisce a qualunque altro programma di fingersi A e di recuperare T (e quindi, come da ipotesi citrullesca, tutte le nostre password)? Magari A deve dare un altro token di riconoscimento al repository? E questo token dove sta? In chiaro dentro A, cosi' lo vedono tutti? Cifrato simmetricamente dentro A (visto che A deve darlo in chiaro ad R), cosi' spostiamo solo un po' la fatica necessaria, visto che a quel punto la chiave simmetrica dove la mettiamo, in chiaro dentro A?

Detto questo :-), non mi e' chiarissimo dove voleva arrivare il citrullo, anche assumendo che il suo repository fosse teoricamente possibile. Nel senso che se una cosa e' teoricamente possibile ma non c'e', occorre trovare altre soluzioni, e non ho visto proposte :-)).

Invece ne approfitto per rispondere ad alcune osservazioni sempre del citrullo, piu' vicine all'argomento design:

5) Nel tuo particolare caso, potresti eliminare dei vincoli che sono stati imposti deliberatamente per rendere il tutto più facile.
Le restrizioni sul tipo di password: ben vengano password complicate e lunghe, non sarebbero un problema per l'utente.
L'abbinamento di una sola password per utente: ora sei costretto ad abbinare un utente a una sola password. Ma se si volessero dare due password, per distinguere diversi diritti di accesso per lo stesso utente? Dovresti rivedere buona parte del tuo design, mentre a livello di sistema sarebbe più facile e del tutto trasparente al programma.

- Il fatto di usare un PIN e non una password non semplifica nulla (a parte evitare una pagina di "cambia password" nel sito web), evita invece cio' che fa una quantita' enorme di persone, ovvero il riuso della stessa password (o un piccolo set) per tutti i servizi utilizzati, o varianti algoritmiche banali della stessa password. Ovviamente, se la password non dovesse piu' essere ricordata, forse gli utenti userebbero password diverse, ma a quel punto, rimane da dire, che si inventano una password a fare? Diamogli solo dei PIN e via.
- Se voglio usare N password, per fortuna, il mio design non cambia di nulla, ovvero cambia tanto quanto dovrebbe cambiare con un repository centralizzato. Oggi io istanzio un solo oggetto di classe Credentials quando mi serve; ne ho uno solo perche' mi basta una sola credenziale. Se me ne servono due, ne istanzio due diversi e via (potenza dell'information hiding :-)). Anche col repository centralizzato, a meno che questo non sia metamagico, e' il programma che deve decidere di quante credenziali ha bisogno e quando usarle, quindi le modifiche da fare al codice sono esattamente le stesse.

Anzi, tanto per reiterare un concetto: la classe Credentials, che si preoccupa da sola di recuperare le credenziali se esistono, e di mantenerle sul client in qualche modo se desiderato, e' una piccola opzione. Avendo dato un nome (e quindi una forma) ad un concetto come le credenziali, cio' che fa dentro e' facilmente sostituibile.
Cio' non significa che nel codice che sta fuori non ci saranno mai modifiche. In particolare, se il programma necessita di livelli di autenticazione multipli, dovra' in momenti diversi ottenere credenziali diverse. Nessuno puo' farlo al posto suo, perche' nessuno sa quali sono i momenti in cui servono le diverse credenziali...
 
Mah... un po' mi sorprende che non ci siamo intesi subito. Non era una roba così interpretabile.

Per chiarire il "repository citrullesco": ovviamente è un aggeggio automatico, basta leggere qui:
2) I programmi che fanno uso di password inoltrano una richiesta al sistema e attendono la risposta, con la quale sono liberi di autenticarsi dove devono.
Nel caso non fosse abbastanza ovvio:
3) Il vantaggio più immediato è che i programmatori non si preoccupano di checkbox, password e affini perché ci pensa il sistema.
"Ci pensa il sistema" non vuol dire che lo fa a mano l'utente.

Sulla "possibilità teorica" che esista questo mirabolante strumento, sgombro il campo da ogni incertezza citando (potevo farlo prima, ma non volevo parteggiare, dovrò farlo ora, povero me che dispiacere) il Keychain di Mac OS X, che esiste nella forma attuale da 6 anni e che milioni di persone usano felici, dimentichi di tutte le password tranne una. (Nun sai che te perdi ahò!)
Per i dettagli miracolosi riguardo a "cosa impedisce a qualunque altro programma di fingersi A e di recuperare T (e quindi, come da ipotesi citrullesca, tutte le nostre password)" ti rimando al sito developer.apple.com (warning: abbondante presenza di Objective C) che spiega tutto per bene. Brevemente: quando un programma chiede indietro il suo token T, il sistema chiede a sua volta all'utente: può il programma tal dei tali accedere al tale elemento del keychain? E l'utente risponde "sì, solo per stavolta", "sì, sempre", "no", eccetera.

Detto questo :-), di soluzioni alternative non ce ne sono mica tante.
Una che mi piace viene fuori da questi post. Invece che criptare il PIN con una chiave asimmetrica, criptalo simmetricamente con una password scelta dall'utente. L'utente furbino sceglierà una password a lui familiare, che il tuo programma gli chiederà ogni volta che vuole decrittare il PIN. Immagino una volta al giorno o due, non di più.

Riguardo alla possibilità di recupero password in caso di smarrimento, dici che "l'eventuale fastidio di andarsi a recuperare il PIN in chiaro in una email e' ragionevole". Quindi mi viene da credere che in Outlook ogni utente abbia il suo bel PIN in chiaro. Ora, se una manolesta è così brava da ciuffare il PIN criptato, ovvero se ha accesso per un minuto al pc, sarà anche abbastanza sveglia da controllare Outlook, no? Io stabilirei che a una richiesta di recupero password corrisponde l'obbligo di cambiarla.

"Se voglio usare N password, per fortuna, il mio design non cambia di nulla, ovvero cambia tanto quanto dovrebbe cambiare con un repository centralizzato. Oggi io istanzio un solo oggetto di classe Credentials quando mi serve; ne ho uno solo perche' mi basta una sola credenziale."
Non sarei così sicuro: il diagramma che hai postato andrebbe sempre bene? Comunque, rispetto ad usare un sistema centralizzato ci sono delle belle differenze.
Nel "mio" caso il programma conterrebbe una chiamata al sistema per ogni volta che ha bisogno di una password, del tipo getpwd(IN token, OUT pwd).
Il token indica per esempio il livello di autenticazione: USER oppure ADMIN, che ne so. Basta, non serve altro.

Dai non fare un checkbox "Remember me", è troppo anni '90! ;-)
 
Citrullo, appena vedi apple ti vengono gli occhi a forma di cuore?? Immaginavo fosse la classica deviazione da mac :-).

Non e' che mi piaccia particolarmente l'idea di scaricare tutto sulle spalle dell'utente.
Puo' il programma X accedere all'elemento Y del keychain? E che ne so? :-).
Sembra un po' come "posso far partire un activex [a caso]?" E che ne so?

Da come lo descrivi, sembra pure che tu stia glissando su un enorme problema di security (anche se richiede un accesso fisico temporaneo).
Diciamo che in 10 minuti mi faccio un programmino che inizia a chiedere tutti i token di cui sono a conoscenza. Se ho accesso per 1 min al tuo PC, lo lancio e mi recupero e salvo tutti i tuoi token, senza batter ciglio, perche' io alla domanda del sistema e' ovvio che rispondo si...
Mi vengono in mente anche altre varianti che non richiedono violazioni della sicurezza fisica, ma in effetti non sono andato a leggermi la documentazione apple, e quindi potrebbero esserci misure precauzionali ulteriori.

Il diagramma, proprio perche' e' comportamentale e non strutturale, va benissimo cosi' come e'.
Dove tu scrivi:
getpwd(IN token, OUT pwd).
io scrivo
Credentials c = new Credentials( role );
Tutto il diagramma descrive il comportamento di una singola istanza di Credentials. Lo spiegavo anche :-).

La tua soluzione alternativa (senza keychain) fa scrivere la password all'utente ogni volta. A quel punto tanto vale che sia la password di accesso al servizio... (capisco le differenze del caso).

Comunque guarda, quando sul sistema operativo target diventera' uno standard l'uso di tecniche alternative, magari uno standard non troppo bucherellato, sicuramente abbandonero' la soluzione anni 90 :-). Nel frattempo mi sa che il checkmark rimane, ma grazie ugualmente :-).
 
"Citrullo, appena vedi apple ti vengono gli occhi a forma di cuore??"
Decisamente. Anzi diciamo quando vedo roba fatta bene, di fianco a roba fatta... maluccio. :-)

"Puo' il programma X accedere all'elemento Y del keychain? E che ne so?"
Se il programma ce l'hai messo tu e sai perche' vuole accedere, si', altrimenti no. Lo sai se il tuo client puo' accedere alla sua password?
"Sembra un po' come "posso far partire un activex [a caso]?" E che ne so?"
Infatti questa e' una mostruosita', perche' l'activex non ce l'ha messo l'utente e non ha idea di cosa voglia fare. Ci sono chili di articoli su internet a proposito.

"Diciamo che in 10 minuti mi faccio un programmino che inizia a chiedere tutti i token di cui sono a conoscenza. Se ho accesso per 1 min al tuo PC, lo lancio e mi recupero e salvo tutti i tuoi token, senza batter ciglio, perche' io alla domanda del sistema e' ovvio che rispondo si"
Impossibile: ogni eseguibile che accede al keychain deve far parte di una ACL, cosa che naturalmente richiede l'inserimento manuale della password dell'utente. E ogni volta che un eseguibile viene modificato, di nuovo. (Questo presuppone una serie di cose a livello piu' basso presenti solo in un sistema operativo civile).

Qui c'e' la documentazione, leggi che magari ti piace pure... ;-)
http://developer.apple.com/documentation
/Security/Conceptual
/keychainServConcepts/index.html#/
/apple_ref/doc/uid/TP30000897

"Comunque guarda, quando sul sistema operativo target diventera' uno standard l'uso di tecniche alternative, magari uno standard non troppo bucherellato"
Mai.

"Nel frattempo mi sa che il checkmark rimane, ma grazie ugualmente"
Prego, e' sempre un piacere! ;-)
 
Ho letto la documentazione ed in effetti e' piuttosto carino, diciamo che se fosse disponibile su Windows sicuramente userei questo e non il checkmark, dato che come facevo notare la mia e' una applicazione dove non ci sono grossi requisiti di sicurezza. Personalmente, come utente e come progettista, non ci terrei cose importanti, tipo i codici per un sistema di pagamento.

La cosa peculiare e' che e' proprio una tecnologia anni 80, altro che 90 :-), con i suoi concetti di ACL, di Trusted Application, e soprattutto di utente che capisce tutto questo, come suggerisci anche tu:

Se il programma ce l'hai messo tu e sai perche' vuole accedere, si', altrimenti no. Lo sai se il tuo client puo' accedere alla sua password?


In realta' una applicazione puo' tentare di accedere a cio' che vuole, che siano dati propri o meno (se una applicazione potesse accedere solo ai propri dati, sarebbe anche discutibile il fatto che l'utente debba approvare).
Per cui, quando l'utente di vede chiedere (parafrasando la figura 1.4 della documentazione) "CMail wants permission to use the "cMailData" item from your keycahin. Do you want to allow this?", sarebbe tutto da capire come fa l'utente a sapere che cMailData e' un dato a cui CMail puo' legittimamente accedere, piuttosto che il dato privato di una applicazione dal nome simile (magari per phishing).
Dovremmo anche chiederci quanti utenti ci farebbero caso, ecc ecc.

A dire il vero, peraltro, mi pare ci sia di peggio (non usando il mac e' solo un sospetto che mi viene guardando le figure).
Un meccanismo come il keychain porta gli utenti ad aspettarsi di ricevere piu' o meno frequentemente dal sistema le fatidiche domande di unlock keychain (vedi sempre figura 1.4).
Ora, sai che farei io se volessi attaccare il sistema? Non cercherei di romperne la crittografia [vedi oltre per un parallelo con gli anni 80] ma creerei un bellissimo cavallo di troia. Un programma utile, che un bel po' di utenti si installeranno.
Ad un certo punto, come parte della logica del programma, farei saltare fuori un dialog box del tutto identico al "Confirm Access to KeyChain", fingendo che il mio programma voglia accedere a dati evidentemente del tutto leciti (trasformando quel bel senso di sicurezza che mostravi nel tuo commento in una debolezza).
Un bel po' di utenti accetteranno senza andare a controllare nel programma di amministrazione del keychain (che mi chiedo quanti utenti normali useranno), proprio perche' e' ovvio che voglio accedere a dati a cui ho diritto di accedere. Peccato che siccome il dialog box e' mio, la password la scriveranno a me. Da qui in avanti e' tutta discesa. (magari occorre lavorare un po' intorno all'idea per renderla operativa, ma please, non fatelo :-).

Giusto per divertimento, ho provato a scrivere in google Cracking Apple Keychain. Il primo link (http://svenontech.com/tag/Crack) punta ad un tool molto divertente:
http://www.subrosasoft.com/OSXSoftware/index.php?main_page=product_info&cPath=200&products_id=195
chissa' se funziona :-). Gli altri parlano di crittografia simmetrica triple-DES ecc, proprio roba da anni 80. Ricordo che in quegli anni molti sistemi unix criptavano le password in triple-des (ottenendo pero' un hash non invertibile, mentre il keychain deve necessariamente poterle invertire). Il file delle password (cifrate) era accessibile pubblicamente in /etc/passwd, e molti tentavano i classici attacchi a dizionario (che ho visto citati anche per il keychain in alcuni risultati della ricerca fatta con google).

Io ed un collega di universita', invece, avevamo creato un banale script csh di poche righe, che riproponeva semplicemente la schermata di login, salvava user id / pwd, si loggava come quell'utente, e via. Nessuno si accorgeva di niente, ma noi avevamo la password (non ne abbiamo mai abusato :-). Probabilmente potrei usarlo ancora oggi su qualche "sicurissimo" box unix con schermata di login a carattere :-). Penso si noti facilmente la totale simmetria con il dialog box di cui sopra.

Curiosamente, e' stato proprio quello sfigato di Windows a portare su un sistema operativo di massa il concetto di fare ctrl-alt-del per il login; il ctrl-alt-del viene intercettato dal sistema e serviva appunto per evitare che qualcuno facesse un programmetto simile al nostro script csh.

Domanda: secondo te, un sistema che regge bene in un sistema operativo di nicchia in cui gli utenti hanno a disposizione 3 programmi :-)) ben conosciuti, adorano il loro giocattolo e non provano mai a romperlo, reggerebbe anche se lo stesso sistema fosse usato da centinaia di milioni di utenti noncuranti, ognuno dei quali installa innumerevoli applicazioni, ed e' costantemente sotto l'attacco di una banda di unni senza scrupoli? (Non e' strettamente necessario rispondere :-)).
 
Beh, Carlo che fa il cracker non me lo sarei mai aspettato... chissà che ci facevate con quelle password... :D
 
Super-brevemente: ho tenuto dei codici di pagamento, e la mia preoccupazione che qualcuno li potesse leggere era infinitamente inferiore a quella che la banca mi rubasse i soldi. :-)

È anni 80, e infatti funziona. :-) Se fosse anni 70 sarebbe un'illusione, anni 2000 sarebbe una fregatura. Di solito è così no? :-)

C'è modo di controllare che la finestra sia davvero quella di sistema. Comunque è vero che se uno è distratto o molto superficiale, gli si può fregare la password.
Ma il tuo ragionamento è un po' debole: presuppone che un programma molto diffuso compia azioni malevole. Non esiste, ti beccano subito... A un programma famoso sono concessi solo i bug.

In Unix c'è la combinazione Control-C per evitare che un programma impostore faccio quel trucco, più altri accorgimenti che rendono il trucco molto difficile da mettere in pratica. Comunque è sempre vero che un utente distratto ecc.

Quanto regge un sistema di nicchia? Lo si sa solo se diventa di massa... se no sono solo supposizioni vaganti.
Al momento è un sistema con decine di milioni di utenti che tengono nel portachiavi dati interessanti, ma nessuno li ha mai rubati. Sono noti zero virus (e zero antivirus), zero trojan, zero tutto dal 2001, anno in cui è uscito. Cosa che non si può dire per esempio di Vista, uscito da pochissimo.
Se fosse solo un problema di numeri, come mai i cellulari Symbian sono stati vittima dei virus fin da subito? Il virus si diffondeva via bluetooth da un Symbian all'altro, e di Symbian ce n'erano ben pochi in giro.
E come mai prima di Mac OS X c'erano i virus anche per Mac, e poi sono spariti? Gli utenti non sono mica diminuiti, anzi, e ora sono sempre collegati a internet.
(Semiserio) Ma supponiamo che la questione non sia solo di bontà del sistema, supponiamo che all'aumentare del numero di utenti aumentino anche i malvagi che provano e fare virus (ammesso che ci riescano). Reggerebbe lo stesso?
Tu in pratica dici che un sistema regge se gli utenti lo adorano. Ti sei già risposto... Il problema di Windows è che non si fa adorare, mentre Mac e Linux sì! :-)
 
Ma il tuo ragionamento è un po' debole: presuppone che un programma molto diffuso compia azioni malevole. Non esiste, ti beccano subito... A un programma famoso sono concessi solo i bug.
Certo il mindset della nicchia che ha a disposizione 3 programmi si vede lontano un miglio :-). Ho detto utile e che si diffonde, non famoso. Certo per voi e' uguale :-)).

Ma tu credi sinceramente che ogni utente unix faccia ctrl-c prima di loggarsi, che ogni utente mac si metta con cura a verificare se la finestra e' realmente di sistema (come si fa?) prima di approvare il keychain, e che i pochi restanti siano distratti e/o superficiali? E che se gli utenti fossero 100 volte tanto non ci sarebbe una massa critica sufficiente a giustificare il tentativo di spennarli? Credi realmente che se anche un sistema fosse "tecnicamente sicuro" non si potrebbero fregare gli utenti agendo su altri fattori (umani)? Credi davvero che proprio questo senso di sicurezza non possa diventare una debolezza? Suvvia... capisco il giocare al "ti si indica la luna e guardi il dito" per piacere di polemica, ma insomma...

Sul resto non credo ci sia molto da dire, ognuno ha il diritto di avere i propri sogni ed i propri miti...
Non ho invece ben capito se a parer tuo dovrebbe seccarmi se Windows non si fa adorare, non sono mica azionista Microsoft :-).

Mi piacerebbe invece, avendo vissuto i tempi del bellissimo Apple ][ e della comunita' inclusiva che si era creata al suo contorno, che chi adora "l'azienda Apple" di oggi provasse a fare delle ipotesi concrete (magari senza umorismo superficiale) su come sarebbe il mondo se ci fosse stato solo il Mac e la politica di chiusura ed esclusione totale praticata dall'Apple post-1982 [o giu' di li', le date mi sfuggono sempre :-)], anziche' un PC di cui veniva distribuito da IBM anche lo schema elettrico e l'intero contenuto della ROM disassemblato e commentato (che ho ancora). Ma non ci provo neppure, perche' come mi hai piu' volte dimostrato, quando ci sono di mezzo gli occhi a cuoricino, la ragione vacilla, e chissa' cosa salta fuori :-).

Ti lascio volentieri l'ultima replica (senza sottintesi scortesi, giuro :-). Non credo che questo thread risulti molto interessante per chi legge e preferisco passare a qualcosa di piu' costruttivo; saro' piu' cauto in futuro quando tira aria di mac ;-)
 
Post a Comment

<< Home