1 /***********************************************************************
4 programmer: Michael J. Palmer <106177.1156@compuserve.com>
7 environment: RedHat 4.0 Linux 2.0.18
8 description: RPA authorisation code for POP3 client
10 The sole entry point is POP3_auth_rpa()
12 For license terms, see the file COPYING in this directory.
14 ***********************************************************************/
18 #if defined(POP3_ENABLE) && defined(RPA_ENABLE)
24 #include <sys/types.h>
27 #include "fetchmail.h"
32 extern unsigned char line1[];
33 extern unsigned char line2[];
34 extern unsigned char line3[];
39 /* prototypes for internal functions */
40 static int POP3_rpa_resp(char* argbuf, int socket );
41 static void LenAppend(char** pptr, int len);
42 static int LenSkip(char** pptr, int rxlen);
43 static int DecBase64(char* bufp);
44 static void EncBase64(char* bufp, int len);
45 static void ToUnicode(char** pptr, char delim, unsigned char* buf, int* plen,
47 static int SetRealmService(char* bufp);
48 static void GenChallenge(unsigned char* buf, int len);
49 static int DigestPassphrase(char* passphrase,
50 unsigned char* rbuf, int unicodeit);
51 static void CompUserResp(void);
52 static int CheckUserAuth(void);
53 static void md5(const void* in, int len, unsigned char* out);
55 /* RPA protocol definitions */
57 #define EARLYVER "\x01\x00" /* Earliest supp version */
58 #define LATEVER "\x03\x00" /* Latest supp version */
59 #define HDR 0x60 /* ASN.1 SEQUENCE */
60 #define MECH "\x06\x09\x60\x86\x48\x01\x86\xF8\x73\x01\x01"
61 #define FLAGS "\x00\x01" /* Mutual authentication */
62 #define STRMAX 128 /* Bytes in Unicode */
63 #define Tsl 14 /* Timestamp bytelen */
64 #define Pul 16 /* Passphrase digest len */
65 #define Cul 16 /* Usr challenge bytelen */
66 #define Rul 16 /* Usr response bytelen */
67 #define Aul 16 /* User auth bytelen */
68 #define Kusl 16 /* Session key bytelen */
70 #define UNIPASS 1 /* 1=Unicode 0=iso8859 */
71 #define PS_RPA 42 /* Return code */
73 /* RPA authentication items */
75 unsigned char Cs[256]; /* Service challenge */
76 int Csl; /* Length of " " */
77 unsigned char Ts[Tsl+1]; /* Timestamp incl \0 */
78 unsigned char Nu[STRMAX]; /* Username in Unicode */
79 int Nul; /* Length of " in bytes */
80 unsigned char Ns[STRMAX]; /* Service in Unicode */
81 int Nsl; /* Length of " in bytes */
82 unsigned char Nr[STRMAX]; /* Realm in Unicode */
83 int Nrl; /* Length of " in bytes */
84 unsigned char Pu[Pul]; /* Passphrase after MD5 */
85 unsigned char Cu[Cul]; /* User challenge */
86 unsigned char Ru[Rul]; /* User response */
87 unsigned char Au[Aul]; /* User auth from Deity */
88 unsigned char Kusu[Kusl]; /* Obscured Session key */
89 unsigned char Kus[Kusl]; /* Session key */
91 /*********************************************************************
92 function: POP3_auth_rpa
93 description: send the AUTH RPA commands to the server, and
94 get the server's response. Then progress through the
95 RPA challenge/response protocol until we are
96 (hopefully) granted authorisation.
98 userid user's id@realm e.g. myuserid@csi.com
99 passphrase user's passphrase
100 (upper lower or mixed case as the realm has chosen.
101 spec allows various options :-( )
102 socket socket to which the server is connected.
104 return value: zero if success, else non-zero.
105 calls: SockPrintf, POP3_rpa_resp, EncBase64, DecBase64,
106 LenAppend, GenChallenge
107 globals: read outlevel.
108 *********************************************************************/
110 int POP3_auth_rpa (char *userid, char *passphrase, int socket)
112 int ok,rxlen,verh,verl,i,rll;
113 char buf [POPBUFSIZE];
115 int status,aulin,kuslin;
116 const char* stdec[4] = { N_("Success") ,
117 N_("Restricted user (something wrong with account)") ,
118 N_("Invalid userid or passphrase") ,
121 /* Initiate RPA authorisation */
123 SockPrintf(socket,"AUTH RPA\r\n");
125 if (outlevel >= O_MONITOR)
126 report(stdout, "> AUTH RPA\n");
128 /* Create unicode user name in Nu. */
129 /* Create MD5 digest of user's passphrase in Pu */
132 ToUnicode(&bufp, '@', Nu, &Nul, 1); /* User (lowercase) */
133 DigestPassphrase(passphrase, Pu, UNIPASS);
135 /* Get + response from server (RPA ready) */
137 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
139 if (outlevel > O_SILENT && outlevel < O_MONITOR)
140 report(stdout, "%s\n",buf);
145 /* Assemble Token 1 in buf */
149 LenAppend(&bufp, 17);
150 memcpy(bufp, MECH, 11); bufp += 11;
151 memcpy(bufp, EARLYVER, 2); bufp += 2;
152 memcpy(bufp, LATEVER, 2); bufp += 2;
153 memcpy(bufp, FLAGS, 2); bufp += 2;
155 /* Send Token 1, receive Token 2 */
157 EncBase64(buf, bufp-buf);
159 SockPrintf(socket,"%s\r\n",buf);
161 if (outlevel >= O_MONITOR)
162 report(stdout, "> %s\n",buf);
163 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
165 if (outlevel > O_SILENT && outlevel < O_MONITOR)
166 report(stdout, "%s\n",buf);
169 if ((rxlen = DecBase64(buf)) == 0)
171 if (outlevel > O_SILENT)
172 report(stderr, GT_("RPA token 2: Base64 decode error\n"));
176 *(buf+rxlen) = 0; /* Terminates realm list */
177 if (LenSkip(&bufp,rxlen) == 0) return(PS_RPA);
179 /* Interpret Token 2 */
181 verh = (unsigned char)*(bufp++); verl = (unsigned char)*(bufp++);
182 if (outlevel >= O_DEBUG)
183 report(stdout, GT_("Service chose RPA version %d.%d\n"),verh,verl);
184 Csl = (unsigned char)*(bufp++);
185 memcpy(Cs, bufp, Csl);
187 if (outlevel >= O_DEBUG)
189 report(stdout, GT_("Service challenge (l=%d):\n"),Csl);
190 for (i=0; i<Csl; i++)
191 report_build(stdout, "%02X ",Cs[i]);
192 report_complete(stdout, "\n");
194 memcpy(Ts, bufp, Tsl);
197 if (outlevel >= O_DEBUG)
198 report(stdout, GT_("Service timestamp %s\n"),Ts);
199 rll = (unsigned char)*(bufp++) << 8; rll = rll | (unsigned char)*(bufp++);
200 if ((bufp-buf+rll) != rxlen)
202 if (outlevel > O_SILENT)
203 report(stderr, GT_("RPA token 2 length error\n"));
206 if (outlevel >= O_DEBUG)
207 report(stdout, GT_("Realm list: %s\n"),bufp);
208 if (SetRealmService(bufp) != 0)
210 if (outlevel > O_SILENT)
211 report(stderr, GT_("RPA error in service@realm string\n"));
215 /* Assemble Token 3 in buf */
219 LenAppend(&bufp, 11+2+strlen(userid)+1+Cul+1+Rul );
220 memcpy(bufp, MECH, 11); bufp += 11;
222 *(bufp++) = strlen(userid);
223 memcpy(bufp,userid,strlen(userid)); bufp += strlen(userid);
224 GenChallenge(Cu,Cul);
226 memcpy(bufp, Cu, Cul); bufp += Cul;
229 memcpy(bufp, Ru, Rul); bufp += Rul;
231 /* Send Token 3, receive Token 4 */
233 EncBase64(buf,bufp-buf);
235 SockPrintf(socket,"%s\r\n",buf);
237 if (outlevel >= O_MONITOR)
238 report(stdout, "> %s\n",buf);
239 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
241 if (outlevel > O_SILENT && outlevel < O_MONITOR)
242 report(stdout, "%s\n",buf);
245 if ((rxlen = DecBase64(buf)) == 0)
247 if (outlevel > O_SILENT)
248 report(stderr, GT_("RPA token 4: Base64 decode error\n"));
252 if (LenSkip(&bufp,rxlen) == 0) return(PS_RPA);
254 /* Interpret Token 4 */
256 aulin = (unsigned char)*(bufp++);
257 if (outlevel >= O_DEBUG)
259 report(stdout, GT_("User authentication (l=%d):\n"),aulin);
260 for (i=0; i<aulin; i++)
261 report_build(stdout, "%02X ",bufp[i]);
262 report_complete(stdout, "\n");
264 if (aulin == Aul) memcpy(Au, bufp, Aul);
267 if (kuslin == Kusl) memcpy(Kusu, bufp, Kusl); /* blinded */
272 if (outlevel >= O_DEBUG)
273 report(stdout, GT_("RPA status: %02X\n"),status);
276 if ((bufp - buf) != rxlen)
278 if (outlevel > O_SILENT)
279 report(stderr, GT_("RPA token 4 length error\n"));
284 if (outlevel > O_SILENT) {
286 report(stderr, GT_("RPA rejects you: %s\n"),GT_(stdec[status]));
288 report(stderr, GT_("RPA rejects you, reason unknown\n"));
296 GT_("RPA User Authentication length error: %d\n"),aulin);
301 report(stderr, GT_("RPA Session key length error: %d\n"),kuslin);
304 if (CheckUserAuth() != 0)
306 if (outlevel > O_SILENT)
307 report(stderr, GT_("RPA _service_ auth fail. Spoof server?\n"));
310 if (outlevel >= O_DEBUG)
312 report(stdout, GT_("Session key established:\n"));
313 for (i=0; i<Kusl; i++)
314 report_build(stdout, "%02X ",Kus[i]);
315 report_complete(stdout, "\n");
318 /* Assemble Token 5 in buf and send (not in ver 2 though) */
319 /* Version 3.0 definitely replies with +OK to this. I have */
320 /* no idea what sort of response previous versions gave. */
326 LenAppend(&bufp, 1 );
328 EncBase64(buf,bufp-buf);
330 SockPrintf(socket,"%s\r\n",buf);
332 if (outlevel >= O_MONITOR)
333 report(stdout, "> %s\n",buf);
334 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
336 if (outlevel > O_SILENT && outlevel < O_MONITOR)
337 report(stdout, "%s\n",buf);
342 if (outlevel > O_SILENT)
343 report(stdout, GT_("RPA authorisation complete\n"));
349 /*********************************************************************
350 function: POP3_rpa_resp
351 description: get the server's response to an RPA action.
352 Return received base64 string if successful
354 argbuf buffer to receive the string.
355 socket socket to which the server is connected.
357 return value: zero if okay, else return code.
359 globals: reads outlevel.
360 *********************************************************************/
362 static int POP3_rpa_resp (char *argbuf, int socket)
365 char buf [POPBUFSIZE];
369 if (outlevel >= O_DEBUG)
370 report(stdout, GT_("Get response\n"));
372 sockrc = gen_recv(socket, buf, sizeof(buf));
375 if (linecount == 1) strcpy(buf,line1);
376 if (linecount == 2) strcpy(buf,line2);
377 if (linecount == 3) strcpy(buf,line3);
378 /* report(stdout, "--> "); fflush(stderr); */
379 /* scanf("%s",&buf) */
382 if (sockrc == PS_SUCCESS) {
387 /* if (*bufp == ' ') bufp++; */
392 else if (strcmp(buf,"-ERR") == 0)
394 else ok = PS_PROTOCOL;
399 if (outlevel >= O_DEBUG)
400 report(stdout, GT_("Get response return %d [%s]\n"), ok, buf);
405 /*********************************************************************
407 description: Store token length encoded as per ASN.1 DER rules
408 buffer pointer stepped on appropriately.
409 Copes with numbers up to 32767 at least.
411 buf pointer to buffer to receive result
412 len length value to encode
417 *********************************************************************/
419 static void LenAppend(char **pptr_, int len)
421 unsigned char **pptr = (unsigned char **)pptr_;
425 **pptr = len; (*pptr)++;
427 else if (len < 0x100)
429 **pptr = 0x81; (*pptr)++;
430 **pptr = len; (*pptr)++;
434 **pptr = 0x82; (*pptr)++;
435 **pptr = len >> 8; (*pptr)++;
436 **pptr = len & 0xFF; (*pptr)++;
440 /*********************************************************************
442 description: Check token header, length, and mechanism, and
445 pptr pointer to buffer pointer
446 rxlen number of bytes after base64 decode
448 return value: 0 if error, else token length value
450 globals: reads outlevel.
451 *********************************************************************/
453 int LenSkip(char **pptr, int rxlen)
458 if ((unsigned char)**pptr != HDR)
460 if (outlevel > O_SILENT)
461 report(stderr, GT_("Hdr not 60\n"));
465 if (((unsigned char)(**pptr) & 0x80) == 0 )
467 len = (unsigned char)**pptr; (*pptr)++;
469 else if ((unsigned char)(**pptr) == 0x81)
471 len = (unsigned char)*(*pptr+1); (*pptr) += 2;
473 else if ((unsigned char)(**pptr) == 0x82)
475 len = ((unsigned char)(*(*pptr+1)) << 8) | (unsigned char)*(*pptr+2);
481 if (outlevel>O_SILENT)
482 report(stderr, GT_("Token length error\n"));
484 else if (((*pptr-save)+len) != rxlen)
486 if (outlevel>O_SILENT)
487 report(stderr, GT_("Token Length %d disagrees with rxlen %d\n"),len,rxlen);
490 else if (memcmp(*pptr,MECH,11))
492 if (outlevel > O_SILENT)
493 report(stderr, GT_("Mechanism field incorrect\n"));
496 else (*pptr) += 11; /* Skip mechanism field */
500 /*********************************************************************
502 description: Decode a Base64 string, overwriting the original.
503 Note that result cannot be longer than input.
508 return value: 0 if error, else number of bytes in decoded result
510 globals: reads outlevel.
511 *********************************************************************/
513 static int DecBase64(char *bufp)
515 unsigned int newx, bits=0, cnt=0, i, part=0;
519 while((ch=(unsigned char)*(inp++)) != 0)
521 if ((ch != '=') && (ch != ' ') && (ch != '\n') && (ch != '\r'))
523 if ((ch>='A') && (ch <= 'Z')) newx = ch - 'A';
524 else if ((ch>='a') && (ch <= 'z')) newx = ch - 'a' + 26;
525 else if ((ch>='0') && (ch <= '9')) newx = ch - '0' + 52;
526 else if ( ch=='+' ) newx = 62;
527 else if ( ch=='/' ) newx = 63;
529 report(stderr, GT_("dec64 error at char %d: %x\n"), (int)(inp - bufp), ch);
532 part=((part & 0x3F)*64) + newx;
537 *outp = (part >> bits);
542 if (outlevel >= O_MONITOR)
544 report(stdout, GT_("Inbound binary data:\n"));
545 for (i=0; i<cnt; i++)
547 report_build(stdout, "%02X ",(unsigned char)bufp[i]);
548 if (((i % 16)==15) || (i==(cnt-1)))
549 report_complete(stdout, "\n");
555 /*********************************************************************
557 description: Encode into Base64 string, overwriting the original.
558 Note that result CAN be longer than input, the buffer
559 is assumed to be big enough. Result string is
564 len number of bytes in buffer (>0)
568 globals: reads outlevel;
569 *********************************************************************/
571 static void EncBase64(char *bufp, int len)
574 unsigned char c1,c2,c3;
575 char x[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
578 if (outlevel >= O_MONITOR)
580 report(stdout, GT_("Outbound data:\n"));
581 for (i=0; i<len; i++)
583 report_build(stdout, "%02X ",(unsigned char)bufp[i]);
584 if (((i % 16)==15) || (i==(len-1)))
585 report_complete(stdout, "\n");
588 outp = bufp + (((len-1)/3)*4);
590 /* So we can do the update in place, start at the far end! */
591 for (i=((len-1)/3)*3; i>=0; i-=3)
593 c1 = (unsigned char)bufp[i];
594 if ((i+1) < len) c2 = (unsigned char)bufp[i+1]; else c2=0;
595 if ((i+2) < len) c3 = (unsigned char)bufp[i+2]; else c3=0;
597 *(outp+1) = x[((c1 & 3)*16) + (c2/16)];
598 if ((i+1) < len) *(outp+2) = x[((c2 & 0x0F)*4) + (c3/64)];
599 else *(outp+2) = '=';
600 if ((i+2) < len) *(outp+3) = x[c3 & 0x3F];
601 else *(outp+3) = '=';
606 /*********************************************************************
608 description: Convert ASCII (or iso-8859-1) byte string into
609 Unicode. Ensure length isn't too long (STRMAX).
612 pptr pointer to input buffer
613 delim delimiter character (in addition to \0)
614 buf buffer where Unicode will go
615 plen pointer to length variable (# bytes output)
616 conv 1 to convert to lowercase, 0 leaves alone
620 globals: reads outlevel;
621 *********************************************************************/
623 static void ToUnicode(char **pptr /* input string*/,
624 char delim, unsigned char *buf /* output buffer */,
630 while ( ((**pptr)!=delim) && ((**pptr)!=0) && ((*plen)<STRMAX) )
634 *(p++) = tolower((unsigned char)**pptr);
640 if ( ((**pptr)!=delim) && ((**pptr)!=0) && ((*plen)==STRMAX) )
642 if (outlevel > O_SILENT)
643 report(stderr, GT_("RPA String too long\n"));
646 if (outlevel >= O_DEBUG)
648 report(stdout, GT_("Unicode:\n"));
649 for (i=0; i<(*plen); i++)
651 report_build(stdout, "%02X ",buf[i]);
652 if (((i % 16)==15) || (i==((*plen)-1)))
653 report_complete(stdout, "\n");
658 /*********************************************************************
659 function: SetRealmService
660 description: Select a realm from list, and store it.
663 bufp pointer to buffer
667 globals: reads outlevel.
669 *********************************************************************/
671 static int SetRealmService(char *bufp)
673 /* For the moment we pick the first available realm. It would */
674 /* make more sense to verify that the realm which the user */
675 /* has given (as part of id) is in the list, and select it's */
676 /* corresponding service name. */
677 ToUnicode(&bufp, '@', Ns, &Nsl, 1); /* Service */
678 bufp++; /* Skip the @ */
679 ToUnicode(&bufp, ' ', Nr, &Nrl, 1); /* Realm name */
680 if ((Nrl == 0) || (Nsl == 0))
685 /*********************************************************************
686 function: GenChallenge
687 description: Generate a random User challenge
690 buf pointer to buffer
695 globals: reads outlevel.
697 *********************************************************************/
699 static void GenChallenge(unsigned char *buf, int len)
704 devrandom = fopen("/dev/urandom","rb");
705 if (devrandom == NULL && outlevel > O_SILENT)
707 report(stdout, GT_("RPA Failed open of /dev/urandom. This shouldn't\n"));
708 report(stdout, GT_(" prevent you logging in, but means you\n"));
709 report(stdout, GT_(" cannot be sure you are talking to the\n"));
710 report(stdout, GT_(" service that you think you are (replay\n"));
711 report(stdout, GT_(" attacks by a dishonest service are possible.)\n"));
715 buf[i] = devrandom ? fgetc(devrandom) : random();
718 fclose(devrandom); /* should be safe, file mode was "r" */
720 if (outlevel >= O_DEBUG)
722 report(stdout, GT_("User challenge:\n"));
723 for (i=0; i<len; i++)
725 report_build(stdout, "%02X ",buf[i]);
726 if (((i % 16)==15) || (i==(len-1)))
727 report_complete(stdout, "\n");
732 /*********************************************************************
733 function: DigestPassphrase
734 description: Use MD5 to compute digest (Pu) of Passphrase
735 Don't map to lower case. We assume the user is
736 aware of the case requirement of the realm.
737 (Why oh why have options in the spec?!)
739 passphrase buffer containing string, \0 terminated
740 rbuf buffer into which digest goes
742 return value: 0 if ok, else error code
744 globals: reads authentication items listed above.
746 *********************************************************************/
748 static int DigestPassphrase(char *passphrase,unsigned char *rbuf,
752 unsigned char workarea[STRMAX];
755 if (unicodeit) /* Option in spec. Yuck. */
758 ToUnicode(&ptr, '\0', workarea, &len, 0); /* No case conv here */
761 md5(workarea,len,rbuf);
764 md5(rbuf,strlen(passphrase),rbuf);
768 /*********************************************************************
769 function: CompUserResp
770 description: Use MD5 to compute User Response (Ru) from
771 Pu Z(48) Nu Ns Nr Cu Cs Ts Pu
777 globals: reads authentication items listed above.
779 *********************************************************************/
781 static void CompUserResp(void)
783 unsigned char workarea[Pul+48+STRMAX*5+Tsl+Pul];
786 memcpy(p , Pu, Pul); p += Pul;
787 memset(p , '\0', 48); p += 48;
788 memcpy(p , Nu, Nul); p += Nul;
789 memcpy(p , Ns, Nsl); p += Nsl;
790 memcpy(p , Nr, Nrl); p += Nrl;
791 memcpy(p , Cu, Cul); p += Cul;
792 memcpy(p , Cs, Csl); p += Csl;
793 memcpy(p , Ts, Tsl); p += Tsl;
794 memcpy(p , Pu, Pul); p += Pul;
795 md5(workarea,p-workarea,Ru);
798 /*********************************************************************
799 function: CheckUserAuth
800 description: Use MD5 to verify Authentication Response to User (Au)
801 using Pu Z(48) Ns Nu Nr Kusu Cs Cu Ts Kus Pu
802 Also creates unobscured session key Kus from obscured
807 return value: 0 if ok, PS_RPA if mismatch
809 globals: reads authentication items listed above.
811 *********************************************************************/
813 static int CheckUserAuth(void)
815 unsigned char workarea[Pul+48+STRMAX*7+Tsl+Pul];
817 unsigned char md5ans[16];
819 /* Create unobscured Kusu */
821 memcpy(p , Pu, Pul); p += Pul;
822 memset(p , '\0', 48); p += 48;
823 memcpy(p , Ns, Nsl); p += Nsl;
824 memcpy(p , Nu, Nul); p += Nul;
825 memcpy(p , Nr, Nrl); p += Nrl;
826 memcpy(p , Cs, Csl); p += Csl;
827 memcpy(p , Cu, Cul); p += Cul;
828 memcpy(p , Ts, Tsl); p += Tsl;
829 memcpy(p , Pu, Pul); p += Pul;
830 md5(workarea,p-workarea,md5ans);
831 for (i=0; i<16; i++) Kus[i] = Kusu[i] ^ md5ans[i];
832 /* Compute Au from our information */
834 memcpy(p , Pu, Pul); p += Pul;
835 memset(p , '\0', 48); p += 48;
836 memcpy(p , Ns, Nsl); p += Nsl;
837 memcpy(p , Nu, Nul); p += Nul;
838 memcpy(p , Nr, Nrl); p += Nrl;
839 memcpy(p , Kusu,Kusl);p += Kusl;
840 memcpy(p , Cs, Csl); p += Csl;
841 memcpy(p , Cu, Cul); p += Cul;
842 memcpy(p , Ts, Tsl); p += Tsl;
843 memcpy(p , Kus, Kusl);p += Kusl;
844 memcpy(p , Pu, Pul); p += Pul;
845 md5(workarea,p-workarea,md5ans);
846 /* Compare the two */
848 if (Au[i] != md5ans[i]) return(PS_RPA);
852 /*********************************************************************
854 description: Apply MD5
858 out 128 bit result buffer
860 calls: MD5 primitives
861 globals: reads outlevel
862 *********************************************************************/
864 static void md5(const void *in_,int len,unsigned char *out)
868 const unsigned char *in = (const unsigned char *)in_;
870 if (outlevel >= O_DEBUG)
872 report(stdout, GT_("MD5 being applied to data block:\n"));
873 for (i=0; i<len; i++)
875 report_build(stdout, "%02X ",in[i]);
876 if (((i % 16)==15) || (i==(len-1)))
877 report_complete(stdout, "\n");
880 MD5Init( &md5context );
881 MD5Update( &md5context, in, len );
882 MD5Final( out, &md5context );
883 if (outlevel >= O_DEBUG)
885 report(stdout, GT_("MD5 result is:\n"));
888 report_build(stdout, "%02X ",out[i]);
890 report_complete(stdout, "\n");
893 #endif /* POP3_ENABLE && RPA_ENABLE */
895 /* rpa.c ends here */