1/*
2 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package java.security;
27
28import java.io.IOException;
29import java.math.BigInteger;
30import java.util.Arrays;
31import java.util.regex.Pattern;
32import sun.security.util.*;
33
34/**
35 * An attribute associated with a PKCS12 keystore entry.
36 * The attribute name is an ASN.1 Object Identifier and the attribute
37 * value is a set of ASN.1 types.
38 *
39 * @since 1.8
40 */
41public final class PKCS12Attribute implements KeyStore.Entry.Attribute {
42
43    private static final Pattern COLON_SEPARATED_HEX_PAIRS =
44        Pattern.compile("^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2})+$");
45    private String name;
46    private String value;
47    private byte[] encoded;
48    private int hashValue = -1;
49
50    /**
51     * Constructs a PKCS12 attribute from its name and value.
52     * The name is an ASN.1 Object Identifier represented as a list of
53     * dot-separated integers.
54     * A string value is represented as the string itself.
55     * A binary value is represented as a string of colon-separated
56     * pairs of hexadecimal digits.
57     * Multi-valued attributes are represented as a comma-separated
58     * list of values, enclosed in square brackets. See
59     * {@link Arrays#toString(java.lang.Object[])}.
60     * <p>
61     * A string value will be DER-encoded as an ASN.1 UTF8String and a
62     * binary value will be DER-encoded as an ASN.1 Octet String.
63     *
64     * @param name the attribute's identifier
65     * @param value the attribute's value
66     *
67     * @exception NullPointerException if {@code name} or {@code value}
68     *     is {@code null}
69     * @exception IllegalArgumentException if {@code name} or
70     *     {@code value} is incorrectly formatted
71     */
72    public PKCS12Attribute(String name, String value) {
73        if (name == null || value == null) {
74            throw new NullPointerException();
75        }
76        // Validate name
77        ObjectIdentifier type;
78        try {
79            type = new ObjectIdentifier(name);
80        } catch (IOException e) {
81            throw new IllegalArgumentException("Incorrect format: name", e);
82        }
83        this.name = name;
84
85        // Validate value
86        int length = value.length();
87        String[] values;
88        if (value.charAt(0) == '[' && value.charAt(length - 1) == ']') {
89            values = value.substring(1, length - 1).split(", ");
90        } else {
91            values = new String[]{ value };
92        }
93        this.value = value;
94
95        try {
96            this.encoded = encode(type, values);
97        } catch (IOException e) {
98            throw new IllegalArgumentException("Incorrect format: value", e);
99        }
100    }
101
102    /**
103     * Constructs a PKCS12 attribute from its ASN.1 DER encoding.
104     * The DER encoding is specified by the following ASN.1 definition:
105     * <pre>
106     *
107     * Attribute ::= SEQUENCE {
108     *     type   AttributeType,
109     *     values SET OF AttributeValue
110     * }
111     * AttributeType ::= OBJECT IDENTIFIER
112     * AttributeValue ::= ANY defined by type
113     *
114     * </pre>
115     *
116     * @param encoded the attribute's ASN.1 DER encoding. It is cloned
117     *     to prevent subsequent modificaion.
118     *
119     * @exception NullPointerException if {@code encoded} is
120     *     {@code null}
121     * @exception IllegalArgumentException if {@code encoded} is
122     *     incorrectly formatted
123     */
124    public PKCS12Attribute(byte[] encoded) {
125        if (encoded == null) {
126            throw new NullPointerException();
127        }
128        this.encoded = encoded.clone();
129
130        try {
131            parse(encoded);
132        } catch (IOException e) {
133            throw new IllegalArgumentException("Incorrect format: encoded", e);
134        }
135    }
136
137    /**
138     * Returns the attribute's ASN.1 Object Identifier represented as a
139     * list of dot-separated integers.
140     *
141     * @return the attribute's identifier
142     */
143    @Override
144    public String getName() {
145        return name;
146    }
147
148    /**
149     * Returns the attribute's ASN.1 DER-encoded value as a string.
150     * An ASN.1 DER-encoded value is returned in one of the following
151     * {@code String} formats:
152     * <ul>
153     * <li> the DER encoding of a basic ASN.1 type that has a natural
154     *      string representation is returned as the string itself.
155     *      Such types are currently limited to BOOLEAN, INTEGER,
156     *      OBJECT IDENTIFIER, UTCTime, GeneralizedTime and the
157     *      following six ASN.1 string types: UTF8String,
158     *      PrintableString, T61String, IA5String, BMPString and
159     *      GeneralString.
160     * <li> the DER encoding of any other ASN.1 type is not decoded but
161     *      returned as a binary string of colon-separated pairs of
162     *      hexadecimal digits.
163     * </ul>
164     * Multi-valued attributes are represented as a comma-separated
165     * list of values, enclosed in square brackets. See
166     * {@link Arrays#toString(java.lang.Object[])}.
167     *
168     * @return the attribute value's string encoding
169     */
170    @Override
171    public String getValue() {
172        return value;
173    }
174
175    /**
176     * Returns the attribute's ASN.1 DER encoding.
177     *
178     * @return a clone of the attribute's DER encoding
179     */
180    public byte[] getEncoded() {
181        return encoded.clone();
182    }
183
184    /**
185     * Compares this {@code PKCS12Attribute} and a specified object for
186     * equality.
187     *
188     * @param obj the comparison object
189     *
190     * @return true if {@code obj} is a {@code PKCS12Attribute} and
191     * their DER encodings are equal.
192     */
193    @Override
194    public boolean equals(Object obj) {
195        if (this == obj) {
196            return true;
197        }
198        if (!(obj instanceof PKCS12Attribute)) {
199            return false;
200        }
201        return Arrays.equals(encoded, ((PKCS12Attribute) obj).getEncoded());
202    }
203
204    /**
205     * Returns the hashcode for this {@code PKCS12Attribute}.
206     * The hash code is computed from its DER encoding.
207     *
208     * @return the hash code
209     */
210    @Override
211    public int hashCode() {
212        if (hashValue == -1) {
213            Arrays.hashCode(encoded);
214        }
215        return hashValue;
216    }
217
218    /**
219     * Returns a string representation of this {@code PKCS12Attribute}.
220     *
221     * @return a name/value pair separated by an 'equals' symbol
222     */
223    @Override
224    public String toString() {
225        return (name + "=" + value);
226    }
227
228    private byte[] encode(ObjectIdentifier type, String[] values)
229            throws IOException {
230        DerOutputStream attribute = new DerOutputStream();
231        attribute.putOID(type);
232        DerOutputStream attrContent = new DerOutputStream();
233        for (String value : values) {
234            if (COLON_SEPARATED_HEX_PAIRS.matcher(value).matches()) {
235                byte[] bytes =
236                    new BigInteger(value.replace(":", ""), 16).toByteArray();
237                if (bytes[0] == 0) {
238                    bytes = Arrays.copyOfRange(bytes, 1, bytes.length);
239                }
240                attrContent.putOctetString(bytes);
241            } else {
242                attrContent.putUTF8String(value);
243            }
244        }
245        attribute.write(DerValue.tag_Set, attrContent);
246        DerOutputStream attributeValue = new DerOutputStream();
247        attributeValue.write(DerValue.tag_Sequence, attribute);
248
249        return attributeValue.toByteArray();
250    }
251
252    private void parse(byte[] encoded) throws IOException {
253        DerInputStream attributeValue = new DerInputStream(encoded);
254        DerValue[] attrSeq = attributeValue.getSequence(2);
255        ObjectIdentifier type = attrSeq[0].getOID();
256        DerInputStream attrContent =
257            new DerInputStream(attrSeq[1].toByteArray());
258        DerValue[] attrValueSet = attrContent.getSet(1);
259        String[] values = new String[attrValueSet.length];
260        String printableString;
261        for (int i = 0; i < attrValueSet.length; i++) {
262            if (attrValueSet[i].tag == DerValue.tag_OctetString) {
263                values[i] = Debug.toString(attrValueSet[i].getOctetString());
264            } else if ((printableString = attrValueSet[i].getAsString())
265                != null) {
266                values[i] = printableString;
267            } else if (attrValueSet[i].tag == DerValue.tag_ObjectId) {
268                values[i] = attrValueSet[i].getOID().toString();
269            } else if (attrValueSet[i].tag == DerValue.tag_GeneralizedTime) {
270                values[i] = attrValueSet[i].getGeneralizedTime().toString();
271            } else if (attrValueSet[i].tag == DerValue.tag_UtcTime) {
272                values[i] = attrValueSet[i].getUTCTime().toString();
273            } else if (attrValueSet[i].tag == DerValue.tag_Integer) {
274                values[i] = attrValueSet[i].getBigInteger().toString();
275            } else if (attrValueSet[i].tag == DerValue.tag_Boolean) {
276                values[i] = String.valueOf(attrValueSet[i].getBoolean());
277            } else {
278                values[i] = Debug.toString(attrValueSet[i].getDataBytes());
279            }
280        }
281
282        this.name = type.toString();
283        this.value = values.length == 1 ? values[0] : Arrays.toString(values);
284    }
285}
286