org.jruby.ext.krypt.asn1.Asn1Codecs.java Source code

Java tutorial

Introduction

Here is the source code for org.jruby.ext.krypt.asn1.Asn1Codecs.java

Source

/*
 * krypt-core API - Java version
 *
 * Copyright (c) 2011-2013
 * Hiroshi Nakamura <nahi@ruby-lang.org>
 * Martin Bosslet <martin.bosslet@gmail.com>
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.jruby.ext.krypt.asn1;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import org.jcodings.specific.UTF8Encoding;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.jruby.Ruby;
import org.jruby.RubyBignum;
import org.jruby.RubyBoolean;
import org.jruby.RubyFixnum;
import org.jruby.RubyNumeric;
import org.jruby.RubyString;
import org.jruby.RubyTime;
import org.jruby.ext.krypt.Errors;
import org.jruby.ext.krypt.asn1.RubyAsn1.Asn1Codec;
import org.jruby.ext.krypt.asn1.RubyAsn1.DecodeContext;
import org.jruby.ext.krypt.asn1.RubyAsn1.EncodeContext;
import org.jruby.ext.krypt.asn1.RubyAsn1.ValidateContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;

/**
 * 
 * @author <a href="mailto:Martin.Bosslet@gmail.com">Martin Bosslet</a>
 */
public class Asn1Codecs {

    private Asn1Codecs() {
    }

    static final Asn1Codec DEFAULT = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            IRubyObject value = ctx.getValue();
            if (value == null || value.isNil())
                return null;
            return ctx.getValue().convertToString().getBytes();
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            byte[] value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            if (value == null || value.length == 0)
                return runtime.newString();
            else
                return runtime.newString(new ByteList(value, false));
        }

        @Override
        public void validate(ValidateContext ctx) {
            IRubyObject value = ctx.getValue();
            if (value == null || value.isNil())
                return;
            if (!(value instanceof RubyString))
                throw Errors.newASN1Error(ctx.getRuntime(), "Value must be a string");
        }
    };

    private static final Asn1Codec END_OF_CONTENTS = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            return null;
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            byte[] value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            if (!(value == null || value.length == 0))
                throw Errors.newASN1Error(runtime, "Invalid end of contents encoding");
            return runtime.getNil();
        }

        @Override
        public void validate(ValidateContext ctx) {
            if (!ctx.getValue().isNil())
                throw Errors.newASN1Error(ctx.getRuntime(), "Value for END_OF_CONTENTS must be nil");
        }
    };

    private static final Asn1Codec BOOLEAN = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            IRubyObject value = ctx.getValue();
            byte[] b = new byte[1];
            if (!value.isTrue())
                b[0] = (byte) 0x00;
            else
                b[0] = (byte) 0xff;
            return b;
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            byte[] value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            if (value == null || value.length != 1)
                throw Errors.newASN1Error(runtime, "Boolean value with length != 1 found");
            if (value[0] == ((byte) 0x00))
                return runtime.getFalse();
            else
                return runtime.getTrue();
        }

        @Override
        public void validate(ValidateContext ctx) {
            IRubyObject value = ctx.getValue();
            if (!(value instanceof RubyBoolean))
                throw Errors.newASN1Error(ctx.getRuntime(), "Value for BOOLEAN must be either true or false");
        }
    };

    private static final Asn1Codec INTEGER = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            IRubyObject value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            if (value instanceof RubyFixnum) {
                return BigInteger.valueOf(RubyNumeric.num2long(value)).toByteArray();
            } else if (value instanceof RubyBignum) {
                BigInteger big = ((RubyBignum) value).getValue();
                return big.toByteArray();
            } else {
                throw Errors.newASN1Error(runtime, "Value is not a number");
            }
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            byte[] value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            if (value == null)
                throw Errors.newASN1Error(runtime, "Invalid integer encoding");
            return RubyBignum.newBignum(runtime, new BigInteger(value));
        }

        @Override
        public void validate(ValidateContext ctx) {
            IRubyObject value = ctx.getValue();
            if (!(value instanceof RubyFixnum || value instanceof RubyBignum))
                throw Errors.newASN1Error(ctx.getRuntime(), "Value for integer type must be an integer Number");
        }
    };

    private static final Asn1Codec BIT_STRING = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            IRubyObject recv = ctx.getReceiver();
            IRubyObject value = ctx.getValue();
            IRubyObject unusedBitsIv = recv.getInstanceVariables().getInstanceVariable("unused_bits");
            int unusedBits = RubyNumeric.fix2int(unusedBitsIv);
            checkUnusedBits(ctx.getRuntime(), unusedBits);
            byte[] bytes = value.convertToString().getBytes();
            byte[] ret = new byte[bytes.length + 1];
            ret[0] = (byte) (unusedBits & 0xff);
            System.arraycopy(bytes, 0, ret, 1, bytes.length);
            return ret;
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            byte[] value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            IRubyObject recv = ctx.getReceiver();
            if (value == null)
                throw Errors.newASN1Error(runtime, "Invalid BIT STRING encoding");
            int unusedBits = value[0] & 0xff;
            checkUnusedBits(runtime, unusedBits);
            IRubyObject ret = runtime.newString(new ByteList(value, 1, value.length - 1, false));
            recv.getInstanceVariables().setInstanceVariable("unused_bits",
                    RubyNumeric.int2fix(runtime, unusedBits));
            return ret;
        }

        @Override
        public void validate(ValidateContext ctx) {
            if (ctx.getValue().isNil())
                throw Errors.newASN1Error(ctx.getRuntime(), "BIT STRING value cannot be empty");
            DEFAULT.validate(ctx);
        }

    };

    private static void checkUnusedBits(Ruby runtime, int unusedBits) {
        if (unusedBits < 0 || unusedBits > 7)
            throw Errors.newASN1Error(runtime, "Unused bits must be 0..7");
    }

    private static final Asn1Codec OCTET_STRING = DEFAULT;

    private static final Asn1Codec NULL = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            return null;
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            byte[] value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            if (!(value == null || value.length == 0))
                throw Errors.newASN1Error(runtime, "Invalid null encoding");
            return runtime.getNil();
        }

        @Override
        public void validate(ValidateContext ctx) {
            IRubyObject value = ctx.getValue();
            if (!(value == null || value.isNil()))
                throw Errors.newASN1Error(ctx.getRuntime(), "Value must be nil for NULL");
        }
    };

    private static final Asn1Codec OBJECT_ID = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            Ruby runtime = ctx.getRuntime();
            IRubyObject value = ctx.getValue();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            long first, second, cur;
            ObjectIdEncodeContext oidctx = new ObjectIdEncodeContext(value.convertToString().getBytes(), runtime);

            if ((first = oidctx.nextSubId()) == -1)
                throw Errors.newASN1Error(runtime, "Error while encoding object identifier");
            if ((second = oidctx.nextSubId()) == -1)
                throw Errors.newASN1Error(runtime, "Error while encoding object identifier");
            checkFirstSubId(runtime, first);
            checkSecondSubId(runtime, second);

            cur = 40 * first + second;

            try {
                writeLong(baos, cur);

                while ((cur = oidctx.nextSubId()) != -1) {
                    writeLong(baos, cur);
                }
            } catch (IOException ex) {
                throw Errors.newASN1Error(runtime, ex.getMessage());
            }

            return baos.toByteArray();
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            byte[] value = ctx.getValue();
            Ruby runtime = ctx.getRuntime();
            if (value == null)
                throw Errors.newASN1Error(runtime, "Invalid object id encoding");
            long first, second, cur;
            ObjectIdParseContext oidctx = new ObjectIdParseContext(value, runtime);
            StringBuilder builder = new StringBuilder();

            if ((cur = oidctx.parseNext()) == -1)
                throw Errors.newASN1Error(runtime, "Error while parsing object identifier");
            if (cur > 40 * 2 + 39)
                throw Errors.newASN1Error(runtime, "Illegal first octet, value too large");
            first = determineFirst(cur);
            second = cur - 40 * first;
            checkFirstSubId(runtime, first);
            checkSecondSubId(runtime, second);
            builder.append(String.valueOf(first));
            appendNumber(builder, second);

            while ((cur = oidctx.parseNext()) != -1)
                appendNumber(builder, cur);

            return runtime.newString(new ByteList(builder.toString().getBytes()));
        }

        private void appendNumber(StringBuilder b, long cur) {
            b.append('.').append(String.valueOf(cur));
        }

        private long determineFirst(long combined) {
            long f = 1;
            while (40 * f <= combined)
                f++;
            return f - 1;
        }

        private void checkFirstSubId(Ruby runtime, long first) {
            if (first > 2)
                throw Errors.newASN1Error(runtime, "First sub id must be 0..2");
        }

        private void checkSecondSubId(Ruby runtime, long second) {
            if (second > 39)
                throw Errors.newASN1Error(runtime, "Second sub id must be 0..39");
        }

        @Override
        public void validate(ValidateContext ctx) {
            if (!(ctx.getValue() instanceof RubyString))
                throw Errors.newASN1Error(ctx.getRuntime(), "Value for OBJECT IDENTIFIER must be a String");
        }
    };

    static int determineNumberOfShifts(long value, int shiftBy) {
        int i;
        for (i = 0; value > 0; i++) {
            value >>= shiftBy;
        }
        return i;
    }

    private static void writeLong(ByteArrayOutputStream baos, long cur) throws IOException {
        if (cur == 0) {
            baos.write(0);
            return;
        }

        int numShifts = determineNumberOfShifts(cur, 7);
        byte[] bytes = new byte[numShifts];

        for (int i = numShifts - 1; i >= 0; i--) {
            byte b = (byte) (cur & 0x7f);
            if (i < numShifts - 1)
                b |= 0x80;
            bytes[i] = b;
            cur >>= 7;
        }

        baos.write(bytes);
    }

    private static final Asn1Codec ENUMERATED = INTEGER;

    private static final Asn1Codec UTF8_STRING = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            IRubyObject value = ctx.getValue();
            if (value.isNil())
                return null;
            RubyString s = value.convertToString();
            s.associateEncoding(UTF8Encoding.INSTANCE);
            return s.getBytes();
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            IRubyObject obj = DEFAULT.decode(ctx);
            obj.asString().associateEncoding(UTF8Encoding.INSTANCE);
            return obj;
        }

        @Override
        public void validate(ValidateContext ctx) {
            DEFAULT.validate(ctx);
        }
    };

    private static final DateTimeFormatter UTC_FORMATTER = DateTimeFormat.forPattern("yyMMddHHmmss'Z'")
            .withZone(DateTimeZone.UTC);
    private static final DateTimeFormatter GT_FORMATTER = DateTimeFormat.forPattern("yyyyMMddHHmmss'Z'")
            .withZone(DateTimeZone.UTC);

    private static byte[] encodeTime(Ruby runtime, IRubyObject value, DateTimeFormatter formatter) {
        if (value instanceof RubyTime)
            return encodeRubyTime(runtime, value, formatter);
        else
            return encodeFixnumTime(runtime, value, formatter);
    }

    private static byte[] encodeRubyTime(Ruby runtime, IRubyObject value, DateTimeFormatter formatter) {
        try {
            return ((RubyTime) value).getDateTime().toString(formatter).getBytes();
        } catch (Exception ex) {
            throw Errors.newASN1Error(runtime, "Error while encoding time value: " + ex.getMessage());
        }
    }

    private static byte[] encodeFixnumTime(Ruby runtime, IRubyObject value, DateTimeFormatter formatter) {
        try {
            long val = RubyNumeric.num2long(value);
            if (val < 0)
                throw Errors.newASN1Error(runtime, "Negative time value given");
            /* Ruby time is *seconds* since the epoch */
            DateTime dt = new DateTime(val * 1000, DateTimeZone.UTC);
            return dt.toString(formatter).getBytes();
        } catch (Exception ex) {
            throw Errors.newASN1Error(runtime, "Error while encoding time value: " + ex.getMessage());
        }
    }

    private static IRubyObject decodeTime(Ruby runtime, byte[] value, DateTimeFormatter formatter) {
        if (value == null)
            throw Errors.newASN1Error(runtime, "Invalid time encoding");
        try {
            DateTime dateTime = formatter.parseDateTime(new String(value, Charset.forName("US-ASCII")));
            return RubyTime.newTime(runtime, dateTime);
        } catch (Exception ex) {
            throw Errors.newASN1Error(runtime, "Error while decoding time value: " + ex.getMessage());
        }
    }

    private static void validateTime(ValidateContext ctx) {
        IRubyObject value = ctx.getValue();
        if (!(value instanceof RubyTime || value instanceof RubyFixnum || value instanceof RubyString))
            throw Errors.newASN1Error(ctx.getRuntime(), "Time type must be either a Time, a Fixnum or a String");
    }

    private static final Asn1Codec UTC_TIME = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            return encodeTime(ctx.getRuntime(), ctx.getValue(), UTC_FORMATTER);
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            return decodeTime(ctx.getRuntime(), ctx.getValue(), UTC_FORMATTER);
        }

        @Override
        public void validate(ValidateContext ctx) {
            validateTime(ctx);
        }
    };

    private static final Asn1Codec GENERALIZED_TIME = new Asn1Codec() {

        @Override
        public byte[] encode(EncodeContext ctx) {
            return encodeTime(ctx.getRuntime(), ctx.getValue(), GT_FORMATTER);
        }

        @Override
        public IRubyObject decode(DecodeContext ctx) {
            return decodeTime(ctx.getRuntime(), ctx.getValue(), GT_FORMATTER);
        }

        @Override
        public void validate(ValidateContext ctx) {
            validateTime(ctx);
        }
    };

    static Asn1Codec[] CODECS = new Asn1Codec[] { END_OF_CONTENTS, BOOLEAN, INTEGER, BIT_STRING, OCTET_STRING, NULL,
            OBJECT_ID, null, null, null, ENUMERATED, null, UTF8_STRING, null, null, null, null, null, OCTET_STRING,
            OCTET_STRING, OCTET_STRING, OCTET_STRING, OCTET_STRING, UTC_TIME, GENERALIZED_TIME, OCTET_STRING,
            OCTET_STRING, OCTET_STRING, OCTET_STRING, OCTET_STRING, OCTET_STRING };

    private static class ObjectIdEncodeContext {
        private final byte[] raw;
        private final Ruby runtime;
        private int offset = 0;

        private static long ENCODE_LIMIT = Long.MAX_VALUE / 10;

        public ObjectIdEncodeContext(byte[] raw, Ruby runtime) {
            this.raw = raw;
            this.runtime = runtime;
        }

        public final long nextSubId() {
            if (offset >= raw.length)
                return -1;

            long ret = 0;

            char c = (char) (raw[offset] & 0xff);
            if (c == '.')
                throw Errors.newASN1Error(runtime, "Sub identifier cannot start with '.'");

            while (offset < raw.length && (c = (char) (raw[offset] & 0xff)) != '.') {
                if (c < '0' || c > '9')
                    throw Errors.newASN1Error(runtime, "Invalid character in object identifer: " + c);
                if (ret > ENCODE_LIMIT)
                    throw Errors.newASN1Error(runtime, "Sub object identifier too large");
                if (offset + 1 == Long.MAX_VALUE)
                    throw Errors.newASN1Error(runtime, "Object id value too large");

                ret *= 10;
                ret += c - '0';
                offset++;
            }

            offset++; /* skip '.' */
            return ret;
        }
    }

    private static class ObjectIdParseContext {
        private final byte[] raw;
        private final Ruby runtime;
        int offset = 0;

        private static long LIMIT_PARSE = Long.MAX_VALUE >> 7;

        public ObjectIdParseContext(byte[] raw, Ruby runtime) {
            this.raw = raw;
            this.runtime = runtime;
        }

        public long parseNext() {
            long num = 0;

            if (offset >= raw.length)
                return -1;

            while ((raw[offset] & 0x80) > 0) {
                if (num > LIMIT_PARSE)
                    throw Errors.newASN1Error(runtime, "Sub identifier too large");
                num <<= 7;
                num |= raw[offset++] & 0x7f;
                if (offset >= raw.length)
                    throw Errors.newASN1Error(runtime, "Invalid object identifier encoding");
            }

            num <<= 7;
            num |= raw[offset++] & 0x7f;

            return num;
        }
    }
}