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}