001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.security;
020
021import org.jpos.util.Loggeable;
022
023import java.io.PrintStream;
024import java.util.*;
025
026/**
027 * Cryptographic Service Message (CSM for short).
028 *
029 * A message for transporting keys or
030 * related information used to control a keying relationship.
031 * It is typically the contents of ISOField(123).
032 * For more information refer to ANSI X9.17: Financial Institution Key Mangement
033 * (Wholesale).
034 * @author Hani S. Kirollos
035 * @version $Revision$ $Date$
036 */
037
038@SuppressWarnings("unchecked")
039public class CryptographicServiceMessage implements Loggeable {
040    Map<String, String> fields = new LinkedHashMap<>();
041    String mcl;
042
043    /** Message Class Label for RSI. */
044    public static final String MCL_RSI = "RSI";
045    /** Message Class Label for KSM (Key Service Message). */
046    public static final String MCL_KSM = "KSM";
047    /** Message Class Label for RSM. */
048    public static final String MCL_RSM = "RSM";
049    /** Message Class Label for ESM. */
050    public static final String MCL_ESM = "ESM";
051
052    /** Tag name for Receiver. */
053    public static final String TAG_RCV = "RCV";
054    /** Tag name for Originator. */
055    public static final String TAG_ORG = "ORG";
056    /** Tag name for Server. */
057    public static final String TAG_SVR = "SVR";
058    /** Tag name for Key Data. */
059    public static final String TAG_KD  = "KD" ;
060    /** Tag name for Clear Text PIN. */
061    public static final String TAG_CTP = "CTP";
062    /** Tag name for Counter. */
063    public static final String TAG_CTR = "CTR";
064    /** Tag name for Error Flag. */
065    public static final String TAG_ERF = "ERF";
066
067    /** Exception thrown when CSM parsing fails. */
068    public static class ParsingException extends Exception {
069
070        private static final long serialVersionUID = 6984718759445061L;
071        /** Default constructor. */
072        public ParsingException() {
073            super();
074        }
075        /**
076         * Constructs a ParsingException with the given message.
077         * @param detail the error message
078         */
079        public ParsingException(String detail) {
080            super(detail);
081        }
082    }
083
084    /** Default constructor. */
085    public CryptographicServiceMessage() {
086    }
087
088    /**
089     * Creates a CSM and sets its Message Class
090     * @param mcl message class name. e.g. MCL_KSM, MCL_RSM...
091     */
092    public CryptographicServiceMessage(String mcl) {
093        setMCL(mcl);
094    }
095
096    /**
097     * Sets the message class label.
098     * @param mcl the message class label (e.g. MCL_KSM, MCL_RSM)
099     */
100    public void setMCL(String mcl) {
101        this.mcl = mcl;
102    }
103
104    /**
105     * Returns the message class label.
106     * @return the message class label
107     */
108    public String getMCL() {
109        return mcl;
110    }
111
112    /**
113     * adds a field to the CSM
114     * @param tag Field Tag
115     * @param content Field Content, can't be null, use an empty string ("") instead
116     * @throws NullPointerException if tag or content is null
117     */
118    public void addField(String tag, String content) {
119        Objects.requireNonNull(tag, "The tag is required");
120        Objects.requireNonNull(content, "The content is required");
121        tag = tag.toUpperCase();
122        fields.put(tag, content);
123    }
124
125    /**
126     * Remove field from CSM
127     * @param tag Field Tag
128     * @throws NullPointerException on null tag
129     */
130    public void removeField (String tag) {
131        Objects.requireNonNull(tag, "The tag is required");
132        fields.remove(tag);
133    }
134
135    /**
136     * Remove fields from CSM
137     * @param tags tag list
138     * @throws NullPointerException on null tag
139     */
140    public void removeFields (String... tags) {
141        for (String tag : tags) {
142            Objects.requireNonNull(tag, "The tag is required");
143            fields.remove(tag);
144        }
145    }
146
147    /**
148     * Returns the field content of a field with the given tag.
149     * @param tag the field tag (case-insensitive)
150     * @return field content, or {@code null} if tag not found
151     */
152    public String getFieldContent(String tag) {
153        return fields.get(tag.toUpperCase());
154    }
155
156
157    /**
158     * Formats the CSM as a string, suitable for transfer.
159     * This is the inverse of parse
160     * @return the CSM in string format
161     */
162    /** {@inheritDoc} */
163    @Override
164    public String toString() {
165        StringBuilder csm = new StringBuilder();
166        csm.append("CSM(MCL/");
167        csm.append(getMCL());
168        csm.append(" ");
169        for (String tag : fields.keySet()) {
170            csm.append(tag);
171            csm.append("/");
172            csm.append(getFieldContent(tag));
173            csm.append(" ");
174        }
175
176        csm.append(")");
177        return csm.toString();
178    }
179
180
181    /**
182     * dumps CSM basic information
183     * @param p a PrintStream usually supplied by Logger
184     * @param indent indention string, usually suppiled by Logger
185     * @see org.jpos.util.Loggeable
186     */
187    @Override
188    /** {@inheritDoc} */
189    public void dump (PrintStream p, String indent) {
190        String inner = indent + "  ";
191        p.print(indent + "<csm");
192        p.print(" class=\"" + getMCL() + "\"");
193        p.println(">");
194        for (String tag : fields.keySet()) {
195            p.println(inner + "<field tag=\"" + tag + "\" value=\"" + getFieldContent(tag) + "\"/>");
196        }
197        p.println(indent + "</csm>");
198    }
199
200    /**
201     * Parses a CSM string into a {@link CryptographicServiceMessage} object.
202     * @param csmString the CSM string to parse
203     * @return the parsed CSM object
204     * @throws ParsingException if the string cannot be parsed
205     */
206    public static CryptographicServiceMessage parse(String csmString) throws ParsingException {
207        CryptographicServiceMessage csm = new CryptographicServiceMessage();
208        StringTokenizer st = new StringTokenizer(csmString, "() \t\n\r\f");
209        if (!st.nextToken().equalsIgnoreCase("CSM"))
210            throw new ParsingException("Invalid CSM, doesn't start with the \"CSM(\" tag: " + csmString);
211        do {
212            String field = st.nextToken();
213            int separatorIndex = field.indexOf('/');
214            if (separatorIndex > 0) {
215                String tag = field.substring(0, separatorIndex).toUpperCase();
216                String content = "";
217                if (separatorIndex < field.length())
218                    content = field.substring(separatorIndex + 1);
219                if (tag.equalsIgnoreCase("MCL"))
220                    csm.setMCL(content);
221                else {
222                    csm.addField(tag, content);
223                }
224            } else
225                throw new ParsingException("Invalid field, doesn't have a tag: " + field);
226        } while (st.hasMoreTokens());
227        if (csm.getMCL() == null)
228            throw new ParsingException("Invalid CSM, doesn't contain an MCL: " + csmString);
229        return csm;
230    }
231
232}