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[];
40 /* prototypes for internal functions */
41 static int POP3_rpa_resp(char* argbuf, int socket );
42 static void LenAppend(char** pptr, int len);
43 static int LenSkip(char** pptr, int rxlen);
44 static int DecBase64(char* bufp);
45 static void EncBase64(char* bufp, int len);
46 static void ToUnicode(char** pptr, char delim, unsigned char* buf, int* plen,
48 static int SetRealmService(char* bufp);
49 static void GenChallenge(unsigned char* buf, int len);
50 static int DigestPassphrase(char* passphrase,
51 unsigned char* rbuf, int unicodeit);
52 static void CompUserResp(void);
53 static int CheckUserAuth(void);
54 static void md5(const void* in, int len, unsigned char* out);
57 /* RPA protocol definitions */
59 #define EARLYVER "\x01\x00" /* Earliest supp version */
60 #define LATEVER "\x03\x00" /* Latest supp version */
61 #define HDR 0x60 /* ASN.1 SEQUENCE */
62 #define MECH "\x06\x09\x60\x86\x48\x01\x86\xF8\x73\x01\x01"
63 #define FLAGS "\x00\x01" /* Mutual authentication */
64 #define STRMAX 128 /* Bytes in Unicode */
65 #define Tsl 14 /* Timestamp bytelen */
66 #define Pul 16 /* Passphrase digest len */
67 #define Cul 16 /* Usr challenge bytelen */
68 #define Rul 16 /* Usr response bytelen */
69 #define Aul 16 /* User auth bytelen */
70 #define Kusl 16 /* Session key bytelen */
72 #define UNIPASS 1 /* 1=Unicode 0=iso8859 */
73 #define PS_RPA 42 /* Return code */
75 /* RPA authentication items */
77 unsigned char Cs[256]; /* Service challenge */
78 int Csl; /* Length of " " */
79 unsigned char Ts[Tsl+1]; /* Timestamp incl \0 */
80 unsigned char Nu[STRMAX]; /* Username in Unicode */
81 int Nul; /* Length of " in bytes */
82 unsigned char Ns[STRMAX]; /* Service in Unicode */
83 int Nsl; /* Length of " in bytes */
84 unsigned char Nr[STRMAX]; /* Realm in Unicode */
85 int Nrl; /* Length of " in bytes */
86 unsigned char Pu[Pul]; /* Passphrase after MD5 */
87 unsigned char Cu[Cul]; /* User challenge */
88 unsigned char Ru[Rul]; /* User response */
89 unsigned char Au[Aul]; /* User auth from Deity */
90 unsigned char Kusu[Kusl]; /* Obscured Session key */
91 unsigned char Kus[Kusl]; /* Session key */
93 /*********************************************************************
94 function: POP3_auth_rpa
95 description: send the AUTH RPA commands to the server, and
96 get the server's response. Then progress through the
97 RPA challenge/response protocol until we are
98 (hopefully) granted authorisation.
100 userid user's id@realm e.g. myuserid@csi.com
101 passphrase user's passphrase
102 (upper lower or mixed case as the realm has chosen.
103 spec allows various options :-( )
104 socket socket to which the server is connected.
106 return value: zero if success, else non-zero.
107 calls: SockPrintf, POP3_rpa_resp, EncBase64, DecBase64,
108 LenAppend, GenChallenge
109 globals: read outlevel.
110 *********************************************************************/
112 int POP3_auth_rpa (char *userid, char *passphrase, int socket)
114 int ok,rxlen,verh,verl,i,rll;
115 char buf [POPBUFSIZE];
117 int status,aulin,kuslin;
118 const char* stdec[4] = { N_("Success") ,
119 N_("Restricted user (something wrong with account)") ,
120 N_("Invalid userid or passphrase") ,
123 /* Initiate RPA authorisation */
125 SockPrintf(socket,"AUTH RPA\r\n");
127 if (outlevel >= O_MONITOR)
128 report(stdout, "> AUTH RPA\n");
130 /* Create unicode user name in Nu. */
131 /* Create MD5 digest of user's passphrase in Pu */
134 ToUnicode(&bufp, '@', Nu, &Nul, 1); /* User (lowercase) */
135 DigestPassphrase(passphrase, Pu, UNIPASS);
137 /* Get + response from server (RPA ready) */
139 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
141 if (outlevel > O_SILENT && outlevel < O_MONITOR)
142 report(stdout, "%s\n",buf);
147 /* Assemble Token 1 in buf */
151 LenAppend(&bufp, 17);
152 memcpy(bufp, MECH, 11); bufp += 11;
153 memcpy(bufp, EARLYVER, 2); bufp += 2;
154 memcpy(bufp, LATEVER, 2); bufp += 2;
155 memcpy(bufp, FLAGS, 2); bufp += 2;
157 /* Send Token 1, receive Token 2 */
159 EncBase64(buf, bufp-buf);
161 SockPrintf(socket,"%s\r\n",buf);
163 if (outlevel >= O_MONITOR)
164 report(stdout, "> %s\n",buf);
165 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
167 if (outlevel > O_SILENT && outlevel < O_MONITOR)
168 report(stdout, "%s\n",buf);
171 if ((rxlen = DecBase64(buf)) == 0)
173 if (outlevel > O_SILENT)
174 report(stderr, GT_("RPA token 2: Base64 decode error\n"));
178 *(buf+rxlen) = 0; /* Terminates realm list */
179 if (LenSkip(&bufp,rxlen) == 0) return(PS_RPA);
181 /* Interpret Token 2 */
183 verh = (unsigned char)*(bufp++); verl = (unsigned char)*(bufp++);
184 if (outlevel >= O_DEBUG)
185 report(stdout, GT_("Service chose RPA version %d.%d\n"),verh,verl);
186 Csl = (unsigned char)*(bufp++);
187 memcpy(Cs, bufp, Csl);
189 if (outlevel >= O_DEBUG)
191 report(stdout, GT_("Service challenge (l=%d):\n"),Csl);
192 for (i=0; i<Csl; i++)
193 report_build(stdout, "%02X ",Cs[i]);
194 report_complete(stdout, "\n");
196 memcpy(Ts, bufp, Tsl);
199 if (outlevel >= O_DEBUG)
200 report(stdout, GT_("Service timestamp %s\n"),Ts);
201 rll = (unsigned char)*(bufp++) << 8; rll = rll | (unsigned char)*(bufp++);
202 if ((bufp-buf+rll) != rxlen)
204 if (outlevel > O_SILENT)
205 report(stderr, GT_("RPA token 2 length error\n"));
208 if (outlevel >= O_DEBUG)
209 report(stdout, GT_("Realm list: %s\n"),bufp);
210 if (SetRealmService(bufp) != 0)
212 if (outlevel > O_SILENT)
213 report(stderr, GT_("RPA error in service@realm string\n"));
217 /* Assemble Token 3 in buf */
221 LenAppend(&bufp, 11+2+strlen(userid)+1+Cul+1+Rul );
222 memcpy(bufp, MECH, 11); bufp += 11;
224 *(bufp++) = strlen(userid);
225 memcpy(bufp,userid,strlen(userid)); bufp += strlen(userid);
226 GenChallenge(Cu,Cul);
228 memcpy(bufp, Cu, Cul); bufp += Cul;
231 memcpy(bufp, Ru, Rul); bufp += Rul;
233 /* Send Token 3, receive Token 4 */
235 EncBase64(buf,bufp-buf);
237 SockPrintf(socket,"%s\r\n",buf);
239 if (outlevel >= O_MONITOR)
240 report(stdout, "> %s\n",buf);
241 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
243 if (outlevel > O_SILENT && outlevel < O_MONITOR)
244 report(stdout, "%s\n",buf);
247 if ((rxlen = DecBase64(buf)) == 0)
249 if (outlevel > O_SILENT)
250 report(stderr, GT_("RPA token 4: Base64 decode error\n"));
254 if (LenSkip(&bufp,rxlen) == 0) return(PS_RPA);
256 /* Interpret Token 4 */
258 aulin = (unsigned char)*(bufp++);
259 if (outlevel >= O_DEBUG)
261 report(stdout, GT_("User authentication (l=%d):\n"),aulin);
262 for (i=0; i<aulin; i++)
263 report_build(stdout, "%02X ",bufp[i]);
264 report_complete(stdout, "\n");
266 if (aulin == Aul) memcpy(Au, bufp, Aul);
269 if (kuslin == Kusl) memcpy(Kusu, bufp, Kusl); /* blinded */
274 if (outlevel >= O_DEBUG)
275 report(stdout, GT_("RPA status: %02X\n"),status);
278 if ((bufp - buf) != rxlen)
280 if (outlevel > O_SILENT)
281 report(stderr, GT_("RPA token 4 length error\n"));
286 if (outlevel > O_SILENT) {
288 report(stderr, GT_("RPA rejects you: %s\n"),GT_(stdec[status]));
290 report(stderr, GT_("RPA rejects you, reason unknown\n"));
298 GT_("RPA User Authentication length error: %d\n"),aulin);
303 report(stderr, GT_("RPA Session key length error: %d\n"),kuslin);
306 if (CheckUserAuth() != 0)
308 if (outlevel > O_SILENT)
309 report(stderr, GT_("RPA _service_ auth fail. Spoof server?\n"));
312 if (outlevel >= O_DEBUG)
314 report(stdout, GT_("Session key established:\n"));
315 for (i=0; i<Kusl; i++)
316 report_build(stdout, "%02X ",Kus[i]);
317 report_complete(stdout, "\n");
320 /* Assemble Token 5 in buf and send (not in ver 2 though) */
321 /* Version 3.0 definitely replies with +OK to this. I have */
322 /* no idea what sort of response previous versions gave. */
328 LenAppend(&bufp, 1 );
330 EncBase64(buf,bufp-buf);
332 SockPrintf(socket,"%s\r\n",buf);
334 if (outlevel >= O_MONITOR)
335 report(stdout, "> %s\n",buf);
336 if ((ok = POP3_rpa_resp(buf,socket)) != 0)
338 if (outlevel > O_SILENT && outlevel < O_MONITOR)
339 report(stdout, "%s\n",buf);
344 if (outlevel > O_SILENT)
345 report(stdout, GT_("RPA authorisation complete\n"));
351 /*********************************************************************
352 function: POP3_rpa_resp
353 description: get the server's response to an RPA action.
354 Return received base64 string if successful
356 argbuf buffer to receive the string.
357 socket socket to which the server is connected.
359 return value: zero if okay, else return code.
361 globals: reads outlevel.
362 *********************************************************************/
364 static int POP3_rpa_resp (char *argbuf, int socket)
367 char buf [POPBUFSIZE];
371 if (outlevel >= O_DEBUG)
372 report(stdout, GT_("Get response\n"));
374 sockrc = gen_recv(socket, buf, sizeof(buf));
377 if (linecount == 1) strcpy(buf,line1);
378 if (linecount == 2) strcpy(buf,line2);
379 if (linecount == 3) strcpy(buf,line3);
380 /* report(stdout, "--> "); fflush(stderr); */
381 /* scanf("%s",&buf) */
384 if (sockrc == PS_SUCCESS) {
389 /* if (*bufp == ' ') bufp++; */
394 else if (strcmp(buf,"-ERR") == 0)
396 else ok = PS_PROTOCOL;
401 if (outlevel >= O_DEBUG)
402 report(stdout, GT_("Get response return %d [%s]\n"), ok, buf);
407 /*********************************************************************
409 description: Store token length encoded as per ASN.1 DER rules
410 buffer pointer stepped on appropriately.
411 Copes with numbers up to 32767 at least.
413 buf pointer to buffer to receive result
414 len length value to encode
419 *********************************************************************/
421 static void LenAppend(char **pptr_, int len)
423 unsigned char **pptr = (unsigned char **)pptr_;
427 **pptr = len; (*pptr)++;
429 else if (len < 0x100)
431 **pptr = 0x81; (*pptr)++;
432 **pptr = len; (*pptr)++;
436 **pptr = 0x82; (*pptr)++;
437 **pptr = len >> 8; (*pptr)++;
438 **pptr = len & 0xFF; (*pptr)++;
442 /*********************************************************************
444 description: Check token header, length, and mechanism, and
447 pptr pointer to buffer pointer
448 rxlen number of bytes after base64 decode
450 return value: 0 if error, else token length value
452 globals: reads outlevel.
453 *********************************************************************/
455 int LenSkip(char **pptr, int rxlen)
460 if ((unsigned char)**pptr != HDR)
462 if (outlevel > O_SILENT)
463 report(stderr, GT_("Hdr not 60\n"));
467 if (((unsigned char)(**pptr) & 0x80) == 0 )
469 len = (unsigned char)**pptr; (*pptr)++;
471 else if ((unsigned char)(**pptr) == 0x81)
473 len = (unsigned char)*(*pptr+1); (*pptr) += 2;
475 else if ((unsigned char)(**pptr) == 0x82)
477 len = ((unsigned char)(*(*pptr+1)) << 8) | (unsigned char)*(*pptr+2);
483 if (outlevel>O_SILENT)
484 report(stderr, GT_("Token length error\n"));
486 else if (((*pptr-save)+len) != rxlen)
488 if (outlevel>O_SILENT)
489 report(stderr, GT_("Token Length %d disagrees with rxlen %d\n"),len,rxlen);
492 else if (memcmp(*pptr,MECH,11))
494 if (outlevel > O_SILENT)
495 report(stderr, GT_("Mechanism field incorrect\n"));
498 else (*pptr) += 11; /* Skip mechanism field */
502 /*********************************************************************
504 description: Decode a Base64 string, overwriting the original.
505 Note that result cannot be longer than input.
510 return value: 0 if error, else number of bytes in decoded result
512 globals: reads outlevel.
513 *********************************************************************/
515 static int DecBase64(char *bufp)
517 unsigned int newx, bits=0, cnt=0, i, part=0;
521 while((ch=(unsigned char)*(inp++)) != 0)
523 if ((ch != '=') && (ch != ' ') && (ch != '\n') && (ch != '\r'))
525 if ((ch>='A') && (ch <= 'Z')) newx = ch - 'A';
526 else if ((ch>='a') && (ch <= 'z')) newx = ch - 'a' + 26;
527 else if ((ch>='0') && (ch <= '9')) newx = ch - '0' + 52;
528 else if ( ch=='+' ) newx = 62;
529 else if ( ch=='/' ) newx = 63;
531 report(stderr, GT_("dec64 error at char %d: %x\n"), (int)(inp - bufp), ch);
534 part=((part & 0x3F)*64) + newx;
539 *outp = (part >> bits);
544 if (outlevel >= O_MONITOR)
546 report(stdout, GT_("Inbound binary data:\n"));
547 for (i=0; i<cnt; i++)
549 report_build(stdout, "%02X ",(unsigned char)bufp[i]);
550 if (((i % 16)==15) || (i==(cnt-1)))
551 report_complete(stdout, "\n");
557 /*********************************************************************
559 description: Encode into Base64 string, overwriting the original.
560 Note that result CAN be longer than input, the buffer
561 is assumed to be big enough. Result string is
566 len number of bytes in buffer (>0)
570 globals: reads outlevel;
571 *********************************************************************/
573 static void EncBase64(char *bufp, int len)
576 unsigned char c1,c2,c3;
577 char x[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
580 if (outlevel >= O_MONITOR)
582 report(stdout, GT_("Outbound data:\n"));
583 for (i=0; i<len; i++)
585 report_build(stdout, "%02X ",(unsigned char)bufp[i]);
586 if (((i % 16)==15) || (i==(len-1)))
587 report_complete(stdout, "\n");
590 outp = bufp + (((len-1)/3)*4);
592 /* So we can do the update in place, start at the far end! */
593 for (i=((len-1)/3)*3; i>=0; i-=3)
595 c1 = (unsigned char)bufp[i];
596 if ((i+1) < len) c2 = (unsigned char)bufp[i+1]; else c2=0;
597 if ((i+2) < len) c3 = (unsigned char)bufp[i+2]; else c3=0;
599 *(outp+1) = x[((c1 & 3)*16) + (c2/16)];
600 if ((i+1) < len) *(outp+2) = x[((c2 & 0x0F)*4) + (c3/64)];
601 else *(outp+2) = '=';
602 if ((i+2) < len) *(outp+3) = x[c3 & 0x3F];
603 else *(outp+3) = '=';
608 /*********************************************************************
610 description: Convert ASCII (or iso-8859-1) byte string into
611 Unicode. Ensure length isn't too long (STRMAX).
614 pptr pointer to input buffer
615 delim delimiter character (in addition to \0)
616 buf buffer where Unicode will go
617 plen pointer to length variable (# bytes output)
618 conv 1 to convert to lowercase, 0 leaves alone
622 globals: reads outlevel;
623 *********************************************************************/
625 static void ToUnicode(char **pptr /* input string*/,
626 char delim, unsigned char *buf /* output buffer */,
632 while ( ((**pptr)!=delim) && ((**pptr)!=0) && ((*plen)<STRMAX) )
636 *(p++) = tolower((unsigned char)**pptr);
642 if ( ((**pptr)!=delim) && ((**pptr)!=0) && ((*plen)==STRMAX) )
644 if (outlevel > O_SILENT)
645 report(stderr, GT_("RPA String too long\n"));
648 if (outlevel >= O_DEBUG)
650 report(stdout, GT_("Unicode:\n"));
651 for (i=0; i<(*plen); i++)
653 report_build(stdout, "%02X ",buf[i]);
654 if (((i % 16)==15) || (i==((*plen)-1)))
655 report_complete(stdout, "\n");
660 /*********************************************************************
661 function: SetRealmService
662 description: Select a realm from list, and store it.
665 bufp pointer to buffer
669 globals: reads outlevel.
671 *********************************************************************/
673 static int SetRealmService(char *bufp)
675 /* For the moment we pick the first available realm. It would */
676 /* make more sense to verify that the realm which the user */
677 /* has given (as part of id) is in the list, and select it's */
678 /* corresponding service name. */
679 ToUnicode(&bufp, '@', Ns, &Nsl, 1); /* Service */
680 bufp++; /* Skip the @ */
681 ToUnicode(&bufp, ' ', Nr, &Nrl, 1); /* Realm name */
682 if ((Nrl == 0) || (Nsl == 0))
687 /*********************************************************************
688 function: GenChallenge
689 description: Generate a random User challenge
692 buf pointer to buffer
697 globals: reads outlevel.
699 *********************************************************************/
701 static void GenChallenge(unsigned char *buf, int len)
706 devrandom = fopen("/dev/urandom","rb");
707 if (devrandom == NULL && outlevel > O_SILENT)
709 report(stdout, GT_("RPA Failed open of /dev/urandom. This shouldn't\n"));
710 report(stdout, GT_(" prevent you logging in, but means you\n"));
711 report(stdout, GT_(" cannot be sure you are talking to the\n"));
712 report(stdout, GT_(" service that you think you are (replay\n"));
713 report(stdout, GT_(" attacks by a dishonest service are possible.)\n"));
717 buf[i] = devrandom ? fgetc(devrandom) : random();
720 fclose(devrandom); /* should be safe, file mode was "r" */
722 if (outlevel >= O_DEBUG)
724 report(stdout, GT_("User challenge:\n"));
725 for (i=0; i<len; i++)
727 report_build(stdout, "%02X ",buf[i]);
728 if (((i % 16)==15) || (i==(len-1)))
729 report_complete(stdout, "\n");
734 /*********************************************************************
735 function: DigestPassphrase
736 description: Use MD5 to compute digest (Pu) of Passphrase
737 Don't map to lower case. We assume the user is
738 aware of the case requirement of the realm.
739 (Why oh why have options in the spec?!)
741 passphrase buffer containing string, \0 terminated
742 rbuf buffer into which digest goes
744 return value: 0 if ok, else error code
746 globals: reads authentication items listed above.
748 *********************************************************************/
750 static int DigestPassphrase(char *passphrase,unsigned char *rbuf,
754 unsigned char workarea[STRMAX];
757 if (unicodeit) /* Option in spec. Yuck. */
760 ToUnicode(&ptr, '\0', workarea, &len, 0); /* No case conv here */
763 md5(workarea,len,rbuf);
766 md5(rbuf,strlen(passphrase),rbuf);
770 /*********************************************************************
771 function: CompUserResp
772 description: Use MD5 to compute User Response (Ru) from
773 Pu Z(48) Nu Ns Nr Cu Cs Ts Pu
779 globals: reads authentication items listed above.
781 *********************************************************************/
783 static void CompUserResp(void)
785 unsigned char workarea[Pul+48+STRMAX*5+Tsl+Pul];
788 memcpy(p , Pu, Pul); p += Pul;
789 memset(p , '\0', 48); p += 48;
790 memcpy(p , Nu, Nul); p += Nul;
791 memcpy(p , Ns, Nsl); p += Nsl;
792 memcpy(p , Nr, Nrl); p += Nrl;
793 memcpy(p , Cu, Cul); p += Cul;
794 memcpy(p , Cs, Csl); p += Csl;
795 memcpy(p , Ts, Tsl); p += Tsl;
796 memcpy(p , Pu, Pul); p += Pul;
797 md5(workarea,p-workarea,Ru);
800 /*********************************************************************
801 function: CheckUserAuth
802 description: Use MD5 to verify Authentication Response to User (Au)
803 using Pu Z(48) Ns Nu Nr Kusu Cs Cu Ts Kus Pu
804 Also creates unobscured session key Kus from obscured
809 return value: 0 if ok, PS_RPA if mismatch
811 globals: reads authentication items listed above.
813 *********************************************************************/
815 static int CheckUserAuth(void)
817 unsigned char workarea[Pul+48+STRMAX*7+Tsl+Pul];
819 unsigned char md5ans[16];
821 /* Create unobscured Kusu */
823 memcpy(p , Pu, Pul); p += Pul;
824 memset(p , '\0', 48); p += 48;
825 memcpy(p , Ns, Nsl); p += Nsl;
826 memcpy(p , Nu, Nul); p += Nul;
827 memcpy(p , Nr, Nrl); p += Nrl;
828 memcpy(p , Cs, Csl); p += Csl;
829 memcpy(p , Cu, Cul); p += Cul;
830 memcpy(p , Ts, Tsl); p += Tsl;
831 memcpy(p , Pu, Pul); p += Pul;
832 md5(workarea,p-workarea,md5ans);
833 for (i=0; i<16; i++) Kus[i] = Kusu[i] ^ md5ans[i];
834 /* Compute Au from our information */
836 memcpy(p , Pu, Pul); p += Pul;
837 memset(p , '\0', 48); p += 48;
838 memcpy(p , Ns, Nsl); p += Nsl;
839 memcpy(p , Nu, Nul); p += Nul;
840 memcpy(p , Nr, Nrl); p += Nrl;
841 memcpy(p , Kusu,Kusl);p += Kusl;
842 memcpy(p , Cs, Csl); p += Csl;
843 memcpy(p , Cu, Cul); p += Cul;
844 memcpy(p , Ts, Tsl); p += Tsl;
845 memcpy(p , Kus, Kusl);p += Kusl;
846 memcpy(p , Pu, Pul); p += Pul;
847 md5(workarea,p-workarea,md5ans);
848 /* Compare the two */
850 if (Au[i] != md5ans[i]) return(PS_RPA);
854 /*********************************************************************
856 description: Apply MD5
860 out 128 bit result buffer
862 calls: MD5 primitives
863 globals: reads outlevel
864 *********************************************************************/
866 static void md5(const void *in_,int len,unsigned char *out)
870 const unsigned char *in = (const unsigned char *)in_;
872 if (outlevel >= O_DEBUG)
874 report(stdout, GT_("MD5 being applied to data block:\n"));
875 for (i=0; i<len; i++)
877 report_build(stdout, "%02X ",in[i]);
878 if (((i % 16)==15) || (i==(len-1)))
879 report_complete(stdout, "\n");
882 MD5Init( &md5context );
883 MD5Update( &md5context, in, len );
884 MD5Final( out, &md5context );
885 if (outlevel >= O_DEBUG)
887 report(stdout, GT_("MD5 result is:\n"));
890 report_build(stdout, "%02X ",out[i]);
892 report_complete(stdout, "\n");
895 #endif /* POP3_ENABLE && RPA_ENABLE */
897 /* rpa.c ends here */