Commits

Anonymous committed 474423a Draft

initial commit

  • Participants

Comments (0)

Files changed (29)

+toycrypto
+=========
+toycrypto is an implementation of certain cryptographic primitives and
+protocols in [Ceylon](http://ceylon-lang.org).
+
+**DO NOT USE THIS MODULE IN PRODUCTION.** toycrypto has not been examined by
+any security or cryptography professional, and is a personal project only.
+
+Dependencies
+------------
+Currently [BouncyCastle 1.49](http://www.bouncycastle.org/java.html) for
+testing/comparison, although I intend to factor that out at some point.
+
+JavaScript status
+-----------------
+toycrypto is JVM-only at the moment because there are no implementations of
+`Whole & Binary` available at the moment. (Technically there isn't an
+implementation of `Whole & Binary` for the JVM, either, so I'm using BigInteger
+directly in a few functions).
+import javax.crypto { Cipher { fetchCipher = getInstance, \iENCRYPT_MODE, \iDECRYPT_MODE } }
+import java.security { SecureRandom }
+import java.lang { ByteArray }
+import javax.crypto.spec { IvParameterSpec }
+import toycrypto.keys { SharedKey }
+
+String transformation = "AES/OFB/NoPadding";
+Cipher cipher = fetchCipher(transformation);
+
+shared class CipherText(bytes, iv) {
+    shared ByteArray bytes;
+    shared ByteArray iv;
+}
+
+shared CipherText encrypt(SharedKey key, ByteArray plaintext) {
+    cipher.init(\iENCRYPT_MODE, key.javaKey, SecureRandom());
+    return CipherText { bytes = cipher.doFinal(plaintext); iv = cipher.iv; };
+}
+
+shared ByteArray decrypt(SharedKey key, CipherText ciphertext) {
+    cipher.init(\iDECRYPT_MODE, key.javaKey, IvParameterSpec(ciphertext.iv));
+    return cipher.doFinal(ciphertext.bytes);
+}
+import toycrypto.util { iterate, takeUntil, compareSequences }
+import ceylon.math.whole { Whole }
+import toycrypto.math { encodeWhole, decodeWhole }
+import ceylon.io.buffer { ByteBuffer, newByteBufferWithData }
+
+//
+// TOP-LEVEL TYPES
+//
+
+shared interface ASN1Representable {
+    shared formal ASN1Encodable asn1;
+}
+
+shared interface ASN1Encodable
+        of ASN1Decodable|ImplicitTag
+        satisfies ASN1Representable {
+    shared formal Integer tag;
+    shared formal [Integer*] encodedBody;
+    
+    shared [Integer+] encoded {
+        value body = encodedBody;
+        return encodeSize(body.size).chain(body).sequence.withLeading(tag);
+    }
+    
+    shared actual ASN1Encodable asn1 => this;
+}
+
+shared abstract class ASN1Decodable(type)
+        of ASN1BasicObject|ExplicitTag|OpaqueImplicitTag
+        satisfies ASN1Encodable {
+    shared default ASN1DecodableType type;
+}
+
+shared abstract class ASN1BasicObject(ASN1BasicObjectType type)
+        of ObjectIdentifier|ASN1Sequence|ASN1Set|ASN1Integer|
+           OctetString|BitString
+        extends ASN1Decodable(type)
+        satisfies ASN1Encodable {
+    shared actual Integer tag => type.tag;
+}
+
+//
+// METAMODEL
+//
+
+shared interface ASN1DecodableType
+        of ASN1BasicObjectType|contextSpecificTagType {
+    shared formal Boolean handles(Integer tag);
+    // Implementers aren't obliged to do any checking on tag
+    shared formal ASN1Decodable parse(Integer tag, ByteBuffer bytes);
+}
+
+[ASN1DecodableType*] asn1DecodableTypes = [
+    contextSpecificTagType,
+    asn1SequenceType,
+    objectIdentifierType,
+    asn1IntegerType,
+    octetStringType,
+    bitStringType,
+    asn1SetType
+];
+
+ASN1DecodableType? asn1DecodableType(Integer tag) {
+    return asn1DecodableTypes.find(
+        (ASN1DecodableType elem) => elem.handles(tag));
+}
+
+shared abstract class ASN1BasicObjectType(tag)
+        of asn1SequenceType|objectIdentifierType|asn1IntegerType|
+           octetStringType|bitStringType|asn1SetType
+        satisfies ASN1DecodableType {
+    shared Integer tag;
+    shared actual Boolean handles(Integer tag) => tag == this.tag;
+    shared actual formal ASN1BasicObject parse(Integer tag, ByteBuffer bytes);
+}
+
+object asn1SequenceType
+        extends ASN1BasicObjectType(derTags.sequence.or(derTags.constructed)) {
+    shared actual ASN1Sequence parse(Integer tag, ByteBuffer bytes) =>
+        ASN1Sequence(readSequential(bytes, parseASN1Buffer));
+}
+object objectIdentifierType
+        extends ASN1BasicObjectType(derTags.objectIdentifier)
+        satisfies ASN1DecodableType {
+    shared actual ObjectIdentifier parse(Integer tag, ByteBuffer bytes) =>
+        createObjectIdentifier(bytes);
+}
+object asn1IntegerType
+        extends ASN1BasicObjectType(derTags.integer)
+        satisfies ASN1DecodableType {
+    shared actual ASN1Integer parse(Integer tag, ByteBuffer bytes) =>
+        ASN1Integer(decodeWhole(bytes.sequence));
+}
+object octetStringType
+        extends ASN1BasicObjectType(derTags.octetString)
+        satisfies ASN1DecodableType {
+    shared actual OctetString parse(Integer tag, ByteBuffer bytes) =>
+        OctetString(getBytes(bytes, bytes.available));
+    
+}
+object bitStringType
+        extends ASN1BasicObjectType(derTags.bitString)
+        satisfies ASN1DecodableType {
+    shared actual BitString parse(Integer tag, ByteBuffer bytes) {
+        assert(bytes.available > 0);
+        value padBits = bytes.get();
+        return BitString(getBytes(bytes, bytes.available), padBits);
+    }
+    
+}
+object asn1SetType
+        extends ASN1BasicObjectType(derTags.set.or(derTags.constructed))
+        satisfies ASN1DecodableType {
+    shared actual ASN1Set parse(Integer tag, ByteBuffer bytes) =>
+        ASN1Set(LazySet(readSequential(bytes, parseASN1Buffer)));
+    
+}
+ object contextSpecificTagType
+        satisfies ASN1DecodableType {
+    shared actual Boolean handles(Integer tag) => tag.and(derTags.tagged) != 0;
+    shared actual ExplicitTag|OpaqueImplicitTag parse(Integer tag, ByteBuffer bytes) {
+        value contextTag = readContextTag(bytes, tag);
+        if (tag.and(derTags.constructed) != 0) {
+            value contents = readSequential(bytes, parseASN1Buffer);
+            if (contents.size == 1) { // IDK, this is how BouncyCastle does it
+                assert(nonempty contents);
+                return ExplicitTag(contextTag, contents.first);
+            } else {
+                return OpaqueImplicitConstructedTag(contextTag, contents);
+            }
+        } else {
+            return OpaqueImplicitPrimitiveTag(contextTag, bytes.sequence);
+        }
+    }
+}
+
+//
+// TAGS
+//
+
+shared interface ContextSpecificTag {
+    shared formal Integer contextTag;
+}
+
+shared class ExplicitTag(contextTag, wrappedObject)
+        extends ASN1Decodable(contextSpecificTagType)
+        satisfies ContextSpecificTag {
+    shared actual Integer contextTag;
+    shared ASN1Encodable wrappedObject;
+
+    shared actual Integer tag =>
+        contextTag.or(derTags.tagged).or(derTags.constructed);
+    shared actual Integer[] encodedBody => wrappedObject.encoded;
+}
+
+shared class ImplicitTag(contextTag, wrappedObject)
+        satisfies ASN1Encodable & ContextSpecificTag {
+    shared actual Integer contextTag;
+    shared ASN1Encodable wrappedObject;
+    
+    shared actual Integer tag => contextTag.or(derTags.tagged);
+    shared actual Integer[] encodedBody => wrappedObject.encodedBody;
+}
+
+shared abstract class OpaqueImplicitTag(contextTag)
+        of OpaqueImplicitPrimitiveTag|OpaqueImplicitConstructedTag
+        extends ASN1Decodable(contextSpecificTagType)
+        satisfies ContextSpecificTag {
+    shared actual Integer contextTag;
+    shared actual Integer tag => contextTag.or(derTags.tagged);
+
+    shared formal ASN1BasicObject decodeAs(ASN1BasicObjectType type);
+}
+
+class OpaqueImplicitPrimitiveTag(Integer contextTag, encodedBody)
+        extends OpaqueImplicitTag(contextTag) {
+    shared actual [Integer*] encodedBody;
+    
+    shared actual Boolean equals(Object that) {
+        if (is OpaqueImplicitPrimitiveTag that) {
+            return contextTag == that.contextTag &&
+                    encodedBody == that.encodedBody;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [contextTag.hash, encodedBody.hash].hash;
+
+    shared actual ASN1BasicObject decodeAs(ASN1BasicObjectType type) =>
+        type.parse(type.tag, newByteBufferWithData(*encodedBody));
+}
+
+class OpaqueImplicitConstructedTag(Integer contextTag, sequence)
+        extends OpaqueImplicitTag(contextTag) {
+    [ASN1Encodable*] sequence;
+    
+    shared actual [Integer*] encodedBody =>
+        join(*sequence.map((ASN1Encodable elem) => elem.encoded));
+    
+    shared actual Boolean equals(Object that) {
+        if (is OpaqueImplicitConstructedTag that) {
+            return contextTag == that.contextTag &&
+                    sequence == that.sequence;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [contextTag.hash, sequence.hash].hash;
+
+    shared actual ASN1Sequence|ASN1Set decodeAs(ASN1BasicObjectType type) {
+        switch (type)
+        case (asn1SequenceType) {
+            return ASN1Sequence(sequence);
+        } case (asn1SetType) {
+            return ASN1Set(LazySet(sequence));
+        } else {
+            throw Exception("Contents of this opaque implicit tag cannot be ``type`` (contents are constructed type).");
+        }
+    }
+}
+
+//
+// BASIC OBJECTS
+//
+
+shared class ObjectIdentifier(sequence)
+        extends ASN1BasicObject(objectIdentifierType) {
+    shared [Integer+] sequence;
+    assert(sequence.size > 2);
+    
+    [Integer+] writeField(Integer field) {
+        return takeUntil(
+                iterate(field, (Integer i) => i.rightLogicalShift(7)),
+                (Integer i) => i == 0)
+            .collect((Integer elem) => elem.and(#7f).or(#80))
+            .withLeading(field.and(#7f))
+            .reversed;
+    }
+
+    shared actual [Integer+] encodedBody {
+        value first = sequence.first * 40;
+        assert(exists second = sequence[1]);
+        return join(*sequence[2...].map((Integer elem) => writeField(elem)))
+            .withLeading(first + second);
+    }
+    
+    shared actual String string => ".".join(sequence.map((Integer elem) => elem.string));
+    
+    shared actual Boolean equals(Object that) {
+        if (is ObjectIdentifier that) {
+            return sequence == that.sequence;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [sequence.hash].hash;
+}
+
+shared class ASN1Sequence(sequence)
+        extends ASN1BasicObject(asn1SequenceType) {
+    shared [ASN1Encodable*] sequence;
+    
+    shared actual [Integer*] encodedBody =>
+        join(*sequence.map((ASN1Encodable elem) => elem.encoded));
+    
+    shared actual String string => "ASN1Sequence(``", ".join(sequence.map((ASN1Encodable elem) => elem.string))``)";
+    
+    shared actual Boolean equals(Object that) {
+        if (is ASN1Sequence that) {
+            return sequence == that.sequence;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [sequence.hash].hash;
+}
+
+shared class ASN1Set(set)
+        extends ASN1BasicObject(asn1SetType) {
+    shared Set<ASN1Encodable> set;
+    
+    shared actual [Integer*] encodedBody =>
+        join(*set.map((ASN1Encodable elem) => elem.encoded)
+                 .sort(compareSequences<Integer>));
+    
+    shared actual String string => "ASN1Set(``", ".join(set.map((ASN1Encodable elem) => elem.string))``)";
+    
+    shared actual Boolean equals(Object that) {
+        if (is ASN1Set that) {
+            return set == that.set;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [set.hash].hash;
+}
+
+shared class ASN1Integer(whole)
+        extends ASN1BasicObject(asn1IntegerType) {
+    shared Whole whole;
+    
+    shared actual [Integer+] encodedBody => encodeWhole(whole);
+    
+    shared actual String string => "ASN1Integer(``whole``)";
+    
+    shared actual Boolean equals(Object that) {
+        if (is ASN1Integer that) {
+            return whole == that.whole;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [whole.hash].hash;
+}
+
+shared class OctetString(bytes)
+        extends ASN1BasicObject(octetStringType) {
+    shared [Integer*] bytes;
+    
+    shared actual [Integer*] encodedBody = bytes;
+    
+    shared actual String string => "OctetString(``bytes``)";
+    
+    shared actual Boolean equals(Object that) {
+        if (is OctetString that) {
+            return bytes == that.bytes;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [bytes.hash].hash;
+}
+
+shared class BitString(bytes, padBits = 0)
+        extends ASN1BasicObject(bitStringType) {
+    shared [Integer*] bytes;
+    shared Integer padBits;
+    assert(padBits < 8);
+    
+    shared actual [Integer+] encodedBody => bytes.withLeading(padBits);
+    
+    shared actual String string => "BitString(``bytes``)";
+    
+    shared actual Boolean equals(Object that) {
+        if (is BitString that) {
+            return bytes == that.bytes;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [bytes.hash].hash;
+}
+
+//
+// ENCODING TOOLS
+//
+
+[Integer+] encodeSize(Integer size) {
+     if (size > 127) {
+        value sizeSize = takeUntil(
+            iterate(size, (Integer i) => i.rightLogicalShift(8)),
+            (Integer i) => i == 0).size + 1;
+        value bytes = ((sizeSize - 1)*8..0).by(8)
+            .map((Integer i) => size.rightLogicalShift(i).and($11111111));
+        return bytes.sequence.withLeading(sizeSize.or(#80));
+    } else {
+        return [size];
+    }
+}
+
+object derTags {
+    shared Integer boolean = #01;
+    shared Integer integer = #02;
+    shared Integer bitString = #03;
+    shared Integer octetString = #04;
+    shared Integer null = #05;
+    shared Integer objectIdentifier = #06;
+    shared Integer external = #08;
+    shared Integer enumerated = #0a;
+    shared Integer utf8String = #0c;
+    shared Integer sequence = #10;
+    shared Integer set = #11;
+    shared Integer numericString = #12;
+    shared Integer printableString = #13;
+    shared Integer t61String = #14;
+    shared Integer ia5String = #16;
+    shared Integer utcTime = #17;
+    shared Integer generalisedTime = #18;
+    shared Integer visibleString = #1a;
+    shared Integer generalString = #1b;
+    shared Integer universalString = #1c;
+    shared Integer bmpString = #1e;
+    shared Integer constructed = #20;
+    shared Integer application = #40;
+    shared Integer tagged = #80;
+}
+import ceylon.math.whole { wholeNumber }
+
+abstract class ContentType([Integer+] identifier)
+    of dataContentType|signedDataContentType|envelopedDataContentType
+    extends ObjectIdentifier(identifier) {}
+
+ObjectIdentifier pkcs7 = ObjectIdentifier([1, 2, 840, 113549, 1, 7]);
+//id-data OBJECT IDENTIFIER ::= { iso(1) member-body(2)
+//         us(840) rsadsi(113549) pkcs(1) pkcs7(7) 1 }
+object dataContentType extends ContentType(pkcs7.sequence.withTrailing(1)) {}
+//id-signedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
+//         us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 }
+object signedDataContentType extends ContentType(pkcs7.sequence.withTrailing(2)) {}
+//id-envelopedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
+//          us(840) rsadsi(113549) pkcs(1) pkcs7(7) 3 }
+object envelopedDataContentType extends ContentType(pkcs7.sequence.withTrailing(3)) {}
+
+abstract class ContentInfo(contentType)
+        of ArbitraryData|SignedData|EnvelopedData
+        satisfies ASN1Representable {
+    shared ContentType contentType;
+    shared formal ASN1Representable content;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([contentType, ExplicitTag(0, content.asn1)]);
+}
+
+class EncryptedContentInfo(contentType, encryptionAlgorithm, content)
+        satisfies ASN1Representable {
+    shared ContentType contentType;
+    shared AlgorithmIdentifier encryptionAlgorithm;
+    shared OctetString content;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        contentType,
+        encryptionAlgorithm.asn1,
+        ImplicitTag(0, content)
+    ]);
+}
+
+class EncapsulatedContentInfo(contentInfo)
+        satisfies ASN1Representable {
+    shared ContentInfo contentInfo;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        contentInfo.contentType,
+        ExplicitTag(0, OctetString(contentInfo.content.asn1.encoded))
+    ]);
+}
+
+class ArbitraryData(content)
+        extends ContentInfo(dataContentType) {
+    shared actual ASN1Representable content;
+}
+
+class SignedData(digestAlgorithms, encapContentInfo, signerInfos)
+        extends ContentInfo(signedDataContentType) {
+    shared Integer version => nothing; // TODO
+    shared Set<AlgorithmIdentifier> digestAlgorithms;
+    shared EncapsulatedContentInfo encapContentInfo;
+    shared Set<SignerInfo> signerInfos;
+    
+    shared actual ASN1Sequence content => ASN1Sequence([
+        ASN1Integer(wholeNumber(version)),
+        ASN1Set(LazySet(digestAlgorithms.map((AlgorithmIdentifier elem) => elem.asn1))),
+        encapContentInfo.asn1,
+        ASN1Set(LazySet(signerInfos.map((SignerInfo elem) => elem.asn1)))
+    ]);
+}
+
+class SignerInfo(signerIdentifier, digestAlgorithm, signedAttrs,
+        signatureAlgorithm, signature)
+        satisfies ASN1Representable {
+    shared Integer version => nothing; // TODO
+    shared SignerIdentifier signerIdentifier;
+    shared AlgorithmIdentifier digestAlgorithm;
+    shared Set<Attribute> signedAttrs;
+    shared AlgorithmIdentifier signatureAlgorithm;
+    shared OctetString signature;
+    
+    assert(signedAttrs.size > 0);
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        ASN1Integer(wholeNumber(version)),
+        signerIdentifier.asn1,
+        digestAlgorithm.asn1,
+        ImplicitTag(0, ASN1Set(LazySet(signedAttrs.map((Attribute elem) => elem.asn1)))),
+        signatureAlgorithm.asn1,
+        signature
+    ]);
+}
+
+abstract class SignerIdentifier()
+        of SubjectKeyIdentifier
+        satisfies ASN1Representable {}
+
+class SubjectKeyIdentifier(identifier)
+        extends SignerIdentifier() {
+    shared OctetString identifier;
+
+    shared actual ExplicitTag asn1 => ExplicitTag(0, identifier);
+}
+
+class Attribute(type, values) satisfies ASN1Representable {
+    shared ObjectIdentifier type;
+    shared Set<ASN1Encodable> values;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        type,
+        ASN1Set(values)
+    ]);
+}
+
+class EnvelopedData(recipientInfos, encryptedContentInfo)
+        extends ContentInfo(envelopedDataContentType) {
+    shared Integer version => nothing;
+    shared Set<RecipientInfo> recipientInfos;
+    shared EncryptedContentInfo encryptedContentInfo;
+    
+    assert(recipientInfos.size > 0);
+    
+    shared actual ASN1Sequence content => ASN1Sequence([
+        ASN1Integer(wholeNumber(version)),
+        ASN1Set(LazySet(recipientInfos.map((RecipientInfo elem) => elem.asn1))),
+        encryptedContentInfo.asn1
+    ]);
+}
+
+abstract class RecipientInfo()
+        of KEKRecipientInfo
+        satisfies ASN1Representable {}
+
+class KEKRecipientInfo(kekIdentifier, keyEncryptionAlgorithm, encryptedKey)
+        extends RecipientInfo() {
+    shared Integer version = 4;
+    shared KEKIdentifier kekIdentifier;
+    shared AlgorithmIdentifier keyEncryptionAlgorithm;
+    shared OctetString encryptedKey;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        ASN1Integer(wholeNumber(version)),
+        kekIdentifier.asn1,
+        keyEncryptionAlgorithm.asn1,
+        encryptedKey
+    ]);
+}
+
+class KEKIdentifier(keyIdentifier)
+        satisfies ASN1Representable {
+    shared OctetString keyIdentifier;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([keyIdentifier]);
+}
+
+ContentType checkContentType(ObjectIdentifier identifier) {
+    if (dataContentType == identifier) {
+        return dataContentType;
+    } else if (signedDataContentType == identifier) {
+        return signedDataContentType;
+    } else if (envelopedDataContentType == identifier) {
+        return envelopedDataContentType;
+    } else {
+        throw Exception("Found non-content-type identifier " + identifier.string);
+    }
+}
+
+EncapsulatedContentInfo asn1EncapsulatedContentInfo(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 2);
+    assert(is ObjectIdentifier identifier = asn1.sequence.first);
+    assert(is ExplicitTag wrappedEncContent = asn1.sequence.last);
+    assert(wrappedEncContent.tag == 0);
+    assert(is OctetString encContentString = wrappedEncContent.wrappedObject);
+    assert(nonempty encContent = encContentString.bytes);
+    value content = asn1Decodable(encContent);
+    
+    return EncapsulatedContentInfo(createContentInfo(identifier, content));
+}
+
+SignerIdentifier asn1SignerIdentifier(ExplicitTag tag) {
+    assert(tag.contextTag == 0);
+    assert(is OctetString identifier = tag.wrappedObject);
+    return SubjectKeyIdentifier(identifier);
+}
+
+Attribute asn1Attribute(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 2);
+    assert(is ObjectIdentifier type = asn1.sequence.first);
+    assert(is ASN1Set values = asn1.sequence[1]);
+    return Attribute(type, values.set);
+}
+
+SignerInfo asn1SignerInfo(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 6);
+    assert(is ASN1Integer version = asn1.sequence.first);
+    
+    assert(is ExplicitTag wrappedSignerIdentifier = asn1.sequence[1]);
+    value signerIdentifier = asn1SignerIdentifier(wrappedSignerIdentifier);
+    
+    assert(is ASN1Sequence encDigestAlgorithm = asn1.sequence[2]);
+    value digestAlgorithm = asn1AlgorithmIdentifier(encDigestAlgorithm);
+    
+    assert(is OpaqueImplicitTag wrappedSignedAttrs = asn1.sequence[3]);
+    assert(is ASN1Set encSignedAttrs = wrappedSignedAttrs.decodeAs(asn1SetType));
+    value signedAttrs = LazySet(encSignedAttrs.set.map((ASN1Encodable elem) {
+        assert(is ASN1Sequence elem);
+        return asn1Attribute(elem);
+    }));
+    
+    assert(is ASN1Sequence encSignatureAlgorithm = asn1.sequence[4]);
+    value signatureAlgorithm = asn1AlgorithmIdentifier(encSignatureAlgorithm);
+    
+    assert(is OctetString signature = asn1.sequence[5]);
+    
+    return SignerInfo(signerIdentifier, digestAlgorithm, signedAttrs, signatureAlgorithm, signature);
+}
+
+SignedData asn1SignedData(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 4);
+    assert(is ASN1Integer version = asn1.sequence.first);
+    
+    assert(is ASN1Set encAlgorithms = asn1.sequence[1]);
+    value digestAlgorithms = LazySet(encAlgorithms.set.map((ASN1Encodable elem) {
+        assert(is ASN1Sequence elem);
+        return asn1AlgorithmIdentifier(elem);
+    }));
+    
+    assert(is ASN1Sequence encEncapContentInfo = asn1.sequence[3]);
+    value encapContentInfo = asn1EncapsulatedContentInfo(encEncapContentInfo);
+    
+    assert(is ASN1Set encSignerInfos = asn1.sequence[4]);
+    value signerInfos = LazySet(encSignerInfos.set.map((ASN1Encodable elem) {
+        assert(is ASN1Sequence elem);
+        return asn1SignerInfo(elem);
+    }));
+
+    return SignedData(digestAlgorithms, encapContentInfo, signerInfos);
+}
+
+KEKIdentifier asn1KekIdentifier(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 1);
+    assert(is OctetString keyIdentifier = asn1.sequence.first);
+    
+    return KEKIdentifier(keyIdentifier);
+}
+
+RecipientInfo asn1RecipientInfo(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 4);
+    assert(is ASN1Integer version = asn1.sequence.first);
+    assert(version.whole == wholeNumber(4));
+    
+    assert(is ASN1Sequence encKekIdentifier = asn1.sequence[1]);
+    value kekIdentifier = asn1KekIdentifier(encKekIdentifier);
+    
+    assert(is ASN1Sequence enckeyEncryptionAlgorithm = asn1.sequence[2]);
+    value keyEncryptionAlgorithm = asn1AlgorithmIdentifier(enckeyEncryptionAlgorithm);
+    
+    assert(is OctetString encryptedKey = asn1.sequence[3]);
+    
+    return KEKRecipientInfo(kekIdentifier, keyEncryptionAlgorithm, encryptedKey);
+}
+
+EncryptedContentInfo asn1EncryptedContentInfo(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 3);
+    
+    assert(is ObjectIdentifier identifier = asn1.sequence.first);
+    value contentType = checkContentType(identifier);
+    
+    assert(is ASN1Sequence encEncryptionAlgorithm = asn1.sequence[1]);
+    value encryptionAlgorithm = asn1AlgorithmIdentifier(encEncryptionAlgorithm);
+    
+    assert(is OpaqueImplicitTag wrappedContent = asn1.sequence[2]);
+    assert(is OctetString content = wrappedContent.decodeAs(octetStringType));
+    
+    return EncryptedContentInfo(contentType, encryptionAlgorithm, content);
+}
+
+EnvelopedData asn1EnvelopedData(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 3);
+    assert(is ASN1Integer version = asn1.sequence.first);
+    
+    assert(is ASN1Set encRecipientInfos = asn1.sequence[1]);
+    value recipientInfos = LazySet(encRecipientInfos.set.map((ASN1Encodable elem) {
+        assert(is ASN1Sequence elem);
+        return asn1RecipientInfo(elem);
+    }));
+    
+    assert(is ASN1Sequence encEncryptedContentInfo = asn1.sequence[2]);
+    value encryptedContentInfo = asn1EncryptedContentInfo(encEncryptedContentInfo);
+    
+    return EnvelopedData(recipientInfos, encryptedContentInfo);
+}
+
+ContentInfo createContentInfo(ObjectIdentifier identifier, ASN1Representable content) {
+    switch (checkContentType(identifier))
+    case (dataContentType) {
+        return ArbitraryData(content);
+    }
+    case (signedDataContentType) {
+        assert(is ASN1Sequence content);
+        return asn1SignedData(content);
+    }
+    case (envelopedDataContentType) {
+        return nothing;
+    }
+}
+
+ContentInfo asn1ContentInfo(ASN1Sequence asn1) {
+    value seq = asn1.sequence;
+    assert(seq.size == 2);
+    assert(is ObjectIdentifier identifier = seq.first);
+    assert(is ExplicitTag tag = seq.last);
+    assert(tag.contextTag == 0);
+    value content = tag.wrappedObject;
+
+    return createContentInfo(identifier, content);
+}

asn1/ecpointconv.ceylon

+import ceylon.math.whole { Whole, wholeNumber, one }
+import toycrypto.math { NonInfinitePoint, Curve, testBit, InfinityPoint, Point, decodeWhole }
+
+// TODO: move to toycrypto.primitives
+
+// from org.bouncycastle.math.ec.ECCurve
+NonInfinitePoint decompressPoint(Curve curve, Integer yTilde, Whole x1) {
+    value x = curve.fieldElement(x1);
+    value alpha = x.times(x.power(wholeNumber(2)).plus(curve.coefficientA)).plus(curve.coefficientB);
+    value beta = alpha.power(one.negativeValue);
+    
+    if (testBit(beta.x, 0) == (yTilde == 1)) {
+        return NonInfinitePoint(curve, x, beta);
+    } else {
+        return NonInfinitePoint(curve, x, curve.fieldElement(curve.primeP.minus(beta.x)));
+    }
+}
+
+// from org.bouncycastle.math.ec.ECCurve
+shared Point decodePoint(Curve curve, [Integer+] encoded) {
+    value expectedLength = (curve.fieldSize + 7) / 8;
+    
+    if (encoded.first == #00) { // infinity
+        assert(encoded.size == 1);
+        return InfinityPoint(curve);
+    } else if (encoded.first == #02 || encoded.first == #03) { // compressed
+        assert(encoded.size == expectedLength + 1);
+        value yTilde = encoded.first.and(1);
+        value x1 = decodeWhole(encoded.rest);
+        return decompressPoint(curve, yTilde, x1);
+    } else if (encoded.first == #04 || // uncompressed
+            encoded.first == #06 || encoded.first == #07) { // hybrid
+        assert(encoded.size == 2 * expectedLength + 1);
+        value x1 = decodeWhole(encoded[1..expectedLength]);
+        value y1 = decodeWhole(encoded[expectedLength + 1...]);
+        return NonInfinitePoint(curve, curve.fieldElement(x1), curve.fieldElement(y1));
+    } else {
+        throw Exception("Invalid point encoding");
+    }
+}
+
+shared [Integer+] encodePoint(Point point) {
+    switch (point)
+    case (is InfinityPoint) {
+        return [1];
+    }
+    case (is NonInfinitePoint) {
+        return wholeToX9Bytes(point.x.x, point.x)
+            .chain(wholeToX9Bytes(point.y.x, point.x))
+            .sequence.withLeading(#04);
+    }
+}

asn1/package.ceylon

+shared package toycrypto.asn1;

asn1/parse.ceylon

+import ceylon.io.buffer { ByteBuffer, newByteBufferWithData }
+import toycrypto.util { takeUntil }
+
+// Taken from org.bouncycastle.asn1.ASN1InputStream
+
+shared ASN1Decodable asn1Decodable([Integer+]|ByteBuffer bytes) {
+    if (is ByteBuffer bytes) {
+        return parseASN1Buffer(bytes);
+    } else {
+        return parseASN1Buffer(newByteBufferWithData(*bytes));
+    }
+}
+
+Integer readContextTag(ByteBuffer bytes, Integer tag) {
+    value tagNo = tag.and(#1f);
+    // with tagged object tag number is bottom 5 bits, or stored at the start of the content
+    if (tagNo == #1f) {
+        value b = bytes.get();
+        
+        // X.690-0207 8.1.2.4.2
+        // "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
+        if (b.and(#7f) == 0) { // Note: -1 will pass
+            throw Exception("corrupted stream - invalid high tag number found");
+        }
+        
+        // TODO: find out why this is necessary
+        value init_ = takeUntil(bytes.following(b), (Integer byte) => byte.and(#80) == 0);
+        value init = init_
+            .fold(0, (Integer partial, Integer byte) => partial.or(byte.and(#7f)).leftLogicalShift(7));
+        bytes.position -= 1;
+        value final = bytes.get();
+        doc "make sure buffer didn't stop inside tag value"
+        assert(final.and(#80) == 0);
+        return init.or(final.and(#7f));
+    } else {
+        return tagNo;
+    }
+//        if (tagNo == 0x1f)
+//        {
+//            tagNo = 0;
+//
+//            int b = s.read();
+//
+//            // X.690-0207 8.1.2.4.2
+//            // "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
+//            if ((b & 0x7f) == 0) // Note: -1 will pass
+//            {
+//                throw new IOException("corrupted stream - invalid high tag number found");
+//            }
+//
+//            while ((b >= 0) && ((b & 0x80) != 0))
+//            {
+//                tagNo |= (b & 0x7f);
+//                tagNo <<= 7;
+//                b = s.read();
+//            }
+//
+//            if (b < 0)
+//            {
+//                throw new EOFException("EOF found inside tag value.");
+//            }
+//            
+//            tagNo |= (b & 0x7f);
+//        }
+//        
+//        return tagNo;
+}
+
+Integer readSize(ByteBuffer bytes, Integer max) {
+    value sizeByte = bytes.get();
+    
+    if (sizeByte == #80) {
+        return -1; // indefinite-length encoding
+    } else if (sizeByte <= 127) {
+        doc "Size must be within bounds of available bytes."
+        assert(sizeByte < max);
+        return sizeByte;
+    } else {
+        value sizeSize = sizeByte.and(#7f);
+        
+        // Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
+        if (sizeSize > 4) {
+            throw Exception("DER length more than 4 bytes: " + sizeSize.string);
+        }
+        
+        value size = (0:sizeSize).fold(0, (Integer partial, Integer i) =>
+            partial.leftLogicalShift(8) + bytes.get());
+        assert(size >= 0);
+        doc "Size must be within bounds of available bytes."
+        assert(size < max - sizeSize);
+        return size;
+    }
+}
+
+Result withBufferSegment<Result>(Integer tag, Integer size, ByteBuffer bytes,
+        Result(Integer, ByteBuffer) func) {
+    value currentLimit = bytes.limit;
+    bytes.limit = bytes.position + size;
+    value result = func(tag, bytes);
+    bytes.limit = currentLimit;
+    return result;
+}
+
+// TODO: find out why bytes are so weird
+[Integer*] getBytes(ByteBuffer bytes, Integer count) =>
+    bytes.taking(count).collect((Integer byte) => byte > 127 then byte - 256 else byte);
+
+ASN1Decodable parseASN1Buffer(ByteBuffer bytes) {
+    value tag = bytes.get();
+    if (tag == 0) {
+        throw Exception("unexpected end-of-contents marker");
+    }
+    
+    value size = readSize(bytes, bytes.available);
+    doc "Indefinite sizes are unsupported."
+    assert(size >= 0);
+    
+    doc "Application specific objects are unsupported."
+    assert(tag.and(derTags.application) == 0);
+    
+    if (exists type = asn1DecodableType(tag)) {
+        return withBufferSegment(tag, size, bytes, type.parse);
+    } else {
+        doc "Externals are unsupported."
+        assert(tag != derTags.external.and(derTags.constructed));
+
+        doc "BMP strings are unsupported."
+        assert(tag != derTags.bmpString);
+        doc "Booleans are unsupported."
+        assert(tag != derTags.boolean);
+        doc "Enumerations are unsupported."
+        assert(tag != derTags.enumerated);
+        doc "Generalised times are unsupported."
+        assert(tag != derTags.generalisedTime);
+        doc "General strings are unsupported."
+        assert(tag != derTags.generalString);
+        doc "IA5 strings are unsupported."
+        assert(tag != derTags.ia5String);
+        doc "Nulls are unsupported."
+        assert(tag != derTags.null);
+        doc "Numeric strings are unsupported."
+        assert(tag != derTags.numericString);
+        doc "Printable strings are unsupported."
+        assert(tag != derTags.printableString);
+        doc "T61 strings are unsupported."
+        assert(tag != derTags.t61String);
+        doc "Universal strings are unsupported."
+        assert(tag != derTags.universalString);
+        doc "UTC times are unsupported."
+        assert(tag != derTags.utcTime);
+        doc "UTF8 strings are unsupported."
+        assert(tag != derTags.utf8String);
+        doc "Visible strings are unsupported."
+        assert(tag != derTags.visibleString);
+        
+        throw Exception("Unknown ASN1 DER tag ``tag`` encountered");
+    }
+}
+
+[Element*] readSequential<Element>(ByteBuffer bytes, Element(ByteBuffer) read) {
+    variable [Element*] seq = [];
+    while (bytes.hasAvailable) {
+        seq = seq.withTrailing(read(bytes));
+    }
+    return seq;
+}
+
+ObjectIdentifier createObjectIdentifier(ByteBuffer bytes) {
+    Integer readField(ByteBuffer bytes) {
+        // TODO: find out why this is necessary
+        value init_ = takeUntil(bytes, (Integer byte) => byte.and(#80) == 0);
+        value init = init_
+            .fold(0, (Integer partial, Integer byte) => (partial + byte.and(#7f)).leftLogicalShift(7));
+        bytes.position -= 1;
+        return init + bytes.get().and(#7f);
+    }
+    
+    value firstAndSecond = readField(bytes);
+    Integer first;
+    Integer second;
+    if (firstAndSecond < 40) {
+        first = 0;
+        second = firstAndSecond;
+    } else if (firstAndSecond < 80) {
+        first = 1;
+        second = firstAndSecond - 40;
+    } else {
+        first = 2;
+        second = firstAndSecond - 80;
+    }
+    
+    return ObjectIdentifier(readSequential(bytes, readField)
+        .withLeading(second).withLeading(first));
+//        for (int i = 0; i != bytes.length; i++)
+//        {
+//            int b = bytes[i] & 0xff;
+//
+//                value += (b & 0x7f);
+//                if ((b & 0x80) == 0)             // end of number reached
+//                {
+//                    if (first)
+//                    {
+//                        if (value < 40)
+//                        {
+//                            objId.append('0');
+//                        }
+//                        else if (value < 80)
+//                        {
+//                            objId.append('1');
+//                            value -= 40;
+//                        }
+//                        else
+//                        {
+//                            objId.append('2');
+//                            value -= 80;
+//                        }
+//                        first = false;
+//                    }
+//
+//                    objId.append('.');
+//                    objId.append(value);
+//                    value = 0;
+//                }
+//                else
+//                {
+//                    value <<= 7;
+//                }
+//        }
+}
+[Integer+] wrap(String message) {
+    value data = OctetString(message.characters.collect((Character elem) => elem.integer));
+    return ArbitraryData(data).asn1.encoded;
+}
+
+String unwrap([Integer+] bytes) {
+    value container = asn1Decodable(bytes); 
+    assert(is ASN1Sequence container);
+    assert(is ArbitraryData contentInfo = asn1ContentInfo(container));
+    assert(is OctetString codepoints = contentInfo.content);
+    return string(codepoints.bytes.collect((Integer element) => element.character));
+}
+
+doc "Run the module `toycrypto`."
+void run() {
+    value message = "Hello world!";
+    value container = wrap(message);
+    assert(message == unwrap(container));
+}
+import toycrypto.math { Domain, Point }
+
+shared class AlgorithmIdentifier(identifier, params)
+        satisfies ASN1Representable {
+    shared ObjectIdentifier identifier;
+    shared ASN1Representable params;
+
+    shared actual ASN1BasicObject asn1 => ASN1Sequence([identifier, params.asn1]);
+}
+
+shared AlgorithmIdentifier asn1AlgorithmIdentifier(ASN1Sequence seq) {
+    assert(seq.sequence.size == 2);
+    assert(is ObjectIdentifier identifier = seq.sequence.first);
+    assert(is ASN1Representable params = seq.sequence[1]);
+    return AlgorithmIdentifier(identifier, params);
+}
+
+shared class SubjectPublicKeyInfo(algorithmIdentifier, keyData)
+        satisfies ASN1Representable {
+    shared AlgorithmIdentifier algorithmIdentifier;
+    shared [Integer+] keyData;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        algorithmIdentifier.asn1,
+        BitString(keyData)
+    ]);
+}
+
+shared SubjectPublicKeyInfo asn1SubjectPublicKeyInfo(ASN1Sequence seq) {
+    assert(seq.sequence.size == 2);
+    
+    assert(is ASN1Sequence algorithmIdentifierSeq = seq.sequence.first);
+    value algorithmIdentifier = asn1AlgorithmIdentifier(algorithmIdentifierSeq);
+    
+    assert(is BitString keyDataBitString = seq.sequence[1]);
+    assert(nonempty keyData = keyDataBitString.bytes);
+    
+    return SubjectPublicKeyInfo(algorithmIdentifier, keyData);
+}
+
+shared class ECPublicKeyInfo(domain, q)
+        extends SubjectPublicKeyInfo(
+            AlgorithmIdentifier(x9Identifiers.ecPublicKey, X9ECParameters(domain)),
+            encodePoint(q)) {
+    shared Domain domain;
+    shared Point q;
+}
+
+shared ECPublicKeyInfo asn1ECPublicKeyInfo(ASN1Sequence seq) {
+    value keyInfo = asn1SubjectPublicKeyInfo(seq);
+    assert(keyInfo.algorithmIdentifier.identifier == x9Identifiers.ecPublicKey);
+    assert(is ASN1Sequence paramSeq = keyInfo.algorithmIdentifier.params);
+    
+    value domain = asn1X9ECParameters(paramSeq).domain;
+    return ECPublicKeyInfo(domain, decodePoint(domain.curve, keyInfo.keyData));
+}
+import toycrypto.math { Domain, Curve, FieldElement, PrimeFieldConstraint, encodeWhole, Point, decodeWhole }
+import ceylon.math.whole { Whole, one }
+import toycrypto.util { forever }
+
+shared object x9Identifiers {
+    shared ObjectIdentifier ansiX9_62 = ObjectIdentifier([1, 2, 840, 10045]);
+    
+    shared ObjectIdentifier fieldType = ObjectIdentifier(ansiX9_62.sequence.withTrailing(1));
+    shared ObjectIdentifier primeField = ObjectIdentifier(fieldType.sequence.withTrailing(1));
+    
+    shared ObjectIdentifier publicKeyType = ObjectIdentifier(ansiX9_62.sequence.withTrailing(2));
+    shared ObjectIdentifier ecPublicKey = ObjectIdentifier(publicKeyType.sequence.withTrailing(1));
+}
+
+// from package org.bouncycastle.asn1.x9.X9IntegerConverter
+shared [Integer+] wholeToX9Bytes(Whole whole, PrimeFieldConstraint fieldConstraint) {
+    value bytes = encodeWhole(whole);
+    value sizeConstraint = (fieldConstraint.fieldSize + 7) / 8;
+    if (sizeConstraint < bytes.size) {
+        assert(nonempty span = bytes[bytes.size - sizeConstraint...]);
+        return span;
+    } else {
+        {Integer*} zeroes = forever(() => 0).taking(sizeConstraint - bytes.size);
+        assert(nonempty seq = zeroes.chain(bytes).sequence);
+        return seq;
+    }
+}
+
+shared class X9FieldId(primeP) satisfies ASN1Representable {
+    shared Whole primeP;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        x9Identifiers.primeField,
+        ASN1Integer(primeP)
+    ]);
+}
+
+shared X9FieldId asn1X9FieldId(ASN1Sequence seq) {
+    assert(seq.sequence.size == 2);
+    assert(is ObjectIdentifier identifier = seq.sequence.first);
+    assert(identifier == x9Identifiers.primeField);
+    assert(is ASN1Integer primeP = seq.sequence[1]);
+    return X9FieldId(primeP.whole);
+}
+
+shared class X9FieldElement(fieldElement) satisfies ASN1Representable {
+    shared FieldElement fieldElement;
+    
+    shared actual OctetString asn1 => OctetString(wholeToX9Bytes(fieldElement.x, fieldElement));
+}
+
+shared X9FieldElement asn1X9FieldElement(Whole primeP, OctetString octetString) {
+    return X9FieldElement(FieldElement(primeP, decodeWhole(octetString.bytes)));
+}
+
+shared class X9Curve(curve) satisfies ASN1Representable {
+    shared Curve curve;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        X9FieldElement(curve.coefficientA).asn1,
+        X9FieldElement(curve.coefficientB).asn1
+    ]);
+}
+
+shared X9Curve asn1X9Curve(Whole primeP, ASN1Sequence seq) {
+    assert(seq.sequence.size == 2);
+    assert(is OctetString coefficientAString = seq.sequence.first);
+    assert(is OctetString coefficientBString = seq.sequence[1]);
+    return X9Curve(Curve(
+        primeP,
+        asn1X9FieldElement(primeP, coefficientAString).fieldElement,
+        asn1X9FieldElement(primeP, coefficientBString).fieldElement
+    ));
+}
+
+shared class X9ECPoint(point) satisfies ASN1Representable {
+    shared Point point;
+    
+    shared actual OctetString asn1 => OctetString(encodePoint(point));
+}
+
+shared X9ECPoint asn1X9ECPoint(Curve curve, OctetString octetString) {
+    assert(nonempty bytes = octetString.bytes);
+    return X9ECPoint(decodePoint(curve, bytes));
+}
+
+shared class X9ECParameters(domain) satisfies ASN1Representable {
+    shared Domain domain;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        ASN1Integer(one),
+        X9FieldId(domain.curve.primeP).asn1,
+        X9Curve(domain.curve).asn1,
+        X9ECPoint(domain.basepointG).asn1,
+        ASN1Integer(domain.nOrderOfBasepointG),
+        ASN1Integer(domain.cofactorH)
+    ]);
+}
+
+shared X9ECParameters asn1X9ECParameters(ASN1Sequence seq) {
+    assert(seq.sequence.size == 6);
+    
+    assert(is ASN1Integer shouldBeOne = seq.sequence.first);
+    assert(shouldBeOne.whole == one);
+    
+    assert(is ASN1Sequence fieldIdSeq = seq.sequence[1]);
+    value primeP = asn1X9FieldId(fieldIdSeq).primeP;
+    
+    assert(is ASN1Sequence curveSeq = seq.sequence[2]);
+    value curve = asn1X9Curve(primeP, curveSeq).curve;
+    
+    assert(is OctetString pointOctetString = seq.sequence[3]);
+    value basepointG = asn1X9ECPoint(curve, pointOctetString).point;
+    
+    assert(is ASN1Integer nOrderOfBasepointG = seq.sequence[4]);
+    assert(is ASN1Integer cofactorH = seq.sequence[5]);
+    
+    return X9ECParameters(Domain(curve, basepointG, nOrderOfBasepointG.whole,
+        cofactorH.whole));
+}
+import toycrypto.primitives { randomBytes, newKeysFromSharedSecret, randomKeyParts, ECDHKeyParts, combineToSharedSecret }
+import toycrypto.math { Point, Domain }
+import toycrypto.keys { KeySpec, SharedKey }
+import toycrypto.asn1 { ECPublicKeyInfo, asn1ECPublicKeyInfo, ASN1Sequence, asn1Decodable, OctetString, ASN1Representable }
+import toycrypto.util { compareSequences }
+
+// TODO: determine good nonce length by consulting RFCs
+shared Integer nonceByteLength = 24;
+
+shared abstract class EphemeralKey([Integer+] sharedSecret, [Integer+] sharedNonce) {
+    value keys = newKeysFromSharedSecret {
+        keySpecs = [KeySpec(256 / 8, "AES"), KeySpec(256 / 8, "HmacSHA1")];
+        sharedSecret = sharedSecret;
+        info = "EphemeralKey";
+        sharedNonce = sharedNonce;
+    };
+
+    shared SharedKey encryptionKey = keys[0];
+    assert(exists _hmacKey = keys[1]);
+    shared SharedKey hmacKey = _hmacKey;
+    
+    shared actual Boolean equals(Object that) {
+        if (is EphemeralKey that) {
+            return encryptionKey == that.encryptionKey &&
+                   hmacKey == that.hmacKey;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [encryptionKey.hash, hmacKey.hash].hash;
+}
+
+shared abstract class EphemeralSharedPart() satisfies ASN1Representable {
+    shared formal Domain domain;
+    shared formal Point q;
+    shared formal [Integer+] nonce;
+    
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        OctetString(nonce),
+        ECPublicKeyInfo(domain, q).asn1
+    ]);
+    
+    shared [Integer+] encoded => asn1.encoded;
+    
+    shared actual Boolean equals(Object that) {
+        if (is EphemeralSharedPart that) {
+            return q == that.q &&
+                   nonce == that.nonce;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [q.hash, nonce.hash].hash;
+}
+
+shared class IncompleteEphemeralKey() {
+    ECDHKeyParts localKeyParts = randomKeyParts();
+
+    shared object sharedPart extends EphemeralSharedPart() {
+        shared actual Domain domain = localKeyParts.domain;
+        shared actual Point q = localKeyParts.q;
+        assert(nonempty nonce = randomBytes(nonceByteLength));
+        shared actual [Integer+] nonce = nonce;
+    }
+
+    shared EphemeralKey complete(EphemeralSharedPart remoteSharedPart) {
+        assert(remoteSharedPart.domain == localKeyParts.domain);
+        value sharedSecret = combineToSharedSecret(localKeyParts, remoteSharedPart.q);
+        
+        assert(nonempty sharedNonce = join(*
+            [sharedPart.nonce, remoteSharedPart.nonce].sort(compareSequences<Integer>)));
+
+        object key extends EphemeralKey(sharedSecret, sharedNonce) {}
+        return key;
+    }
+}
+
+shared EphemeralSharedPart decodeSharedPart([Integer+] encodedSharedPart) {
+    assert(is ASN1Sequence seq = asn1Decodable(encodedSharedPart));
+    assert(seq.sequence.size == 2);
+
+    assert(is OctetString nonceString = seq.sequence.first);
+
+    assert(is ASN1Sequence keyInfoSeq = seq.sequence[1]);
+    value keyInfo = asn1ECPublicKeyInfo(keyInfoSeq);
+
+    object result extends EphemeralSharedPart() {
+        shared actual Domain domain = keyInfo.domain;
+        shared actual Point q = keyInfo.q;
+        assert(nonempty nonce = nonceString.bytes);
+        shared actual [Integer+] nonce = nonce;
+    }
+    return result;
+}
+
+//Result doPreservingBuffer<Result, Element>(Buffer<Element> buffer, Result(Buffer<Element>) action) {
+//    Integer initialPosition = buffer.position;
+//    value result = action(buffer);
+//    buffer.position = initialPosition;
+//    return result;
+//}
+    // For some reason this stopped compiling:
+    //zip(first, second).map((Element->Element pair) => pair.key <=> pair.item).find(not(equal.equals)) else equal;
+
+//Boolean(Arg) not<Arg>(Boolean(Arg) func) => (Arg arg) => !func(arg);
+
+//Comparison compareArrays(ByteArray first, ByteArray second) =>
+//    first.array.indexed.map((Integer->Integer elem) => elem.item <=> second.get(elem.key)).find((Comparison elem) => elem != equal) else equal;

keys/interfaces.ceylon

+shared interface Encodable {
+    shared formal [Integer+] encoded;
+}
+
+shared abstract class KeyPrivacy(String name) of me|everyone|recipients {
+    shared actual String string => name;
+}
+
+shared object me extends KeyPrivacy("me") {}
+shared object everyone extends KeyPrivacy("everyone") {}
+shared object recipients extends KeyPrivacy("recipients") {}
+
+shared interface Key satisfies Encodable {
+    shared formal KeySpec keySpec;
+    shared formal KeyPrivacy privacy;
+}
+import javax.crypto { JavaSecretKey = SecretKey }
+import javax.crypto.spec { SecretKeySpec }
+import java.lang { arrays }
+
+shared class KeySpec(byteLength, algorithm) {
+    assert(byteLength > 0);
+    shared Integer byteLength;
+    shared String algorithm;
+}
+
+// TODO: move to module top-level
+shared class SharedKey(encoded, String algorithm)
+        satisfies Key {
+    shared JavaSecretKey javaKey = SecretKeySpec(arrays.toByteArray(encoded), algorithm);
+    
+    shared actual KeySpec keySpec => KeySpec(encoded.size, algorithm);
+    
+    shared actual [Integer+] encoded;
+    
+    shared actual KeyPrivacy privacy = recipients;
+    
+    shared actual Boolean equals(Object that) {
+        if (is SharedKey that) {
+            return javaKey == that.javaKey;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [javaKey.hash].hash;
+}

keys/package.ceylon

+shared package toycrypto.keys;
+import ceylon.math.whole { Whole, wholeNumber, zero, one }
+
+shared interface PrimeFieldConstraint {
+    shared formal Integer fieldSize;
+}
+
+shared class FieldElement(primeP, x)
+        satisfies Exponentiable<FieldElement, Whole> &
+                  PrimeFieldConstraint {
+    shared Whole x;
+    assert(!x.negative);
+    doc "Prime p of the curve this element is on."
+    shared Whole primeP;
+    assert(x < primeP);
+    
+    shared actual FieldElement negativeValue =>
+        FieldElement(primeP, positiveRemainder(x.negativeValue, primeP));
+    shared actual FieldElement positiveValue => this;
+    
+    shared actual FieldElement divided(FieldElement other) =>
+        FieldElement(primeP, x.times(other.x.powerRemainder(wholeNumber(-1), primeP)).remainder(primeP));
+    
+    shared actual FieldElement minus(FieldElement other) =>
+        FieldElement(primeP, positiveRemainder(x.minus(other.x), primeP));
+    
+    shared actual FieldElement plus(FieldElement other) =>
+        FieldElement(primeP, x.plus(other.x).remainder(primeP));
+    
+    shared actual FieldElement power(Whole exponent) {
+        if (exponent >= zero) {
+            return FieldElement(primeP, x.powerRemainder(exponent, primeP));
+        } else if (exponent == one.negativeValue) {
+            return FieldElement(primeP, sqrtModuloPrime(x, primeP));
+        } else {
+            throw Exception("Unsupported exponent " + exponent.string);
+        }
+    }
+    
+    shared actual FieldElement times(FieldElement other) =>
+        FieldElement(primeP, x.times(other.x).remainder(primeP));
+    
+    shared actual Boolean equals(Object that) {
+        if (is FieldElement that) {
+            return x == that.x && primeP == that.primeP;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [primeP.hash, x.hash].hash;
+    
+    shared actual Integer fieldSize => bitLength(primeP);
+}
+
+doc "Elliptic curve over F_p"
+shared class Curve(primeP, coefficientA, coefficientB)
+        satisfies PrimeFieldConstraint {
+    shared FieldElement coefficientA;
+    shared FieldElement coefficientB;
+    shared Whole primeP;
+    
+    shared FieldElement fieldElement(Whole x) => FieldElement(primeP, x);
+    
+    shared actual Integer fieldSize => bitLength(primeP);
+    
+    shared actual Boolean equals(Object that) {
+        if (is Curve that) {
+            return primeP == that.primeP &&
+                coefficientA == that.coefficientA &&
+                coefficientB == that.coefficientB;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [
+            coefficientA.hash, coefficientB.hash, primeP.hash
+        ].hash;
+}
+
+shared abstract class Point(curve)
+        of NonInfinitePoint|InfinityPoint
+        satisfies Invertable<Point> & Summable<Point> {
+    shared Curve curve;
+    shared formal Point times(Whole k);
+    shared formal Point twice();
+}
+
+shared class InfinityPoint(Curve curve)
+        extends Point(curve) {
+    shared actual InfinityPoint times(Whole k) => this;
+    
+    // Not sure if this is right: BC seems to cause NPE for -infinity
+    shared actual Point negativeValue => this;
+    shared actual Point positiveValue => this;
+
+    shared actual Point twice() => this;
+    shared actual Point plus(Point other) => other;
+    
+    shared actual Boolean equals(Object that) {
+        if (is InfinityPoint that) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => 4186587367;
+}
+
+shared class NonInfinitePoint(Curve curve, x, y)
+        extends Point(curve)
+        satisfies Invertable<NonInfinitePoint> {
+    shared FieldElement x;
+    shared FieldElement y;
+    
+    shared actual Point times(Whole k) {
+        assert(!k.negative);
+        if (k == zero) {
+            return InfinityPoint(curve);
+        }
+        
+        // TODO e = k.remainder(n); // n == order of p
+        value e = k;
+        value h = e.times(wholeNumber(3));
+        variable Point r = this;
+        
+        for (i in Range(bitLength(h) - 2, 1)) {
+            r = r.twice();
+            
+            value hBit = testBit(h, i);
+            value eBit = testBit(e, i);
+            
+            if (hBit != eBit) {
+                r = r.plus(hBit then this else negativeValue);
+            }
+        }
+        
+        return r;
+    }
+    
+    shared actual NonInfinitePoint negativeValue => NonInfinitePoint(curve, x, y.negativeValue);
+    shared actual NonInfinitePoint positiveValue => this;
+
+    shared actual Point twice() {
+        if (y.x == zero) {
+            // if y1 == 0, then (x1, y1) == (x1, -y1)
+            // and hence this = -this and thus 2(x1, y1) == infinity
+            return InfinityPoint(curve);
+        }
+        
+        value curveTwo = curve.fieldElement(two);
+        value curveThree = curve.fieldElement(wholeNumber(3));
+        value gamma = x.power(two).times(curveThree).plus(curve.coefficientA)
+            .divided(y.times(curveTwo));
+        
+        value x3 = gamma.power(two).minus(x.times(curveTwo));
+        value y3 = gamma.times(x.minus(x3)).minus(y);
+        
+        return NonInfinitePoint(curve, x3, y3);
+    }
+
+    shared actual Point plus(Point other) {
+        switch (other)
+        case (is InfinityPoint) {
+            return this;
+        } case (is NonInfinitePoint) {
+            // Check if b = this or b = -this
+            if (x == other.x) {
+                if (y == other.y) {
+                    // this = b, i.e. this must be doubled
+                    return this.twice();
+                } else {
+                    // this = -b, i.e. the result is the point at infinity
+                    return InfinityPoint(curve);
+                }
+            } else {
+                value gamma = other.y.minus(y).divided(other.x.minus(x));
+                
+                value x3 = gamma.power(two).minus(x).minus(other.x);
+                value y3 = gamma.times(x.minus(x3)).minus(y);
+                
+                return NonInfinitePoint(curve, x3, y3);
+            }
+        }
+    }
+    
+    shared actual Boolean equals(Object that) {
+        if (is NonInfinitePoint that) {
+            return x == that.x && y == that.y;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [x.hash, y.hash].hash;
+}
+
+shared class Domain(curve, basepointG, nOrderOfBasepointG, cofactorH) {
+    shared Curve curve;
+    shared Point basepointG;
+    doc "By definition, greater than zero."
+    shared Whole nOrderOfBasepointG;
+    shared Whole cofactorH;
+    
+    assert(nOrderOfBasepointG > zero);
+    
+    shared actual Boolean equals(Object that) {
+        if (is Domain that) {
+            return curve == that.curve &&
+                basepointG == that.basepointG &&
+                nOrderOfBasepointG == that.nOrderOfBasepointG &&
+                cofactorH == that.cofactorH;
+        } else {
+            return false;
+        }
+    }
+    
+    shared actual Integer hash => [
+            curve.hash, basepointG.hash, nOrderOfBasepointG.hash, cofactorH.hash
+        ].hash;
+}

math/package.ceylon

+shared package toycrypto.math;

math/whole.ceylon

+import ceylon.math.whole { Whole, fromImplementation, wholeNumber, one }
+import java.math { BigInteger }
+import java.lang { arrays }
+import toycrypto.primitives { randomWhole }
+import toycrypto.util { forever }
+
+doc "A `Whole` instance representing two."
+shared Whole two = wholeNumber(2);
+
+shared [Integer+] encodeWhole(Whole whole) {
+     assert(is BigInteger implementation = whole.implementation);
+     assert(nonempty bytes = implementation.toByteArray().array.sequence);
+     return bytes;
+}
+
+shared Whole decodeWhole([Integer*] encoded) {
+    return fromImplementation(BigInteger(1, arrays.toByteArray(encoded)));
+    //byte[] mag = new byte[length];
+    //System.arraycopy(buf, off, mag, 0, length);
+    //return new BigInteger(1, mag);
+}
+
+shared Integer bitLength(Whole whole) {
+     assert(is BigInteger implementation = whole.implementation);
+     return implementation.bitLength();
+    //return ceil(log2(whole.compare(zero) == smaller then -whole else whole.successor));
+}
+
+shared Boolean testBit(Whole whole, Integer bit) {
+     assert(is BigInteger implementation = whole.implementation);
+     return implementation.testBit(bit);
+}
+
+shared Integer lowestSetBitIdx(Whole whole) {
+    assert(is BigInteger implementation = whole.implementation);
+    return implementation.lowestSetBit;
+}
+
+shared Whole positiveRemainder(Whole x, Whole modulus) {
+    value remainder = x.remainder(modulus);
+    return remainder.negative then remainder + modulus else remainder;
+}
+
+doc "Calculate a square root modulo prime modulus.
+     The routine verifies that the calculation returns the right
+     value - if none exists it throws."
+// D.1.4 91
+// from org.bouncycastle.math.ec.ECFieldElement
+shared Whole sqrtModuloPrime(Whole x, Whole modulus) {
+    value four = wholeNumber(4);
+    
+    // p mod 4 == 3
+    if (testBit(modulus, 1)) {
+        // z = g^(u+1) + p, p = 4u + 3
+        value result = x.powerRemainder(modulus.divided(four).plus(one), modulus);
+        assert(result.powerRemainder(two, modulus) == x);
+        return result;
+    }
+    
+    // p mod 4 == 1
+    value modMinusOne = modulus.minus(one);
+    value legendreExponent = modMinusOne.divided(two);
+    assert(x.powerRemainder(legendreExponent, modulus) == one);
+    
+    value k = modMinusOne.divided(four).times(two).plus(one);
+
+    value fourX = x.times(four).remainder(modulus);
+    
+    while (true) {
+        assert(exists p = forever(() => randomWhole(bitLength(modulus)))
+            .find((Whole p) => p < modulus &&
+                p.power(two).minus(fourX).powerRemainder(legendreExponent, modulus) == modMinusOne));
+        value lucas = LucasSequence(modulus, p, x, k);
+        
+        if (lucas.v.powerRemainder(two, modulus) == fourX) {
+            Whole result;
+            
+            // Integer division by 2, mod q
+            if (testBit(lucas.v, 0)) {
+                result = lucas.v.plus(modulus).divided(two);
+            } else {
+                result = lucas.v.divided(two);
+            }
+
+            assert(result.powerRemainder(two, modulus) == x);
+            return result;
+        } else if (lucas.u == one || lucas.u == modMinusOne) {
+            continue;
+        } else {
+            break;
+        }
+    }
+    
+    throw Exception(x.string + " is not a perfect square modulo " + modulus.string);
+}
+
+class LucasSequence(Whole p, Whole bigP, Whole q, Whole k) {
+    value n = bitLength(k);
+    value s = lowestSetBitIdx(k);
+    
+    variable value uh = one;
+    variable value vl = two;
+    variable value vh = bigP;
+    variable value ql = one;
+    variable value qh = one;
+    
+    for (j in (n - 1)..(s + 1)) {
+        ql = ql.times(qh).remainder(p);
+        if (testBit(k, j)) {
+            qh = ql.times(q).remainder(p);
+            uh = uh.times(vh).remainder(p);
+            vl = positiveRemainder(vh.times(vl).minus(bigP.times(ql)), p);
+            vh = positiveRemainder(vh.times(vh).minus(qh.times(two)), p);
+        } else {
+            qh = ql;
+            uh = positiveRemainder(uh.times(vl).minus(ql), p);
+            vh = positiveRemainder(vh.times(vl).minus(bigP.times(ql)), p);
+            vl = positiveRemainder(vl.times(vl).minus(ql.times(two)), p);
+        }
+    }
+    
+    ql = ql.times(qh).remainder(p);
+    qh = ql.times(q).remainder(p);
+    uh = positiveRemainder(uh.times(vl).minus(ql), p);
+    vl = positiveRemainder(vh.times(vl).minus(bigP.times(ql)), p);
+    ql = ql.times(qh).remainder(p);
+    
+    if (s != 0) {
+        for (j in 1..s) {
+            uh = uh.times(vl).remainder(p);
+            vl = positiveRemainder(vl.times(vl).minus(ql.times(two)), p);
+            ql = ql.times(ql).remainder(p);
+        }
+    }
+    
+    shared Whole u = uh;
+    shared Whole v = vl;
+}
+module toycrypto '0.1.0' {
+    shared import ceylon.io '0.5';
+    shared import ceylon.math '0.5';
+    import org.bouncycastle '1.49';
+    shared import java.base '7';
+}
+shared package toycrypto;

primitives/ecdh.ceylon

+import java.security { SecureRandom }
+import ceylon.math.whole { Whole, parseWhole, one, wholeNumber }
+import toycrypto.math { Domain, Curve, FieldElement, Point, bitLength, NonInfinitePoint, two }
+import toycrypto.asn1 { decodePoint, wholeToX9Bytes }
+import toycrypto.util { decodeLowerHex }
+
+// from org.bouncycastle.asn1.x9.X962NamedCurves
+Domain prime192v1() {
+    assert(exists p = parseWhole("6277101735386680763835789423207666416083908700390324961279"));
+    // fffffffffffffffffffffffffffffffefffffffffffffffc
+    assert(exists a = parseWhole("6277101735386680763835789423207666416083908700390324961276"));
+    // 64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
+    assert(exists b = parseWhole("2455155546008943817740293915197451784769108058161191238065"));
+    
+    value curve = Curve(p, FieldElement(p, a), FieldElement(p, b));
+    
+    assert(nonempty gString = "03188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012".sequence);
+    value g = decodePoint(curve, decodeLowerHex(gString));
+    // ffffffffffffffffffffffff99def836146bc9b1b4d22831
+    assert(exists n = parseWhole("6277101735386680763835789423176059013767194773182842284081"));
+    
+    return Domain {
+        curve = curve;
+        basepointG = g;
+        nOrderOfBasepointG = n;
+        cofactorH = one;
+    }; // TODO? seed from org.bouncycastle.asn1.x9.X962NamedCurves
+}
+
+shared class ECDHKeyParts(q, d, domain = prime192v1()) {
+    shared Domain domain;
+    shared Point q;
+    shared Whole d;
+}
+
+shared ECDHKeyParts randomKeyParts(Domain domain = prime192v1(),
+        SecureRandom randomSource = SecureRandom()) {
+    value high = two.power(wholeNumber(bitLength(domain.nOrderOfBasepointG)))
+        - one; // since a binary number n bits long has a max of 2^n - 1
+    value d = randomWholeInRange(domain.nOrderOfBasepointG, high, randomSource);
+    
+    //assert(exists d = forever(() => randomWhole(nBitLength, randomSource))
+    //    .find((Whole d) => d != zero && d >= domain.nOrderOfBasepointG));
+    
+    value q = domain.basepointG.times(d);
+    
+    return ECDHKeyParts(q, d, domain);
+}
+
+shared [Integer+] combineToSharedSecret(ECDHKeyParts localKeyParts, Point remoteQ) {
+    assert(is NonInfinitePoint p = remoteQ.times(localKeyParts.d));
+    assert(is NonInfinitePoint domainBasepointG = localKeyParts.domain.basepointG);
+    return wholeToX9Bytes(p.x.x, domainBasepointG.x);
+}

primitives/keys.ceylon

+import java.lang { arrays }
+import org.bouncycastle.crypto.generators { HKDFBytesGenerator }
+import org.bouncycastle.crypto.digests { WhirlpoolDigest }
+import org.bouncycastle.crypto.params { HKDFParameters }
+import ceylon.io.buffer { newByteBuffer }
+import toycrypto.keys { SharedKey, KeySpec }
+import toycrypto.util { codepointsOf }
+
+// Create keys from the shared secret as in RFC 5996
+// (https://tools.ietf.org/html/rfc5996#section-2.13)
+shared [SharedKey+] newKeysFromSharedSecret([KeySpec+] keySpecs, [Integer+] sharedSecret, String info, [Integer+] sharedNonce = nothing) {
+    value concatenatedKeys = newByteBuffer(keySpecs.fold(0, (Integer partial, KeySpec elem) => partial + elem.byteLength));
+    value hkdf = HKDFBytesGenerator(WhirlpoolDigest());
+    hkdf.init(HKDFParameters(arrays.toByteArray(sharedSecret),
+        !sharedNonce.empty then arrays.toByteArray(sharedNonce) else null,
+        arrays.toByteArray(codepointsOf(info))));
+    hkdf.generateBytes(arrays.asByteArray(concatenatedKeys.bytes()), 0, concatenatedKeys.size);
+    concatenatedKeys.flip();
+    return keySpecs.collect((KeySpec spec) {
+        assert(nonempty keyBytes = concatenatedKeys.taking(spec.byteLength).sequence);
+        return SharedKey(keyBytes, spec.algorithm);
+    });
+}

primitives/package.ceylon

+shared package toycrypto.primitives;

primitives/random.ceylon

+import java.lang { ByteArray }
+import java.security { SecureRandom }
+import java.math { BigInteger }
+import ceylon.math.whole { Whole, fromImplementation, one }
+import toycrypto.math { bitLength }
+import toycrypto.util { forever }
+
+shared [Integer*] randomBytes(Integer length, SecureRandom randomSource = SecureRandom()) {
+    if (length == 0) {
+        return [];
+    } else {
+        value buf = ByteArray(length);
+        randomSource.nextBytes(buf);
+        return buf.array.sequence;
+    }
+}
+
+doc "Constructs a randomly generated `Whole`, uniformly distributed over the
+     range 0 to (2^bitLength - 1), inclusive."
+shared Whole randomWhole(Integer bitLength, SecureRandom randomSource = SecureRandom()) {
+    return fromImplementation(BigInteger(bitLength, randomSource));
+}
+
+doc "Constructs a randomly generated `Whole`, uniformly distributed over the
+     range `low` to `high`, inclusive."
+shared Whole randomWholeInRange(Whole low, Whole high,
+        SecureRandom randomSource = SecureRandom()) {
+    assert(high > low);
+    value space = high - low + one; // inclusive
+    value spaceBitLength = bitLength(space);
+    assert(exists pointInSpace = forever(() => randomWhole(spaceBitLength, randomSource))
+        .find((Whole d) => d <= space));
+    return pointInSpace + low;
+}

test/package.ceylon

+package toycrypto.test;
+import ceylon.math.whole { wholeNumber }
+
+import java.lang { arrays, ByteArray }
+import java.math { BigInteger { bigInteger = valueOf } }
+import java.security { Security { addSecurityProvider = addProvider }, KeyFactory { fetchKeyFactory = getInstance } }
+import java.security.spec { X509EncodedKeySpec }
+
+import org.bouncycastle.asn1 { JavaASN1Seq = ASN1Sequence { toASN1Sequence = getInstance }, ASN1Primitive { parsePrimitive = fromByteArray }, ASN1ObjectIdentifier, JavaASN1Integer = ASN1Integer }
+import org.bouncycastle.asn1.x509 { JavaSubjectPublicKeyInfo = SubjectPublicKeyInfo { toSubjectPublicKeyInfo = getInstance } }
+import org.bouncycastle.asn1.x9 { X9ObjectIdentifiers { id_ecPublicKey } }
+import org.bouncycastle.jce.provider { BouncyCastleProvider }
+import org.bouncycastle.math.ec { ECFieldElement { Fp } }
+
+import toycrypto { CipherText, IncompleteEphemeralKey, encrypt, decrypt, EphemeralKey, EphemeralSharedPart, decodeSharedPart }
+import toycrypto.asn1 { X9ECParameters, ASN1Representable, x9Identifiers, AlgorithmIdentifier, BitString, encodePoint, SubjectPublicKeyInfo, ASN1Sequence, asn1Decodable, ASN1Integer, X9FieldId, X9Curve, X9ECPoint }
+import toycrypto.keys { SharedKey }
+import toycrypto.math { sqrtModuloPrime, two, encodeWhole, decodeWhole }
+import toycrypto.primitives { randomKeyParts }
+import toycrypto.util { codepointsOf, asCodepoints, forever, takeUntil, iterate }
+
+doc "Run the tests for the module `toycrypto`."
+void run() {
+    addSecurityProvider(BouncyCastleProvider());
+    whole();
+    sqrt();
+    keyParts();
+    value key = ecdh().encryptionKey;
+    writeField();
+    encodeSharedPart();
+    aes(key);
+}
+
+void whole() {
+    value w = wholeNumber(512);
+    value bytes = encodeWhole(w);
+    assert(decodeWhole(bytes) == w);
+}
+
+void sqrt() {
+    value biTwo = bigInteger(2);
+    
+    void test(Integer x, Integer modulus) {
+        value biModulus = bigInteger(modulus);
+        value biX = bigInteger(x);
+        value fe = Fp(biModulus, biX);
+        assert(fe.sqrt().toBigInteger().modPow(biTwo, biModulus) == biX);
+        
+        value wX = wholeNumber(x);
+        value wModulus = wholeNumber(modulus);
+        assert(sqrtModuloPrime(wX, wModulus).powerRemainder(two, wModulus) == wX);
+    }
+    
+    test(4, 7);
+    test(4, 13);
+}
+
+void keyParts() {
+    forever(() => 1).find((Integer elem) => true);
+    randomKeyParts();
+}
+
+EphemeralKey ecdh() {
+    value alice = IncompleteEphemeralKey();
+    value bob = IncompleteEphemeralKey();
+    
+    EphemeralKey aliceSecret = alice.complete { remoteSharedPart = bob.sharedPart; };
+    EphemeralKey bobSecret = bob.complete { remoteSharedPart = alice.sharedPart; };
+    assert(aliceSecret == bobSecret);
+    
+    return aliceSecret;
+}
+
+void writeField() {
+    // from toycrypto.asn1.ObjectIdentifier
+    [Integer+] writeField(Integer field) {
+        return takeUntil(
+                iterate(field, (Integer i) => i.rightLogicalShift(7)),
+                (Integer i) => i == 0)
+            .collect((Integer elem) => elem.and(#7f).or(#80))
+            .withLeading(field.and(#7f))
+            .reversed;
+    }
+    
+    assert(writeField(0) == [0]);
+    assert(writeField(1) == [1]);
+    assert(writeField(255) == [129, 127]);
+}
+
+void deencodeParts(EphemeralSharedPart sharedPart) {
+    ASN1Primitive deencode(ASN1Representable encodable) {
+        value asn1 = encodable.asn1;
+        value encoded = asn1.encoded;
+        value aSN1Object = asn1Decodable(encoded);
+        assert(aSN1Object == asn1);
+        return parsePrimitive(arrays.toByteArray(encoded));
+    }
+    
+    value int = ASN1Integer(wholeNumber(512));
+    value decInt = deencode(int);
+    assert(is JavaASN1Integer decInt);
+    //print(decInt);
+    
+    value identifier = x9Identifiers.ecPublicKey;
+    value decIdentifier = deencode(identifier);
+    assert(is ASN1ObjectIdentifier decIdentifier);
+    assert(decIdentifier.string == identifier.string);
+    //print(decIdentifier);
+    
+    value fieldId = X9FieldId(sharedPart.domain.curve.primeP);
+    value decFieldId = deencode(fieldId);
+    value decSeqFieldId = toASN1Sequence(decFieldId);
+    //print(decSeqFieldId);
+    
+    value curve = X9Curve(sharedPart.domain.curve);
+    value decCurve = deencode(curve);
+    value decSeqCurve = toASN1Sequence(decCurve);
+    //print(decSeqCurve);
+    
+    value ecPoint = X9ECPoint(sharedPart.domain.basepointG);
+    value decEcPoint = deencode(ecPoint);
+    //print(decEcPoint);
+    
+    value params = X9ECParameters(sharedPart.domain);
+    value decParams = deencode(params);
+    value decSeqParams = toASN1Sequence(decParams);
+    //print(decSeqParams);
+    
+    value algorithmIdentifier = AlgorithmIdentifier(identifier, params);
+    value decAlgorithmIdentifier = deencode(algorithmIdentifier);
+    value decSeqAlgorithmIdentifier = toASN1Sequence(decAlgorithmIdentifier);
+    //print(decSeqAlgorithmIdentifier);
+    
+    value encodedPoint = encodePoint(sharedPart.q);
+    value keyData = BitString(encodedPoint);
+    //print(Size(encodedPoint.size).encoded);
+    //print(encodedPoint);
+    //print(keyData.encoded);
+    value decKeyData = deencode(keyData);
+    //print(decKeyData);
+    
+    value bitString = BitString([0]);
+    value testSeq = ASN1Sequence([bitString, identifier]);
+    //print("Size[1]: " + Size(1).encoded.string);
+    //print("BitString: " + bitString.encoded.string);
+    //print("Identifier: " + identifier.encoded.string);
+    //print("TestSeq: " + testSeq.asn1.encoded.string);
+    value decTestSeq = deencode(testSeq);
+    value decSeqTestSeq = toASN1Sequence(decTestSeq);
+    //print(decSeqTestSeq);
+    
+    value publicKey = SubjectPublicKeyInfo(algorithmIdentifier, encodePoint(sharedPart.q));
+    value decPublicKey = deencode(publicKey);
+    value decSeqPublicKey = toASN1Sequence(decPublicKey);
+    //print(decSeqPublicKey);
+}
+
+void decodeJavaSharedPart(EphemeralSharedPart sharedPart) {
+    assert(is ASN1Sequence keySeq = sharedPart.asn1.sequence[1]);
+    value encodedKey = arrays.toByteArray(keySeq.encoded);
+    
+    value subPubKeyInfo = toSubjectPublicKeyInfo(encodedKey);
+    assert(subPubKeyInfo.algorithm.algorithm == id_ecPublicKey);
+    assert(subPubKeyInfo.publicKeyData.bytes.array.sequence == encodePoint(sharedPart.q));
+    
+    value encodedKeySpec = X509EncodedKeySpec(encodedKey);
+    fetchKeyFactory("ECDH").generatePublic(encodedKeySpec);
+}
+
+void encodeSharedPart() {
+    EphemeralSharedPart sharedPart = IncompleteEphemeralKey().sharedPart;
+
+    deencodeParts(sharedPart);
+    decodeJavaSharedPart(sharedPart);
+    
+    value encodedSharedPart = sharedPart.encoded;
+    EphemeralSharedPart decodedPart = decodeSharedPart(encodedSharedPart);
+    assert(decodedPart == sharedPart);
+}
+
+void aes(SharedKey key) {
+    String message = "Hello world!";
+    CipherText enc = encrypt(key, arrays.toByteArray(codepointsOf(message)));
+    
+    ByteArray dec = decrypt(key, enc);
+    String fin = string(asCodepoints(dec.array));
+    assert(fin == message);
+    print(fin);
+}
+shared {Integer*} codepointsOf({Character*} charSequence) {
+    return { for (c in charSequence) c.integer };
+}
+
+shared {Character*} asCodepoints({Integer*} intSequence) {
+    return { for (i in intSequence) i.character };
+}
+
+Range<Integer> digits = '0'.integer..'9'.integer;
+Range<Integer> hexAscii = 'a'.integer..'f'.integer;
+
+Integer decodeLowerHexCharacter(Character hex) {