001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.openid.connect.sdk.id;
019
020
021import java.util.AbstractMap;
022import java.util.Map;
023import javax.crypto.SecretKey;
024import javax.crypto.spec.SecretKeySpec;
025
026import com.nimbusds.jose.util.Base64URL;
027import com.nimbusds.jose.util.ByteUtils;
028import com.nimbusds.oauth2.sdk.id.Subject;
029import net.jcip.annotations.ThreadSafe;
030import org.cryptomator.siv.SivMode;
031
032
033/**
034 * SIV AES - based encoder / decoder of pairwise subject identifiers. Requires
035 * a 256, 384, or 512-bit secret key. Reversal is supported.
036 *
037 * <p>The plain text is formatted as follows ('|' as delimiter):
038 *
039 * <pre>
040 * sector_id|local_sub
041 * </pre>
042 *
043 * <p>The encoder can be configured to pad the local subject up to a certain
044 * string length, typically the maximum expected length of the local subject
045 * identifiers, to ensure the output pairwise subject identifiers are output
046 * with a length that is uniform and doesn't vary with the local subject
047 * identifier length. This is intended as an additional measure against leaking
048 * end-user information and hence correlation. Note that local subjects that
049 * are longer than the configured length will appear as proportionally longer
050 * pairwise identifiers.
051 *
052 * <p>Pad local subjects that are shorter than 50 characters in length:
053 *
054 * <pre>
055 * new SIVAESBasedPairwiseSubjectCodec(secretKey, 50);
056 * </pre>
057 *
058 * <p>Related specifications:
059 *
060 * <ul>
061 *     <li>Synthetic Initialization Vector (SIV) Authenticated Encryption Using
062 *         the Advanced Encryption Standard (AES) (RFC 5297)
063 *     <li>OpenID Connect Core 1.0
064 * </ul>
065 */
066@ThreadSafe
067public class SIVAESBasedPairwiseSubjectCodec extends PairwiseSubjectCodec {
068        
069        
070        /**
071         * The AES SIV crypto engine.
072         */
073        private static final SivMode AES_SIV = new SivMode();
074        
075        
076        /**
077         * The AES CTR key (1st half).
078         */
079        private final byte[] aesCtrKey;
080        
081        
082        /**
083         * The MAC key (2nd half).
084         */
085        private final byte[] macKey;
086        
087        
088        /**
089         * Pads the local subject to the specified length, -1 for no padding.
090         */
091        private final int padSubjectToLength;
092        
093        
094        /**
095         * Creates a new SIV AES - based codec for pairwise subject
096         * identifiers. Local subjects are not padded up to a certain length.
097         *
098         * @param secretKey A 256, 384, or 512-bit secret key. Must not be
099         *                  {@code null}.
100         */
101        public SIVAESBasedPairwiseSubjectCodec(final SecretKey secretKey) {
102                this(secretKey, -1);
103        }
104        
105        
106        /**
107         * Creates a new SIV AES - based codec for pairwise subject
108         * identifiers.
109         *
110         * @param secretKey          A 256, 384, or 512-bit secret key. Must
111         *                           not be {@code null}.
112         * @param padSubjectToLength Pads the local subject to the specified
113         *                           length, -1 (negative integer) for no
114         *                           padding.
115         */
116        public SIVAESBasedPairwiseSubjectCodec(final SecretKey secretKey,
117                                               final int padSubjectToLength) {
118                super(null);
119
120                // SIV-AES does not use a salt
121                byte[] keyBytes = secretKey.getEncoded();
122                
123                switch (keyBytes.length) {
124                        case 32:
125                                aesCtrKey = ByteUtils.subArray(keyBytes, 0, 16);
126                                macKey = ByteUtils.subArray(keyBytes, 16, 16);
127                                break;
128                        case 48:
129                                aesCtrKey = ByteUtils.subArray(keyBytes, 0, 24);
130                                macKey = ByteUtils.subArray(keyBytes, 24, 24);
131                                break;
132                        case 64:
133                                aesCtrKey = ByteUtils.subArray(keyBytes, 0, 32);
134                                macKey = ByteUtils.subArray(keyBytes, 32, 32);
135                                break;
136                        default:
137                                throw new IllegalArgumentException("The SIV AES secret key length must be 256, 384 or 512 bits");
138                }
139                
140                this.padSubjectToLength = padSubjectToLength;
141        }
142        
143        
144        /**
145         * Returns the secret key.
146         *
147         * @return The key.
148         */
149        public SecretKey getSecretKey() {
150                
151                return new SecretKeySpec(ByteUtils.concat(aesCtrKey, macKey), "AES");
152        }
153        
154        
155        /**
156         * Returns the optional padded string length of local subjects.
157         *
158         * @return The padding string length, -1 (negative integer) for no
159         *         padding.
160         */
161        public int getPadSubjectToLength() {
162                
163                return padSubjectToLength;
164        }
165        
166        
167        private static String escapeSeparator(final String s) {
168                
169                return s.replace("|", "\\|");
170        }
171        
172        
173        @Override
174        public Subject encode(final SectorID sectorID, final Subject localSub) {
175                
176                // Escape separator chars
177                final String escapedSectorIDString = escapeSeparator(sectorID.getValue());
178                final String escapedLocalSub = escapeSeparator(localSub.getValue());
179                
180                StringBuilder optionalPadding = new StringBuilder();
181                
182                if (padSubjectToLength > 0) {
183                        // Apply padding
184                        int paddingLength = padSubjectToLength - escapedLocalSub.length();
185                        
186                        if (paddingLength == 1) {
187                                
188                                optionalPadding = new StringBuilder("|");
189                                
190                        } else if (paddingLength > 1) {
191                                
192                                optionalPadding = new StringBuilder("|");
193                                int i = paddingLength;
194                                while (--i > 0) {
195                                        optionalPadding.append("0"); // pad with 0
196                                }
197                        }
198                }
199                
200                // Join parameters, delimited by '|'
201                final String plainTextString = (escapedSectorIDString + '|' + escapedLocalSub + optionalPadding);
202                
203                byte[] plainText = plainTextString.getBytes(CHARSET);
204                byte[] cipherText = AES_SIV.encrypt(aesCtrKey, macKey, plainText);
205                return new Subject(Base64URL.encode(cipherText).toString());
206        }
207        
208        
209        @Override
210        public Map.Entry<SectorID, Subject> decode(final Subject pairwiseSubject)
211                throws InvalidPairwiseSubjectException {
212                
213                byte[] cipherText = new Base64URL(pairwiseSubject.getValue()).decode();
214                
215                byte[] plainText;
216                try {
217                        plainText  = AES_SIV.decrypt(aesCtrKey, macKey, cipherText);
218                } catch (Exception e) {
219                        throw new InvalidPairwiseSubjectException("Decryption failed: " + e.getMessage(), e);
220                }
221                
222                // Split along the '|' delimiter
223                String[] parts = new String(plainText, CHARSET).split("(?<!\\\\)\\|");
224                
225                // Unescape delimiter
226                for (int i=0; i<parts.length; i++) {
227                        parts[i] = parts[i].replace("\\|", "|");
228                }
229                
230                // Check format
231                if (parts.length > 3) {
232                        throw new InvalidPairwiseSubjectException("Invalid format: Unexpected number of tokens: " + parts.length);
233                }
234
235                if (parts.length < 2) {
236                        throw new InvalidPairwiseSubjectException("Invalid format: Expected at least 2 parts, got " + parts.length);
237                }
238
239                if (parts[0].isEmpty()) {
240                        throw new InvalidPairwiseSubjectException("Invalid format: Sector ID is empty");
241                }
242                
243                
244                return new AbstractMap.SimpleImmutableEntry<>(new SectorID(parts[0]), new Subject(parts[1]));
245        }
246}