001/* X500Principal.java -- X.500 principal.
002   Copyright (C) 2003, 2004, 2005 Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.security.auth.x500;
040
041import gnu.java.lang.CPStringBuilder;
042
043import gnu.java.security.OID;
044import gnu.java.security.der.DER;
045import gnu.java.security.der.DERReader;
046import gnu.java.security.der.DERValue;
047
048import java.io.ByteArrayInputStream;
049import java.io.EOFException;
050import java.io.IOException;
051import java.io.InputStream;
052import java.io.NotActiveException;
053import java.io.ObjectInputStream;
054import java.io.ObjectOutputStream;
055import java.io.Reader;
056import java.io.Serializable;
057import java.io.StringReader;
058
059import java.security.Principal;
060
061import java.util.ArrayList;
062import java.util.HashSet;
063import java.util.Iterator;
064import java.util.LinkedHashMap;
065import java.util.LinkedList;
066import java.util.List;
067import java.util.Locale;
068import java.util.Map;
069import java.util.Set;
070
071public final class X500Principal implements Principal, Serializable
072{
073  private static final long serialVersionUID = -500463348111345721L;
074
075  // Constants and fields.
076  // ------------------------------------------------------------------------
077
078  public static final String CANONICAL = "CANONICAL";
079  public static final String RFC1779 = "RFC1779";
080  public static final String RFC2253 = "RFC2253";
081
082  private static final OID CN         = new OID("2.5.4.3");
083  private static final OID C          = new OID("2.5.4.6");
084  private static final OID L          = new OID("2.5.4.7");
085  private static final OID ST         = new OID("2.5.4.8");
086  private static final OID STREET     = new OID("2.5.4.9");
087  private static final OID O          = new OID("2.5.4.10");
088  private static final OID OU         = new OID("2.5.4.11");
089  private static final OID DC         = new OID("0.9.2342.19200300.100.1.25");
090  private static final OID UID        = new OID("0.9.2342.19200300.100.1.1");
091
092  private transient List components;
093  private transient Map currentRdn;
094  private transient boolean fixed;
095  private transient byte[] encoded;
096
097  // Constructors.
098  // ------------------------------------------------------------------------
099
100  private X500Principal()
101  {
102    components = new LinkedList();
103    currentRdn = new LinkedHashMap();
104    components.add (currentRdn);
105  }
106
107  public X500Principal (String name)
108  {
109    this();
110    if (name == null)
111      throw new NullPointerException();
112    try
113      {
114        parseString (name);
115      }
116    catch (IOException ioe)
117      {
118        IllegalArgumentException iae = new IllegalArgumentException("malformed name");
119        iae.initCause (ioe);
120        throw iae;
121      }
122  }
123
124  public X500Principal (byte[] encoded)
125  {
126    this(new ByteArrayInputStream (encoded));
127  }
128
129  public X500Principal (InputStream encoded)
130  {
131    this();
132    try
133      {
134        parseDer (encoded);
135      }
136    catch (IOException ioe)
137      {
138        throw new IllegalArgumentException (ioe.toString());
139      }
140  }
141
142  // Instance methods.
143  // ------------------------------------------------------------------------
144
145  public int hashCode()
146  {
147    int result = size();
148    for (int i = 0; i < size(); ++i)
149      {
150        Map m = (Map) components.get(i);
151        for (Iterator it2 = m.entrySet().iterator(); it2.hasNext(); )
152          {
153            Map.Entry e = (Map.Entry) it2.next();
154            // We don't bother looking at the value of the entry.
155            result = result * 31 + ((OID) e.getKey()).hashCode();
156          }
157      }
158    return result;
159  }
160
161  public boolean equals(Object o)
162  {
163    if (!(o instanceof X500Principal))
164      return false;
165    if (size() != ((X500Principal) o).size())
166      return false;
167    for (int i = 0; i < size(); i++)
168      {
169        Map m = (Map) components.get (i);
170        for (Iterator it2 = m.entrySet().iterator(); it2.hasNext(); )
171          {
172            Map.Entry e = (Map.Entry) it2.next();
173            OID oid = (OID) e.getKey();
174            String v1 = (String) e.getValue();
175            String v2 = ((X500Principal) o).getComponent (oid, i);
176            if (v2 == null)
177              return false;
178            if (!compressWS (v1).equalsIgnoreCase (compressWS (v2)))
179              return false;
180          }
181      }
182    return true;
183  }
184
185  public byte[] getEncoded()
186  {
187    if (encoded == null)
188      encodeDer();
189    return (byte[]) encoded.clone();
190  }
191
192  public String getName()
193  {
194    return getName (RFC2253);
195  }
196
197  public String getName (final String format)
198  {
199    boolean rfc2253 = RFC2253.equalsIgnoreCase (format) ||
200      CANONICAL.equalsIgnoreCase (format);
201    boolean rfc1779 = RFC1779.equalsIgnoreCase (format);
202    boolean canon   = CANONICAL.equalsIgnoreCase (format);
203    if (! (rfc2253 || rfc1779 || canon))
204      throw new IllegalArgumentException ("unsupported format " + format);
205    CPStringBuilder str = new CPStringBuilder();
206    for (Iterator it = components.iterator(); it.hasNext(); )
207      {
208        Map m = (Map) it.next();
209        for (Iterator it2 = m.entrySet().iterator(); it2.hasNext(); )
210          {
211            Map.Entry entry = (Map.Entry) it2.next();
212            OID oid = (OID) entry.getKey();
213            String value = (String) entry.getValue();
214            if (oid.equals (CN))
215              str.append ("CN");
216            else if (oid.equals (C))
217              str.append ("C");
218            else if (oid.equals (L))
219              str.append ("L");
220            else if (oid.equals (ST))
221              str.append ("ST");
222            else if (oid.equals (STREET))
223              str.append ("STREET");
224            else if (oid.equals (O))
225              str.append ("O");
226            else if (oid.equals (OU))
227              str.append ("OU");
228            else if (oid.equals (DC) && rfc2253)
229              str.append ("DC");
230            else if (oid.equals (UID) && rfc2253)
231              str.append ("UID");
232            else
233              str.append (oid.toString());
234            str.append('=');
235            str.append(value);
236            if (it2.hasNext())
237              str.append('+');
238          }
239        if (it.hasNext())
240          str.append(',');
241      }
242    if (canon)
243      return str.toString().toUpperCase (Locale.US).toLowerCase (Locale.US);
244    return str.toString();
245  }
246
247  public String toString()
248  {
249    return getName (RFC2253);
250  }
251
252  // Serialization methods.
253  // ------------------------------------------------------------------------
254
255  private void writeObject (ObjectOutputStream out) throws IOException
256  {
257    if (encoded != null)
258      encodeDer();
259    out.writeObject (encoded);
260  }
261
262  private void readObject (ObjectInputStream in)
263    throws IOException, NotActiveException, ClassNotFoundException
264  {
265    byte[] buf = (byte[]) in.readObject();
266    parseDer (new ByteArrayInputStream (buf));
267  }
268
269  // Own methods.
270  // -------------------------------------------------------------------------
271
272  private int size()
273  {
274    return components.size();
275  }
276
277  private String getComponent(OID oid, int rdn)
278  {
279    if (rdn >= size())
280      return null;
281    return (String) ((Map) components.get (rdn)).get (oid);
282  }
283
284  private void encodeDer()
285  {
286    ArrayList name = new ArrayList(components.size());
287    for (Iterator it = components.iterator(); it.hasNext(); )
288      {
289        Map m = (Map) it.next();
290        if (m.isEmpty())
291          continue;
292        Set rdn = new HashSet();
293        for (Iterator it2 = m.entrySet().iterator(); it2.hasNext(); )
294          {
295            Map.Entry e = (Map.Entry) it2.next();
296            ArrayList atav = new ArrayList(2);
297            atav.add(new DERValue(DER.OBJECT_IDENTIFIER, e.getKey()));
298            atav.add(new DERValue(DER.UTF8_STRING, e.getValue()));
299            rdn.add(new DERValue(DER.SEQUENCE|DER.CONSTRUCTED, atav));
300          }
301        name.add(new DERValue(DER.SET|DER.CONSTRUCTED, rdn));
302      }
303    DERValue val = new DERValue(DER.SEQUENCE|DER.CONSTRUCTED, name);
304    encoded = val.getEncoded();
305  }
306
307  private int sep;
308
309  private void parseString(String str) throws IOException
310  {
311    Reader in = new StringReader(str);
312    while (true)
313      {
314        String key = readAttributeType(in);
315        if (key == null)
316          break;
317        String value = readAttributeValue(in);
318        putComponent(key, value);
319        if (sep == ',')
320          newRelativeDistinguishedName();
321        if (sep == -1)
322          break;
323      }
324  }
325
326  private String readAttributeType(Reader in) throws IOException
327  {
328    CPStringBuilder buf = new CPStringBuilder();
329    int ch;
330    while ((ch = in.read()) != '=')
331      {
332        if (ch == -1)
333          {
334            if (buf.length() > 0)
335              throw new EOFException("partial name read: " + buf);
336            return null;
337          }
338        if (ch > 127)
339          throw new IOException("Invalid char: " + (char) ch);
340        if (Character.isLetterOrDigit((char) ch) || ch == '-' || ch == '.')
341          buf.append((char) ch);
342        else
343          throw new IOException("Invalid char: " + (char) ch);
344      }
345    return buf.toString();
346  }
347
348  private String readAttributeValue(Reader in) throws IOException
349  {
350    CPStringBuilder buf = new CPStringBuilder();
351    int ch = in.read();
352    if (ch == '#')
353      {
354        while (true)
355          {
356            ch = in.read();
357            if (('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
358                || Character.isDigit((char) ch))
359              buf.append((char) ch);
360            else if (ch == '+' || ch == ',')
361              {
362                sep = ch;
363                String hex = buf.toString();
364                return new String(toByteArray(hex));
365              }
366            else
367              throw new IOException("illegal character: " + (char) ch);
368          }
369      }
370    else if (ch == '"')
371      {
372        while (true)
373          {
374            ch = in.read();
375            if (ch == '"')
376              break;
377            else if (ch == '\\')
378              {
379                ch = in.read();
380                if (ch == -1)
381                  throw new EOFException();
382                if (('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
383                    || Character.isDigit((char) ch))
384                  {
385                    int i = Character.digit((char) ch, 16) << 4;
386                    ch = in.read();
387                    if (!(('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
388                          || Character.isDigit((char) ch)))
389                      throw new IOException("illegal hex char");
390                    i |= Character.digit((char) ch, 16);
391                    buf.append((char) i);
392                  }
393                else
394                  buf.append((char) ch);
395              }
396            else
397              buf.append((char) ch);
398          }
399        sep = in.read();
400        if (sep != '+' && sep != ',')
401          throw new IOException("illegal character: " + (char) ch);
402        return buf.toString();
403      }
404    else
405      {
406        while (true)
407          {
408            switch (ch)
409              {
410              case '+':
411              case ',':
412                sep = ch;
413                return buf.toString();
414              case '\\':
415                ch = in.read();
416                if (ch == -1)
417                  throw new EOFException();
418                if (('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
419                    || Character.isDigit((char) ch))
420                  {
421                    int i = Character.digit((char) ch, 16) << 4;
422                    ch = in.read();
423                    if (!(('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
424                          || Character.isDigit((char) ch)))
425                      throw new IOException("illegal hex char");
426                    i |= Character.digit((char) ch, 16);
427                    buf.append((char) i);
428                  }
429                else
430                  buf.append((char) ch);
431                break;
432              case '=':
433              case '<':
434              case '>':
435              case '#':
436              case ';':
437                throw new IOException("illegal character: " + (char) ch);
438              case -1:
439                sep = -1;
440                return buf.toString ();
441              default:
442                buf.append((char) ch);
443              }
444            ch = in.read ();
445          }
446      }
447  }
448
449  private void parseDer (InputStream encoded) throws IOException
450  {
451    DERReader der = new DERReader (encoded);
452    DERValue name = der.read();
453    if (!name.isConstructed())
454      throw new IOException ("malformed Name");
455    this.encoded = name.getEncoded();
456    int len = 0;
457    while (len < name.getLength())
458      {
459        DERValue rdn = der.read();
460        if (!rdn.isConstructed())
461          throw new IOException ("badly formed RDNSequence");
462        int len2 = 0;
463        while (len2 < rdn.getLength())
464          {
465            DERValue atav = der.read();
466            if (!atav.isConstructed())
467              throw new IOException ("badly formed AttributeTypeAndValue");
468            DERValue val = der.read();
469            if (val.getTag() != DER.OBJECT_IDENTIFIER)
470              throw new IOException ("badly formed AttributeTypeAndValue");
471            OID oid = (OID) val.getValue();
472            val = der.read();
473            if (!(val.getValue() instanceof String))
474              throw new IOException ("badly formed AttributeTypeAndValue");
475            String value = (String) val.getValue();
476            putComponent(oid, value);
477            len2 += atav.getEncodedLength();
478          }
479        len += rdn.getEncodedLength();
480        if (len < name.getLength())
481          newRelativeDistinguishedName();
482      }
483  }
484
485  private void newRelativeDistinguishedName()
486  {
487    currentRdn = new LinkedHashMap();
488    components.add(currentRdn);
489  }
490
491  private void putComponent(OID oid, String value)
492  {
493    currentRdn.put(oid, value);
494  }
495
496  private void putComponent(String name, String value)
497  {
498    name = name.trim().toLowerCase();
499    if (name.equals("cn"))
500      putComponent(CN, value);
501    else if (name.equals("c"))
502      putComponent(C, value);
503    else if (name.equals("l"))
504      putComponent(L, value);
505    else if (name.equals("street"))
506      putComponent(STREET, value);
507    else if (name.equals("st"))
508      putComponent(ST, value);
509    else if (name.equals ("o"))
510      putComponent (O, value);
511    else if (name.equals ("ou"))
512      putComponent (OU, value);
513    else if (name.equals("dc"))
514      putComponent(DC, value);
515    else if (name.equals("uid"))
516      putComponent(UID, value);
517    else
518      putComponent(new OID(name), value);
519  }
520
521  private static String compressWS(String str)
522  {
523    CPStringBuilder buf = new CPStringBuilder();
524    char lastChar = 0;
525    for (int i = 0; i < str.length(); i++)
526      {
527        char c = str.charAt(i);
528        if (Character.isWhitespace(c))
529          {
530            if (!Character.isWhitespace(lastChar))
531              buf.append(' ');
532          }
533        else
534          buf.append(c);
535        lastChar = c;
536      }
537    return buf.toString().trim();
538  }
539
540  private static byte[] toByteArray (String str)
541  {
542    int limit = str.length();
543    byte[] result = new byte[((limit + 1) / 2)];
544    int i = 0, j = 0;
545    if ((limit % 2) == 1)
546      {
547        result[j++] = (byte) Character.digit (str.charAt(i++), 16);
548      }
549    while (i < limit)
550      {
551        result[j  ]  = (byte) (Character.digit (str.charAt(i++), 16) << 4);
552        result[j++] |= (byte)  Character.digit (str.charAt(i++), 16);
553      }
554    return result;
555  }
556}