aboutsummaryrefslogtreecommitdiff
path: root/src/main/model/x501/Name.java
blob: b00109d1b1ba12bc8dc1bdefc4e2eb5fe1647a71 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package model.x501;

import annotations.Assoc;
import model.asn1.*;
import model.asn1.exceptions.ParseException;
import model.asn1.parsing.BytesReader;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Represents an X.501 directory Name (a.k.a. RDNSequence).
 * <pre>
 *     Name ::= CHOICE { -- only one possibility for now -- rdnSequence RDNSequence }
 *     RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
 *     DistinguishedName ::= RDNSequence
 * </pre>
 */
public class Name extends ASN1Object {
    @Assoc(partOf = true)
    private final RelativeDistinguishedName[] rdnSequence;

    /**
     * EFFECT: Initialize the Name with the given tags and rdnSequence. For tag and parentTag, consult
     * {@link ASN1Object}.
     * REQUIRES: Items should have SET tag.
     */
    public Name(Tag tag, Tag parentTag, RelativeDistinguishedName[] rdnSequence) {
        super(tag, parentTag);
        this.rdnSequence = rdnSequence;
    }

    /**
     * EFFECT: Parse the Name from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
     * Throws {@link ParseException} for invalid input.
     * MODIFIES: this, encoded
     */
    public Name(BytesReader encoded, boolean hasParentTag) throws ParseException {
        super(encoded, hasParentTag);
        final List<RelativeDistinguishedName> list = new ArrayList<>();
        for (int i = 0; i < getLength(); ) {
            int index = encoded.getIndex();
            final RelativeDistinguishedName name = new RelativeDistinguishedName(encoded, false);
            name.getTag().enforce(TAG_SET);
            list.add(name);
            index = encoded.getIndex() - index;
            i += index;
        }
        this.rdnSequence = list.toArray(new RelativeDistinguishedName[0]);
    }

    /**
     * EFFECTS: Parse OID after last KV and clear context if input is '='. Otherwise add to context.
     *          Throws {@link ParseException} if input is '+' or ',', or if the oid cannot be recognized.
     * MODIFIES: context
     */
    private static ObjectIdentifier handleKey(char c, List<Character> context) throws ParseException {
        if (c == '=') {
            if (context.isEmpty()) {
                throw new ParseException("Unterminated key");
            }
            final ObjectIdentifier oid = new ObjectIdentifier(ObjectIdentifier.TAG, null,
                    ObjectIdentifier.getKnown(
                            context.stream().map(Object::toString).collect(Collectors.joining(""))));
            context.clear();
            return oid;
        } else if (c == '+' || c == ',') {
            throw new ParseException("Unterminated key part: " + context);
        } else {
            context.add(c);
            return null;
        }
    }

    /**
     * EFFECTS: Parse KV after '='. Clear context.
     *          Throws {@link ParseException} if context is empty.
     * MODIFIES: context
     * REQUIRES: curKey to be a valid OID
     */
    private static AttributeTypeAndValue flushKV(ObjectIdentifier curKey, List<Character> context)
            throws ParseException {
        if (context.isEmpty()) {
            throw new ParseException("Unterminated value");
        }
        final AttributeTypeAndValue tv = new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, curKey,
                new PrintableString(PrintableString.TAG, null,
                        context.stream().map(Object::toString).collect(Collectors.joining(""))));
        context.clear();
        return tv;
    }

    /**
     * EFFECTS: Handle value after =, optionally flush to rdns after ',', or add to curListKT if after '+'. Clears
     * context if flushed, otherwise add to context. Returns whether switch to the state of reading key.
     *          Throws {@link ParseException} if c is '=' or context is empty.
     * MODIFIES: context, curListKT, rdns
     * REQUIRES: curKey to be a valid OID
     */
    private static boolean handleValue(char c, List<Character> context, List<AttributeTypeAndValue> curListKT,
                                    ObjectIdentifier curKey, List<RelativeDistinguishedName> rdns)
            throws ParseException {
        if (c == ',') {
            if (context.isEmpty()) {
                throw new ParseException("Unterminated value");
            }
            curListKT.add(flushKV(curKey, context));
            rdns.add(new RelativeDistinguishedName(ASN1Object.TAG_SET, null,
                    curListKT.toArray(AttributeTypeAndValue[]::new)));
            curListKT.clear();
            return true;
        } else if (c == '+') {
            curListKT.add(flushKV(curKey, context));
            return true;
        } else if (c == '=') {
            throw new ParseException("Unterminated value part: " + context);
        } else {
            context.add(c);
            return false;
        }
    }

    /**
     * EFFECTS: Parse the given DN string into structural X.509 RDN Sequence.
     *          Character literals = + , must be escaped.
     *          Values will always be PrintableString.
     *          Throws {@link ParseException} if invalid.
     */
    public static Name parseString(String dn) throws ParseException {
        char state = 0; // 0 - Key, 1 - Value; MSB: Escaped
        List<RelativeDistinguishedName> rdns = new ArrayList<>();
        List<AttributeTypeAndValue> curListKT = new ArrayList<>();
        ObjectIdentifier curKey = null;
        List<Character> context = new ArrayList<>();
        for (char c : (dn + ",").toCharArray()) {
            if ((state >> 7) == 1) {
                context.add(c);
                state &= 127;
                continue;
            } else if (c == '\\') {
                state |= 128;
                continue;
            }
            if (state == 0) {
                if ((curKey = handleKey(c, context)) != null) {
                    state = 1;
                }
            } else if (handleValue(c, context, curListKT, curKey, rdns)) {
                state = 0;
            }
        }
        return new Name(ASN1Object.TAG_SEQUENCE, null, rdns.toArray(RelativeDistinguishedName[]::new));
    }

    /**
     * EFFECTS: Encode the SEQUENCE OF into DER, keep order. RDNs will be encoded one-by-one.
     */
    @Override
    public Byte[] encodeValueDER() {
        return Stream.of(rdnSequence)
                .map(Encodable::encodeDER)
                .flatMap(Arrays::stream)
                .toArray(Byte[]::new);
    }

    /**
     * EFFECT: Convert the name into directory string, like CN=yuuta,OU=users,DC=yuuta,DC=moe
     */
    @Override
    public String toString() {
        return Stream.of(rdnSequence)
                .map(RelativeDistinguishedName::toString)
                .collect(Collectors.joining(","));
    }

    public RelativeDistinguishedName[] getRdnSequence() {
        return rdnSequence;
    }
}