package org.xbib.elasticsearch.index.mapper.crypt; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.document.Field; import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexableField; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.StringFieldMapper; import org.elasticsearch.index.mapper.TextFieldMapper; import java.io.IOException; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.List; import java.util.Map; import static org.elasticsearch.index.mapper.TypeParsers.parseField; import static org.elasticsearch.index.mapper.TypeParsers.parseMultiField; /** * */ public class CryptMapper extends TextFieldMapper { private static final Logger logger = LogManager.getLogger(CryptMapper.class.getName()); public static final String MAPPER_TYPE = "crypt"; private static final char[] hexDigit = new char[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; private String algo; public CryptMapper(String simpleName, TextFieldType fieldType, MappedFieldType defaultFieldType, int positionIncrementGap, Boolean includeInAll, Settings indexSettings, MultiFields multiFields, CopyTo copyTo, String algo) { super(simpleName, fieldType, defaultFieldType, positionIncrementGap, includeInAll, indexSettings, multiFields, copyTo); this.algo = algo; } static StringFieldMapper.ValueAndBoost parseCreateFieldForCrypt(ParseContext context, String nullValue, float defaultBoost, String algo) throws IOException { if (context.externalValueSet()) { return new StringFieldMapper.ValueAndBoost((String) context.externalValue(), defaultBoost); } XContentParser parser = context.parser(); if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { return new StringFieldMapper.ValueAndBoost(nullValue, defaultBoost); } if (parser.currentToken() == XContentParser.Token.START_OBJECT) { XContentParser.Token token; String currentFieldName = null; String value = nullValue; float boost = defaultBoost; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else { if ("value".equals(currentFieldName) || "_value".equals(currentFieldName)) { value = crypt(parser.textOrNull(), algo); } else if ("boost".equals(currentFieldName) || "_boost".equals(currentFieldName)) { boost = parser.floatValue(); } else { throw new IllegalArgumentException("unknown property [" + currentFieldName + "]"); } } } return new StringFieldMapper.ValueAndBoost(value, boost); } return new StringFieldMapper.ValueAndBoost(crypt(parser.textOrNull(), algo), defaultBoost); } static String crypt(String plainText, String algo) { if (plainText == null) { return null; } MessageDigest digest; try { digest = MessageDigest.getInstance(algo); digest.update(plainText.getBytes(Charset.forName("UTF-8"))); return '{' + algo + '}' + bytesToHex(digest.digest()); } catch (NoSuchAlgorithmException e) { logger.error(e.getMessage(), e); } return null; } private static String bytesToHex(byte[] b) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < b.length; i++) { buf.append(hexDigit[(b[i] >> 4) & 0x0f]).append(hexDigit[b[i] & 0x0f]); } return buf.toString(); } @Override protected String contentType() { return MAPPER_TYPE; } @Override protected void parseCreateField(ParseContext context, List<IndexableField> fields) throws IOException { StringFieldMapper.ValueAndBoost valueAndBoost = parseCreateFieldForCrypt(context, fieldType().nullValueAsString(), fieldType().boost(), algo); if (valueAndBoost.value() == null) { return; } if (fieldType().indexOptions() != IndexOptions.NONE || fieldType().stored()) { Field field = new Field(fieldType().name(), valueAndBoost.value(), fieldType()); fields.add(field); } if (fieldType().hasDocValues()) { fields.add(new SortedSetDocValuesField(fieldType().name(), new BytesRef(valueAndBoost.value()))); } } @Override protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, ToXContent.Params params) throws IOException { super.doXContentBody(builder, includeDefaults, params); builder.field("algo", algo); } public static class Builder extends TextFieldMapper.Builder { private String algo; Builder(String name) { super(name); this.builder = this; this.algo = "SHA-256"; } Builder algo(String algo) { this.algo = algo; return this; } @Override public CryptMapper build(Mapper.BuilderContext context) { if (fieldType.indexOptions() != IndexOptions.NONE && !fieldType.tokenized()) { defaultFieldType.setOmitNorms(true); defaultFieldType.setIndexOptions(IndexOptions.DOCS); if (!omitNormsSet && Float.compare(fieldType.boost(), 1.0f) == 0) { fieldType.setOmitNorms(true); } if (!indexOptionsSet) { fieldType.setIndexOptions(IndexOptions.DOCS); } } setupFieldType(context); return new CryptMapper(name, fieldType(), defaultFieldType, 100, includeInAll, context.indexSettings(), multiFieldsBuilder.build(this, context), copyTo, algo); } } public static class TypeParser implements Mapper.TypeParser { @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Mapper.Builder parse(String name, Map<String, Object> node, Mapper.TypeParser.ParserContext parserContext) { Builder builder = new Builder(name); parseField(builder, name, node, parserContext); Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Object> entry = iterator.next(); String propName = entry.getKey(); Object propNode = entry.getValue(); switch (propName) { case "algo" : builder.algo(propNode.toString()); iterator.remove(); break; case "position_increment_gap" : iterator.remove(); break; default: parseMultiField(builder, name, parserContext, propName, propNode); break; } } return builder; } } }