def authenticate(previous_token: str = None) -> str:
    """ Authenticate the client to the server """

    # if we already have a session token, try to authenticate with it
    if previous_token is not None:
        headers = server_connection.request("authenticate", {
            'session_token': previous_token,
            'repository': config['repository']
        })[1]  # Only care about headers

        if headers['status'] == 'ok':
            return previous_token

    # If the session token has expired, or if we don't have one, re-authenticate
    headers = server_connection.request(
        "begin_auth",
        {'repository': config['repository']})[1]  # Only care about headers

    if headers['status'] == 'ok':
        signature: bytes = base64.b64encode(
            pysodium.crypto_sign_detached(headers['auth_token'],
                                          config['private_key']))
        headers = server_connection.request(
            "authenticate", {
                'auth_token': headers['auth_token'],
                'signature': signature,
                'user': config['user'],
                'repository': config['repository']
            })[1]  # Only care about headers

        if headers['status'] == 'ok': return headers['session_token']
    raise SystemExit('Authentication failed')
Beispiel #2
0
 def test_private_key_no_encrypt(self):
     crypto.input = lambda prompt: 'n'
     private_key, public_key = crypto.make_keypair()
     key = crypto.unlock_private_key(private_key)
     sig = pysodium.crypto_sign_detached('test', key)
     pysodium.crypto_sign_verify_detached(sig, 'test',
                                          base64.b64decode(public_key))
Beispiel #3
0
    def sign(self, message, generic=False):
        """
        Sign a raw sequence of bytes
        :param message: sequence of bytes, raw format or hexadecimal notation
        :param generic: do not specify elliptic curve if set to True
        :return: signature in base58 encoding
        """
        message = scrub_input(message)

        if not self.is_secret:
            raise ValueError("Cannot sign without a secret key.")

        # Ed25519
        if self.curve == b"ed":
            digest = pysodium.crypto_generichash(message)
            signature = pysodium.crypto_sign_detached(digest, self._secret_key)
        # Secp256k1
        elif self.curve == b"sp":
            pk = secp256k1.PrivateKey(self._secret_key)
            signature = pk.ecdsa_serialize_compact(
                pk.ecdsa_sign(message, digest=blake2b_32))
        # P256
        elif self.curve == b"p2":
            r, s = sign(msg=message, d=bytes_to_int(self._secret_key), hashfunc=blake2b_32)
            signature = int_to_bytes(r) + int_to_bytes(s)
        else:
            assert False

        if generic:
            prefix = b'sig'
        else:
            prefix = self.curve + b'sig'

        return base58_encode(signature, prefix).decode()
Beispiel #4
0
def sign(message: bytes, secret_key: bytes) -> bytes:
    """A message signature.

    :param message: The message to be signed
    :param secret_key: The secret key to use during signing
    """
    return crypto_sign_detached(message, secret_key)
Beispiel #5
0
    def create_pairing_response_by_serial(self, user_token_id, gda=None):
        """
        Creates a base64-encoded pairing response that identifies
        the token by its serial

        :param user_token_id: the token id (primary key for the user token db)
        :returns base64 encoded pairing response
        """

        if not gda:
            gda = self.gda

        token_serial = self.tokens[user_token_id]['serial']
        server_public_key = self.tokens[user_token_id]['server_public_key']
        partition = self.tokens[user_token_id]['partition']

        # ------------------------------------------------------------------ --

        # assemble header and plaintext

        header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition)

        pairing_response = b''
        pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, user_token_id)

        pairing_response += self.public_key

        pairing_response += token_serial.encode('utf8') + b'\x00\x00'
        pairing_response += gda.encode('utf-8') + b'\x00'

        signature = crypto_sign_detached(pairing_response, self.secret_key)
        pairing_response += signature

        # ------------------------------------------------------------------ --

        # create public diffie hellman component
        # (used to decrypt and verify the reponse)

        r = os.urandom(32)
        R = calc_dh_base(r)

        # ------------------------------------------------------------------ --

        # derive encryption key and nonce

        server_public_key_dh = dsa_to_dh_public(server_public_key)
        ss = calc_dh(r, server_public_key_dh)
        U = SHA256.new(ss).digest()
        encryption_key = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # encrypt in EAX mode

        cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
        cipher.update(header)
        ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

        return encode_base64_urlsafe(header + R + ciphertext + tag)
Beispiel #6
0
def _blake2b_ed25519_deploy_data(current_height,
                                 bytecode,
                                 privatekey,
                                 receiver=None):
    sender = get_sender(private_key, True)
    print(sender)
    nonce = get_nonce(sender)
    print("nonce is {}".format(nonce))

    tx = Transaction()
    tx.valid_until_block = current_height + 88
    tx.nonce = nonce
    if receiver is not None:
        tx.to = receiver
    tx.data = hex2bytes(bytecode)

    message = _blake2b(tx.SerializeToString())
    print("msg is {}".format(message))
    sig = pysodium.crypto_sign_detached(message, hex2bytes(privatekey))
    print("sig {}".format(binascii.b2a_hex(sig)))

    pubkey = pysodium.crypto_sign_sk_to_pk(hex2bytes(privatekey))
    print("pubkey is {}".format(binascii.b2a_hex(pubkey)))
    signature = binascii.hexlify(sig[:]) + binascii.hexlify(pubkey[:])
    print("signature is {}".format(signature))

    unverify_tx = UnverifiedTransaction()
    unverify_tx.transaction.CopyFrom(tx)
    unverify_tx.signature = hex2bytes(signature)
    unverify_tx.crypto = Crypto.Value('SECP')

    print("unverify_tx is {}".format(
        binascii.hexlify(unverify_tx.SerializeToString())))
    return binascii.hexlify(unverify_tx.SerializeToString())
Beispiel #7
0
 def sign(cls, data, key, footer=b''):
     signature = pysodium.crypto_sign_detached(m=pre_auth_encode(
         cls.public_header, data, footer),
                                               sk=key)
     token = cls.public_header + b64encode(data + signature)
     if footer:
         token += b'.' + b64encode(footer)
     return token
Beispiel #8
0
    def create_pairing_response_by_serial(self, user_token_id):

        """
        Creates a base64-encoded pairing response that identifies
        the token by its serial

        :param user_token_id: the token id (primary key for the user token db)
        :returns base64 encoded pairing response
        """

        token_serial = self.tokens[user_token_id]['serial']
        server_public_key = self.tokens[user_token_id]['server_public_key']
        partition = self.tokens[user_token_id]['partition']

        # ------------------------------------------------------------------ --

        # assemble header and plaintext

        header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition)

        pairing_response = b''
        pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, user_token_id)

        pairing_response += self.public_key

        pairing_response += token_serial.encode('utf8') + b'\x00\x00'
        pairing_response += self.gda + b'\x00'

        signature = crypto_sign_detached(pairing_response, self.secret_key)
        pairing_response += signature

        # ------------------------------------------------------------------ --

        # create public diffie hellman component
        # (used to decrypt and verify the reponse)

        r = os.urandom(32)
        R = calc_dh_base(r)

        # ------------------------------------------------------------------ --

        # derive encryption key and nonce

        server_public_key_dh = dsa_to_dh_public(server_public_key)
        ss = calc_dh(r, server_public_key_dh)
        U = SHA256.new(ss).digest()
        encryption_key = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # encrypt in EAX mode

        cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
        cipher.update(header)
        ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

        return encode_base64_urlsafe(header + R + ciphertext + tag)
Beispiel #9
0
def signature_verifier_generate (_local_sign_priv_key, _peer_sess_pub_key) :
	
	log ("[41a10bce]", "[crypto][signature]", "signing `%s`...", _peer_sess_pub_key.encode ("base64"))
	
	_verifier = pysodium.crypto_sign_detached (_peer_sess_pub_key, _local_sign_priv_key)
	
	log ("[66026b06]", "[crypto][signature]", "signed `%s`;", _verifier.encode ("b64"))
	
	return _verifier
Beispiel #10
0
def _blake2b_ed25519_deploy_data(current_height,
                                 bytecode,
                                 value,
                                 quota,
                                 privatekey,
                                 version,
                                 receiver=None):
    sender = get_sender(private_key, True)
    logger.debug(sender)
    nonce = get_nonce()
    logger.debug("nonce is {}".format(nonce))

    tx = Transaction()
    tx.valid_until_block = current_height + 88
    tx.nonce = nonce
    tx.version = version
    if version == 0:
        chainid = get_chainid()
        logger.info("version is {}".format(version))
        logger.info("chainid is {}".format(chainid))
        tx.chain_id = chainid
    elif version < 3:
        chainid = get_chainid_v1()
        logger.info("version is {}".format(version))
        logger.info("chainid_v1 is {}".format(chainid))
        tx.chain_id_v1 = chainid.to_bytes(32, byteorder='big')
    else:
        logger.error("unexpected version {}".format(version))
    if receiver is not None:
        if version == 0:
            tx.to = receiver
        elif version < 3:
            tx.to_v1 = hex2bytes(receiver)
        else:
            logger.error("unexpected version {}".format(version))
    tx.data = hex2bytes(bytecode)
    tx.value = value.to_bytes(32, byteorder='big')
    tx.quota = quota

    message = _blake2b(tx.SerializeToString())
    logger.debug("blake2b msg")
    sig = pysodium.crypto_sign_detached(message, hex2bytes(privatekey))
    logger.debug("sig {}".format(binascii.b2a_hex(sig)))

    pubkey = pysodium.crypto_sign_sk_to_pk(hex2bytes(privatekey))
    logger.debug("pubkey is {}".format(binascii.b2a_hex(pubkey)))
    signature = binascii.hexlify(sig[:]) + binascii.hexlify(pubkey[:])
    logger.debug("signature is {}".format(signature))

    unverify_tx = UnverifiedTransaction()
    unverify_tx.transaction.CopyFrom(tx)
    unverify_tx.signature = hex2bytes(signature)
    unverify_tx.crypto = Crypto.Value('DEFAULT')

    logger.info("unverify_tx is {}".format(
        binascii.hexlify(unverify_tx.SerializeToString())))
    return binascii.hexlify(unverify_tx.SerializeToString())
Beispiel #11
0
 def generateProposeECDH(self, cSuite, pubkey, signkey, senderCertificate,
                         statusCertificate):
     G_temp = struct.pack(
         '>BBI', 0, cSuite,
         len(pubkey)) + pubkey + senderCertificate + statusCertificate
     zeroSig = (0).to_bytes(64, byteorder='big')
     G = G_temp + struct.pack('>I', len(zeroSig)) + zeroSig
     G = pysodium.crypto_sign_detached(G, signkey)
     # print(len(G_temp + struct.pack('>I', len(G)) + G))
     return G_temp + struct.pack('>I', len(G)) + G
Beispiel #12
0
def domsg(ws, msg):
    receive = json.loads(msg)
    if "who" in json.loads(receive["payload"]):
        now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat()
        payloadobj = dict(timestamp=now, nickname=nickname, listen=["Robotville"])
        payload = json.dumps(payloadobj)
        payload_bytes = payload.encode("utf-8")
        sig = pysodium.crypto_sign_detached(payload_bytes, privkey)
        sig_base64 = binascii.b2a_base64(sig).strip()
        cmdobj = dict(idkey=idkey_base64.decode("utf-8"), payload=payload, signature=sig_base64.decode("utf-8"))
        cmd = json.dumps(cmdobj)
        cmd_bytes = cmd.encode("utf-8")
        ws.send(cmd_bytes)
Beispiel #13
0
 def genProposeSalsa(self, cSuite, servPubkey, sessionKey, myEncrypKey,
                     mySignKey, senderCertificate, statusCertificate):
     nonce = pysodium.randombytes(pysodium.crypto_box_NONCEBYTES)
     encrypSession = pysodium.crypto_box(sessionKey, nonce, servPubkey,
                                         myEncrypKey)
     # encrypSession = pysodium.randombytes(32)
     G_temp = struct.pack(
         '>BBI', 0, cSuite, len(nonce + encrypSession)
     ) + nonce + encrypSession + senderCertificate + statusCertificate
     zeroSig = (0).to_bytes(64, byteorder='big')
     G = G_temp + struct.pack('>I', len(zeroSig)) + zeroSig
     G = pysodium.crypto_sign_detached(G, mySignKey)
     return G_temp + struct.pack('>I', len(G)) + G
    def new_event(self, d, p):
        """Create a new event (and also return it's hash)."""

        assert p == () or len(p) == 2                   # 2 parents
        assert p == () or self.hg[p[0]].c == self.pk  # first exists and is self-parent
        assert p == () or self.hg[p[1]].c != self.pk  # second exists and not self-parent
        # TODO: fail if an ancestor of p[1] from creator self.pk is not an
        # ancestor of p[0]

        t = time()
        s = crypto_sign_detached(dumps((d, p, t, self.pk)), self.sk)
        ev = Event(d, p, t, self.pk, s)

        return crypto_generichash(dumps(ev)), ev
Beispiel #15
0
Datei: test.py Projekt: ctpo6/c2c
def on_message(ws, data):
    #print("msg: ", data)
    msg = json.loads(data)
    #id = "".join(map(chr, (msg['id'])))
    id = msg['id']
    if id == get_identify_reply_id:
        global remote_node_id

        pk = bytes(msg['result'][0])
        sg = bytes(msg['result'][1])

        remote_node_id = pk

        hex_id = binascii.hexlify(remote_node_id).decode("utf-8")

        try:
            pysodium.crypto_sign_verify_detached(sg, get_identify_reply_tn, pk)
            print("OK!!! node id: ", hex_id)
        except:
            print("error identify node id: ", hex_id)

        do_send_put(ws)
        return

    if id == get_reply_id:
        print("GET result!!!:\n", msg)
        return

    if id == get_fin_reply_id:
        print("GET finally result!!!:\n", msg)
        return

    if msg["method"] == "get_identify":
        tn = bytes(msg['params'][0])
        digest = binascii.unhexlify(
            b'DAFBABCBC50DF4BB432C72F610878A6990E85734')
        with_digest = msg['params'][1]
        sm = tn
        if with_digest:
            sm.append(digest)
        sg = pysodium.crypto_sign_detached(sm, secretkey)
        ws.send(
            json.dumps({
                "jsonrpc":
                "2.0",
                "id":
                msg['id'],
                "result": (list(bytearray(publickey)), list(bytearray(sg)))
            }))
Beispiel #16
0
def sign_json(message, pk, sk):
    try:
        del message['sender']
    except KeyError:
        True
    try:
        del message['signature']
    except KeyError:
        True

    canonicalized = json.dumps(message, separators=(',', ':'), sort_keys=True)
    signature = pysodium.crypto_sign_detached(canonicalized, sk)
    message['sender'] = base64.b64encode(pk)
    message['signature'] = base64.b64encode(signature)

    return message
Beispiel #17
0
    def GOGOGO(*args):
        while True:
            now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat()
            message = create_message()
            payloadobj = dict(timestamp=now, nickname=nickname, listen=["Robotville"], say={"Robotville":message})
            payload = json.dumps(payloadobj)
            payload_bytes = payload.encode("utf-8")
            sig = pysodium.crypto_sign_detached(payload_bytes, privkey)
            sig_base64 = binascii.b2a_base64(sig).strip()
            cmdobj = dict(idkey=idkey_base64.decode("utf-8"), payload=payload, signature=sig_base64.decode("utf-8"))
            cmd = json.dumps(cmdobj)
            cmd_bytes = cmd.encode("utf-8")
            ws.send(cmd_bytes)
            #print("sent", cmd_bytes)

            time.sleep(15 + random.expovariate(1/10))
Beispiel #18
0
def sign_json(message, pk, sk):
    try:
        del message['sender']
    except KeyError:
        True
    try:
        del message['signature']
    except KeyError:
        True

    canonicalized = json.dumps(message, separators=(',', ':'), sort_keys=True)
    signature = pysodium.crypto_sign_detached(canonicalized, sk)
    message['sender'] = base64.b64encode(pk)
    message['signature'] = base64.b64encode(signature)

    return message
Beispiel #19
0
    def test_signature(self) -> None:
        """Tests the Signature class."""
        keypair = KeyPair.generate()

        data = bytes([i for i in range(10)])

        signature = Signature.sign(data, keypair.secret_key)

        self.assertTrue(isinstance(signature, Signature))
        self.assertTrue(isinstance(signature, _FixedByteArray))
        self.assertEqual(signature.value,
                         crypto_sign_detached(data, keypair.secret_key.value))

        self.assertTrue(signature.verify(data, keypair.public_key))

        wrong_data = bytes([1, 2])
        wrong_pk = PublicKey(bytes([i for i in range(_PUBLIC_KEY_BYTES_LEN)]))
        self.assertFalse(signature.verify(wrong_data, keypair.public_key))
        self.assertFalse(signature.verify(data, wrong_pk))
Beispiel #20
0
def auth(s, id, pwd=None, r=None):
    if r is None:
        nonce = s.recv(32)
        if len(nonce) != 32:
            return False
        rwd = b''
    else:
        msg = s.recv(64)
        if len(msg) != 64:
            return False
        beta = msg[:32]
        nonce = msg[32:]
        rwd = sphinxlib.finish(pwd, r, beta, id)

    sk, pk = get_signkey(id, rwd)
    sig = pysodium.crypto_sign_detached(nonce, sk)
    clearmem(sk)
    s.send(sig)
    return rwd
Beispiel #21
0
def run_client():
    # FIXME: lock state file for duration of the program
    counter = read_counter()
    write_counter(counter + 1)

    log('[INFO] knock '+server_ip+':'+str(server_port)+' '+client_sign_public_key.encode('hex'))

    # NONCE(24) + MAC(16) + MAGIC(8) + SIGNPUB(32) + SIG(64) + COUNTER(14) + LEN(2) + REST

    PROTO_MIN_SIZE = 160

    arg = sys.argv[2] if len(sys.argv) > 2 else b''
    if PROTO_MIN_SIZE + len(arg) > 1024:
        log('[ERROR] argument too long')
        sys.exit(1)

    padded_min_size = PROTO_MIN_SIZE + roundup(len(arg), 16)
    padding = '\x00' * ((roundup(len(arg), 16) - len(arg)) +
                        (16 * random.randint(0, (1024 - padded_min_size) / 16)))

    nonce = saferandom(pysodium.crypto_secretbox_NONCEBYTES)

    magic_bin = '42f9708e2f1369d9'.decode('hex') # chosen by fair die
    counter_bin = hex(counter)[2:].zfill(28).decode('hex')
    data_len_bin = '0000'.decode('hex')
    server_ip_bin = socket.inet_aton(server_ip)

    sig = pysodium.crypto_sign_detached((nonce + magic_bin + client_sign_public_key +
                                         counter_bin + data_len_bin + arg + padding +
                                         server_private_key + server_ip_bin),
                                        client_sign_private_key)

    cdata = nonce + pysodium.crypto_secretbox((magic_bin + client_sign_public_key + sig +
                                               counter_bin + data_len_bin + arg + padding),
                                              nonce,
                                              server_private_key)

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(cdata, (server_ip, server_port))
    log('[INFO] sent '+str(len(cdata)))
Beispiel #22
0
    def sign(self, message: Union[str, bytes], generic: bool = False):
        """Sign a raw sequence of bytes.

        :param message: sequence of bytes, raw format or hexadecimal notation
        :param generic: do not specify elliptic curve if set to True
        :returns: signature in base58 encoding
        """
        encoded_message = scrub_input(message)

        if not self.secret_exponent:
            raise ValueError("Cannot sign without a secret key.")

        # Ed25519
        if self.curve == b"ed":
            digest = pysodium.crypto_generichash(encoded_message)
            signature = pysodium.crypto_sign_detached(digest,
                                                      self.secret_exponent)
        # Secp256k1
        elif self.curve == b"sp":
            pk = secp256k1.PrivateKey(self.secret_exponent)
            signature = pk.ecdsa_serialize_compact(
                pk.ecdsa_sign(encoded_message, digest=blake2b_32))
        # P256
        elif self.curve == b"p2":
            r, s = fastecdsa.ecdsa.sign(msg=encoded_message,
                                        d=bytes_to_int(self.secret_exponent),
                                        hashfunc=blake2b_32)
            signature = r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
        else:
            assert False

        if generic:
            prefix = b'sig'
        else:
            prefix = self.curve + b'sig'

        return base58_encode(signature, prefix).decode()
Beispiel #23
0
def test_pysodium():
    """
    Test all the functions needed from pysodium libarary (libsodium)

    """
    # crypto_sign signatures with Ed25519 keys

    # create keypair without seed
    verkey, sigkey = pysodium.crypto_sign_keypair()
    assert len(verkey) == 32 == pysodium.crypto_sign_PUBLICKEYBYTES
    assert len(sigkey) == 64 == pysodium.crypto_sign_SECRETKEYBYTES

    assert 32 == pysodium.crypto_sign_SEEDBYTES
    sigseed = pysodium.randombytes(pysodium.crypto_sign_SEEDBYTES)
    assert len(sigseed) == 32
    # seed = (b'J\xeb\x06\xf2BA\xd6/T\xe1\xe2\xe2\x838\x8a\x99L\xd9\xb5(\\I\xccRb\xc8\xd5\xc7Y\x1b\xb6\xf0')

    # Ann's seed
    sigseed = (
        b'PTi\x15\xd5\xd3`\xf1u\x15}^r\x9bfH\x02l\xc6\x1b\x1d\x1c\x0b9\xd7{\xc0_\xf2K\x93`'
    )
    assert len(sigseed) == 32

    #  try key stretching from 16 bytes using  pysodium.crypto_pwhash()
    assert 16 == pysodium.crypto_pwhash_SALTBYTES
    salt = pysodium.randombytes(pysodium.crypto_pwhash_SALTBYTES)
    assert len(salt) == 16
    #  salt = b'\x19?\xfa\xc7\x8f\x8b\x7f\x8b\xdbS"$\xd7[\x85\x87'

    # algorithm default is argon2id
    sigseed = pysodium.crypto_pwhash(
        outlen=32,
        passwd="",
        salt=salt,
        opslimit=pysodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
        memlimit=pysodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
        alg=pysodium.crypto_pwhash_ALG_DEFAULT)

    assert len(sigseed) == 32
    #  seed = (b'\xa9p\x89\x7f+\x0e\xc4\x9c\xf2\x01r\xafTI\xc0\xfa\xac\xd5\x99\xf8O\x8f=\x843\xa2\xb6e\x9fO\xff\xd0')

    # creates signing/verification key pair from seed
    verkey, sigkey = pysodium.crypto_sign_seed_keypair(sigseed)
    assert len(verkey) == 32
    assert len(sigkey) == 64
    #  sigkey is seed and verkey concatenated. Libsodium does this as an optimization
    #  because the signing scheme needs both the private key (seed) and the public key so
    #  instead of recomputing the public key each time from the secret key it requires
    #  the public key as an input of and instead of two separate inputs, one for the
    #  secret key and one for the public key, it uses a concatenated form.
    #  Essentially crypto_sign_seed_keypair and crypto_sign_keypair return redundant
    #  information in the duple (verkey, sigkey) because sigkey includes verkey
    #  so one could just store sigkey and extract verkey or sigseed when needed
    #  or one could just store verkey and sigseed and reconstruct sigkey when needed.
    #  crypto_sign_detached requires sigkey (sigseed + verkey)
    #  crypto_sign_verify_detached reqires verkey only
    #  https://crypto.stackexchange.com/questions/54353/why-are-nacl-secret-keys-64-bytes-for-signing-but-32-bytes-for-box
    assert sigseed == sigkey[:32]
    assert verkey == sigkey[32:]
    assert sigkey == sigseed + verkey
    # vk = (b'B\xdd\xbb}8V\xa0\xd6lk\xcf\x15\xad9\x1e\xa7\xa1\xfe\xe0p<\xb6\xbex\xb0s\x8d\xd6\xf5\xa5\xe8Q')

    #  utility function to extract seed from secret sigkey (really just extracting from front half)
    assert sigseed == pysodium.crypto_sign_sk_to_seed(sigkey)

    assert 64 == pysodium.crypto_sign_BYTES

    msg = "The lazy dog jumped over the river"
    msgb = msg.encode(
        "utf-8")  # must convert unicode string to bytes in order to sign it
    assert msgb == b'The lazy dog jumped over the river'
    sig = pysodium.crypto_sign_detached(msgb, sigseed +
                                        verkey)  #  sigkey = seed + verkey
    assert len(sig) == 64
    """
    sig = (b"\x99\xd2<9$$0\x9fk\xfb\x18\xa0\x8c@r\x122.k\xb2\xc7\x1fp\x0e'm\x8f@"
           b'\xaa\xa5\x8c\xc8n\x85\xc8!\xf6q\x91p\xa9\xec\xcf\x92\xaf)\xde\xca'
           b'\xfc\x7f~\xd7o|\x17\x82\x1d\xd4<o"\x81&\t')

    """
    #siga = pysodium.crypto_sign(msg.encode("utf-8"), sk)[:pysodium.crypto_sign_BYTES]
    #assert len(siga) == 64
    #assert sig == siga

    try:  #  verify returns None if valid else raises ValueError
        result = pysodium.crypto_sign_verify_detached(sig, msgb, verkey)
    except Exception as ex:
        assert False
    assert not result
    assert result is None

    sigbad = sig[:-1]
    sigbad += b'A'

    try:  #  verify returns None if valid else raises ValueError
        result = pysodium.crypto_sign_verify_detached(sigbad, msgb, verkey)
    except Exception as ex:
        assert True
        assert isinstance(ex, ValueError)

    # crypto_box authentication encryption with X25519 keys

    apubkey, aprikey = pysodium.crypto_box_keypair()
    assert len(apubkey) == 32 == pysodium.crypto_box_SECRETKEYBYTES
    assert len(aprikey) == 32 == pysodium.crypto_box_PUBLICKEYBYTES

    repubkey = pysodium.crypto_scalarmult_curve25519_base(aprikey)
    assert repubkey == apubkey

    assert 32 == pysodium.crypto_box_SEEDBYTES

    boxseed = pysodium.randombytes(pysodium.crypto_box_SEEDBYTES)
    assert len(boxseed) == 32

    bpubkey, bprikey = pysodium.crypto_box_seed_keypair(boxseed)
    assert len(bpubkey) == 32
    assert len(bprikey) == 32

    repubkey = pysodium.crypto_scalarmult_curve25519_base(bprikey)
    assert repubkey == bpubkey

    assert 24 == pysodium.crypto_box_NONCEBYTES
    nonce = pysodium.randombytes(pysodium.crypto_box_NONCEBYTES)
    assert len(nonce) == 24
    # nonce = b'\x11\xfbi<\xf2\xb6k\xa05\x0c\xf9\x86t\x07\x8e\xab\x8a\x97nG\xe8\x87,\x94'

    atob_tx = "Hi Bob I'm Alice"
    atob_txb = atob_tx.encode("utf-8")

    # Detached recomputes shared key every time.
    # A encrypt to B
    acrypt, amac = pysodium.crypto_box_detached(atob_txb, nonce, bpubkey,
                                                aprikey)
    amacl = pysodium.crypto_box_MACBYTES
    assert amacl == 16
    #  amac =  b'\xa1]\xc6ML\xe2\xa9:\xc0\xdc\xab\xa5\xc4\xc7\xf4\xdb'
    #  acrypt = (b'D\n\x17\xb6z\xd8+t)\xcc`y\x1d\x10\x0cTC\x02\xb5@\xe2\xf2\xc9-(\xec*O\xb8~\xe2\x1a\xebO')
    # when transmitting prepend amac to crypt

    acipher = pysodium.crypto_box(atob_txb, nonce, bpubkey, aprikey)
    assert acipher == amac + acrypt

    atob_rxb = pysodium.crypto_box_open_detached(acrypt, amac, nonce, apubkey,
                                                 bprikey)
    atob_rx = atob_rxb.decode("utf-8")
    assert atob_rx == atob_tx
    assert atob_rxb == atob_txb

    atob_rxb = pysodium.crypto_box_open(acipher, nonce, apubkey, bprikey)
    atob_rx = atob_rxb.decode("utf-8")
    assert atob_rx == atob_tx
    assert atob_rxb == atob_txb

    btoa_tx = "Hello Alice I am Bob"
    btoa_txb = btoa_tx.encode("utf-8")

    # B encrypt to A
    bcrypt, bmac = pysodium.crypto_box_detached(btoa_txb, nonce, apubkey,
                                                bprikey)
    # bmac = b'\x90\xe07=\xd22\x8fh2\xff\xdd\x84tC\x053'
    # bcrypt = (b'8\xb5\xba\xe7\xcc\xae B\xefx\xe6{U\xf7\xefA\x00\xc7|\xdbu\xcfc\x01$\xa9\xa2P\xa7\x84\xa5\xae\x180')
    # when transmitting prepend amac to crypt

    bcipher = pysodium.crypto_box(btoa_txb, nonce, apubkey, bprikey)
    assert bcipher == bmac + bcrypt

    btoa_rxb = pysodium.crypto_box_open_detached(bcrypt, bmac, nonce, bpubkey,
                                                 aprikey)
    btoa_rx = btoa_rxb.decode("utf-8")
    assert btoa_rx == btoa_tx
    assert btoa_rxb == btoa_txb

    btoa_rxb = pysodium.crypto_box_open(bcipher, nonce, bpubkey, aprikey)
    btoa_rx = btoa_rxb.decode("utf-8")
    assert btoa_rx == btoa_tx
    assert btoa_rxb == btoa_txb

    # compute shared key
    asymkey = pysodium.crypto_box_beforenm(bpubkey, aprikey)
    bsymkey = pysodium.crypto_box_beforenm(apubkey, bprikey)
    assert asymkey == bsymkey

    acipher = pysodium.crypto_box_afternm(atob_txb, nonce, asymkey)
    atob_rxb = pysodium.crypto_box_open_afternm(acipher, nonce, bsymkey)
    assert atob_rxb == atob_txb

    bcipher = pysodium.crypto_box_afternm(btoa_txb, nonce, bsymkey)
    btoa_rxb = pysodium.crypto_box_open_afternm(bcipher, nonce, asymkey)
    assert btoa_rxb == btoa_txb

    # crypto_box_seal public key encryption with X25519 keys
    #  uses same X25519 type of keys as crypto_box authenticated encryption
    #  so when converting sign key Ed25519 to X25519 can use for both types of encryption

    pubkey, prikey = pysodium.crypto_box_keypair()
    assert len(pubkey) == 32 == pysodium.crypto_box_PUBLICKEYBYTES
    assert len(prikey) == 32 == pysodium.crypto_box_SECRETKEYBYTES

    assert 48 == pysodium.crypto_box_SEALBYTES

    msg_txb = "Catch me if you can.".encode("utf-8")
    assert msg_txb == b'Catch me if you can.'
    cipher = pysodium.crypto_box_seal(msg_txb, pubkey)
    assert len(cipher) == 48 + len(msg_txb)

    msg_rxb = pysodium.crypto_box_seal_open(cipher, pubkey, prikey)
    assert msg_rxb == msg_txb

    #  convert Ed25519 key pair to X25519 key pair
    #  https://blog.filippo.io/using-ed25519-keys-for-encryption/
    #  https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519
    #  crypto_sign_ed25519_pk_to_curve25519
    #  crypto_sign_ed25519_sk_to_curve25519

    pubkey = pysodium.crypto_sign_pk_to_box_pk(verkey)
    assert len(pubkey) == pysodium.crypto_box_PUBLICKEYBYTES

    prikey = pysodium.crypto_sign_sk_to_box_sk(sigkey)
    assert len(prikey) == pysodium.crypto_box_SECRETKEYBYTES

    repubkey = pysodium.crypto_scalarmult_curve25519_base(prikey)
    assert repubkey == pubkey

    msg_txb = "Encoded using X25519 key converted from Ed25519 key".encode(
        "utf-8")
    cipher = pysodium.crypto_box_seal(msg_txb, pubkey)
    assert len(cipher) == 48 + len(msg_txb)

    msg_rxb = pysodium.crypto_box_seal_open(cipher, pubkey, prikey)
    assert msg_rxb == msg_txb
    """
Beispiel #24
0
def generate_pairing_url(token_type,
                         partition=None,
                         serial=None,
                         callback_url=None,
                         callback_sms_number=None,
                         otp_pin_length=None,
                         hash_algorithm=None,
                         use_cert=False):
    """
    Generates a pairing url that should be sent to the client.

    Mandatory parameters:

    :param: token_type The token type for which this url is generated
        as a string (currently supported is only 'qr')

    Optional parameters:

    :param partition: A partition id that should be used during pairing.
        Partitions identitify a subspace of tokens, that share a common
        key pair. This currently defaults to the enum id of the token
        type when set to None and is reserved for future use.

    :param serial: When a token for the client was already enrolled
        (e.g. manually in the manage interface) its serial has to be
        sent to the client. When serial is not specified the client will
        receive a so-called 'anonymous pairing url' with no token data
        inside it. The token will then be created after the server
        received a pairing response from the client.

    :param callback_url: A callback URL that should be used by the client
        to sent back the pairing reponse. Please note, that this url will
        be cached by the client and used in the challenge step, if the
        challenge doesn't provide a custom url

    :param callback_sms_number: A sms number that can be used by the client
        to send back the pairing response. Typically this is used as a
        fallback for offline pairing.
        As with the callback url please note, that the number will be
        cached by the client. If you want a different number in the
        challenge step you have to send it inside the challenge as
        specified in the challenge protocol

    :param otp_pin_length: The number of digits the otp has to consist of.

    :param hash_algorithm: A string value that signifies the hash algorithm
        used in calculating the hmac. Currently the values 'sha1', 'sha256',
        'sha512' are supported. If the parameter is left out the default
        depends on the token type. qrtoken uses sha256 as default, while
        hotp/totp uses sha1.

    :param use_cert: A boolean, if a server certificate should be used
        in the pairing url

    The function can raise several exceptions:

    :raises InvalidFunctionParameter: If the string given in token_type
        doesn't match a supported token type

    :raises InvalidFunctionParameter: If the string given in hash_algorithm
        doesn't match a supported hash algorithm

    :raises InvalidFunctionParameter: If public key has a different size
        than 32 bytes

    :raises InvalidFunctionParameter: If otp_pin_length value is not between
        1 and 127

    :return: Pairing URL string
    """

    # ----------------------------------------------------------------------- --

    # check the token type

    try:
        TOKEN_TYPE = TOKEN_TYPES[token_type]
    except KeyError:
        allowed_types = ', '.join(TOKEN_TYPES.keys())
        raise InvalidFunctionParameter(
            'token_type', 'Unsupported token type %s. Supported '
            'types for pairing are: %s' % (token_type, allowed_types))

    # ----------------------------------------------------------------------- --

    # initialize the flag bitmap

    flags = 0

    if not use_cert:
        flags |= FLAG_PAIR_PK
    if serial is not None:
        flags |= FLAG_PAIR_SERIAL
    if callback_url is not None:
        flags |= FLAG_PAIR_CBURL
    if callback_sms_number is not None:
        flags |= FLAG_PAIR_CBSMS
    if hash_algorithm is not None:
        flags |= FLAG_PAIR_HMAC
    if otp_pin_length is not None:
        flags |= FLAG_PAIR_DIGITS

    # ----------------------------------------------------------------------- --

    #            ------------------------------
    #  fields   | version | type | flags | ... |
    #            ------------------------------
    #  size     |    1    |  1   |   4   |  ?  |
    #            ------------------------------

    data = struct.pack('<bbI', PAIR_URL_VERSION, TOKEN_TYPE, flags)

    # ----------------------------------------------------------------------- --

    #            -----------------------
    #  fields   | ... | partition | ... |
    #            -----------------------
    #  size     |  6  |     4     |  ?  |
    #            -----------------------

    data += struct.pack('<I', partition)

    # ----------------------------------------------------------------------- --

    #            --------------------------------
    #  fields   | .... | server public key | ... |
    #            --------------------------------
    #  size     |  10  |        32         |  ?  |
    #            --------------------------------

    if flags & FLAG_PAIR_PK:

        server_public_key = get_public_key(partition)

        if len(server_public_key) != 32:
            raise InvalidFunctionParameter('server_public_key',
                                           'Public key must be 32 bytes long')

        data += server_public_key

    # ----------------------------------------------------------------------- --

    # Depending on flags additional data may be sent. If serial was provided
    # serial will be sent back. If callback url or callback sms was provided
    # the corresponding data will be added, too

    #            --------------------------------------------------------
    #  fields   | .... | serial | NUL | cb url | NUL | cb sms | NUL | ... |
    #            --------------------------------------------------------
    #  size     |  42  |   ?    |  1  |   ?    |  1  |   ?    |  1  |  ?  |
    #            --------------------------------------------------------

    if flags & FLAG_PAIR_SERIAL:
        data += serial.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBURL:
        data += callback_url.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBSMS:
        data += callback_sms_number.encode('utf8') + b'\x00'

    # ----------------------------------------------------------------------- --

    # Other optional values: allowed pin length of otp (number of digits)
    # and custom hash algorithm

    #            ---------------------------------------
    #  fields   | ... | otp pin length | hash_algorithm |
    #            ---------------------------------------
    #  size     |  ?  |       1        |       1        |
    #            ---------------------------------------

    if flags & FLAG_PAIR_DIGITS:
        if not (6 <= otp_pin_length <= 12):
            raise InvalidFunctionParameter(
                'otp_pin_length', 'Pin length must '
                'be in the range 6..12')
        data += struct.pack('<b', otp_pin_length)

    if flags & FLAG_PAIR_HMAC:
        try:
            HASH_ALGO = hash_algorithms[hash_algorithm]
        except KeyError:
            allowed_values = ", ".join(hash_algorithms.keys())
            raise InvalidFunctionParameter(
                'hash_algorithm', 'Unsupported hash algorithm %s, '
                'allowed values are %s' % (hash_algorithm, allowed_values))
        data += struct.pack('<b', HASH_ALGO)

    # ----------------------------------------------------------------------- --

    # TODO missing token details for other protocols (hotp, hmac, etc)
    # * counter (u64le)
    # * tstart (u64le)
    # * tstep (u32le)

    # TODO: replace lseqr literal with global config value
    # or global constant

    if not (flags & FLAG_PAIR_PK):

        secret_key = get_secret_key(partition)
        server_sig = crypto_sign_detached(data, secret_key)
        data += server_sig

    return 'lseqr://pair/' + encode_base64_urlsafe(data)
 def test_private_key_no_encrypt(self):
     crypto.raw_input = lambda : 'n'
     private_key, public_key = crypto.make_keypair()
     key = crypto.unlock_private_key(private_key)
     sig = pysodium.crypto_sign_detached('test', key)
     pysodium.crypto_sign_verify_detached(sig, 'test', base64.b64decode(public_key))
Beispiel #26
0
def sign(sk, data):
    return pysodium.crypto_sign_detached(data, sk)
Beispiel #27
0
    def create_challenge_url(self,
                             transaction_id,
                             content_type,
                             callback_url='',
                             message=None,
                             login=None,
                             host=None):

        """
        creates a challenge url (looking like lseqr://push/<base64string>),
        returns the url and the unencrypted challenge data

        :param transaction_id: The transaction id generated by LinOTP

        :param content_type: One of the types CONTENT_TYPE_SIGNREQ,
            CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN

        :param callback_url: callback url (optional), default is
            empty string

        :param message: the transaction message, that should be signed
            by the client. Only for content type CONTENT_TYPE_SIGNREQ

        :param login: the login name of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :param host: hostname of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :returns: tuple (challenge_url, sig_base), with challenge_url being
            the push url and sig_base the message, that is used for
            the client signature
        """

        serial = self.getSerial()

        # ------------------------------------------------------------------- --

        # sanity/format checks

        if content_type not in [CONTENT_TYPE_SIGNREQ,
                                CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN]:
            raise InvalidFunctionParameter('content_type', 'content_type must '
                                           'be CONTENT_TYPE_SIGNREQ, '
                                           'CONTENT_TYPE_PAIRING or '
                                           'CONTENT_TYPE_LOGIN.')

        # ------------------------------------------------------------------- --

        #  after the lseqr://push/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | sign |
        #            ---------------------------------------------------
        #           |          header         |          body           |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     |  64  |
        #            ---------------------------------------------------
        #

        # create header

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', CHALLENGE_URL_VERSION, user_token_id)

        # ------------------------------------------------------------------- --

        # create body

        r = urandom(32)
        R = calc_dh_base(r)

        b64_user_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_user_dsa_public_key)
        user_dh_public_key = dsa_to_dh_public(user_dsa_public_key)

        ss = calc_dh(r, user_dh_public_key)
        U = SHA256.new(ss).digest()
        zerome(ss)

        sk = U[0:16]
        nonce = U[16:32]
        zerome(U)

        # ------------------------------------------------------------------- --

        # create plaintext section

        # ------------------------------------------------------------------- --

        # generate plaintext header

        #            ------------------------------------------------
        #  fields   | content_type  | transaction_id | timestamp | ..
        #            ------------------------------------------------
        #  size     |       1       |        8       |     8     |  ?
        #            -------------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        plaintext = struct.pack('<bQQ', content_type, transaction_id,
                                int(time.time()))

        # ------------------------------------------------------------------- --

        utf8_callback_url = callback_url.encode('utf8')

        # enforce max url length as specified in protocol

        if len(utf8_callback_url) > 511:
            raise InvalidFunctionParameter('callback_url', 'max string '
                                           'length (encoded as utf8) is '
                                           '511')

        # ------------------------------------------------------------------- --

        # create data package depending on content type

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_PAIRING:

            #            -----------------------------------------
            #  fields   | header | serial | NUL | callback | NUL |
            #            -----------------------------------------
            #  size     |   9    |    ?   |  1  |     ?    |  1  |
            #            -----------------------------------------

            utf8_serial = serial.encode('utf8')

            if len(utf8_serial) > 63:
                raise ValueError('serial (encoded as utf8) can only be 63 '
                                 'characters long')

            plaintext += utf8_serial + b'\00' + utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_SIGNREQ:

            if message is None:
                raise InvalidFunctionParameter('message', 'message must be '
                                               'supplied for content type '
                                               'SIGNREQ')

            #            ------------------------------------------
            #  fields   | header | message | NUL | callback | NUL |
            #            ------------------------------------------
            #  size     |   9    |    ?    |  1  |     ?    |  1  |
            #            ------------------------------------------

            utf8_message = message.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_message) > 511:
                raise InvalidFunctionParameter('message', 'max string '
                                               'length (encoded as utf8) is '
                                               '511')

            plaintext += utf8_message + b'\00' + utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_LOGIN:

            if login is None:
                raise InvalidFunctionParameter('login', 'login must be '
                                               'supplied for content type '
                                               'LOGIN')
            if host is None:
                raise InvalidFunctionParameter('host', 'host must be '
                                               'supplied for content type '
                                               'LOGIN')

            #            -----------------------------------------------------
            #  fields   | header | login | NUL | host | NUL | callback | NUL |
            #            -----------------------------------------------------
            #  size     |   9    |   ?   |  1  |   ?  |  1  |     ?    |  1  |
            #            -----------------------------------------------------

            utf8_login = login.encode('utf8')
            utf8_host = host.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_login) > 127:
                raise InvalidFunctionParameter('login', 'max string '
                                               'length (encoded as utf8) is '
                                               '127')
            if len(utf8_host) > 255:
                raise InvalidFunctionParameter('host', 'max string '
                                               'length (encoded as utf8) is '
                                               '255')

            plaintext += utf8_login + b'\00'
            plaintext += utf8_host + b'\00'
            plaintext += utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        # encrypt inner layer

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        ciphertext = cipher.encrypt(plaintext)
        unsigned_raw_data = data_header + R + ciphertext

        # ------------------------------------------------------------------- --

        # create signature

        partition = self.getFromTokenInfo('partition')
        secret_key = get_secret_key(partition)
        signature = crypto_sign_detached(unsigned_raw_data, secret_key)
        raw_data = unsigned_raw_data + signature

        url = 'lseqr://push/' + encode_base64_urlsafe(raw_data)

        return url, (signature + plaintext)
Beispiel #28
0
def sign_blob(blob, id, rwd):
    sk, pk = get_signkey(id, rwd)
    res = pysodium.crypto_sign_detached(blob, sk)
    clearmem(sk)
    return b''.join((blob, res))
Beispiel #29
0
    def sign(cls, data: bytes, key: SecretKey) -> "Signature":
        """Signs the provided bytes sequence with the provided secret key."""

        signature = crypto_sign_detached(data, key.value)

        return Signature(signature)
Beispiel #30
0
def sign(data: bytes, sk: bytes) -> bytes:
    return crypto_sign_detached(data, sk)
Beispiel #31
0
def generate_pairing_url(token_type,
                         server_public_key,
                         serial=None,
                         callback_url=None,
                         callback_sms_number=None,
                         otp_pin_length=None,
                         hash_algorithm=None,
                         cert_id=None):
    """
    Generates a pairing url that should be sent to the client.

    Mandatory parameters:

    :param: token_type The token type for which this url is generated
        as a string (currently supported is only 'qr')

    :param: server_public_key: The servers public key as bytes (length: 32)

    Optional parameters:

    :param serial: When a token for the client was already enrolled
        (e.g. manually in the manage interface) its serial has to be
        sent to the client. When serial is not specified the client will
        receive a so-called 'anonymous pairing url' with no token data
        inside it. The token will then be created after the server
        received a pairing response from the client.

    :param callback_url: A callback URL that should be used by the client
        to sent back the pairing reponse. Please note, that this url will
        be cached by the client and used in the challenge step, if the
        challenge doesn't provide a custom url

    :param callback_sms_number: A sms number that can be used by the client
        to send back the pairing response. Typically this is used as a
        fallback for offline pairing.
        As with the callback url please note, that the number will be
        cached by the client. If you want a different number in the
        challenge step you have to send it inside the challenge as
        specified in the challenge protocol

    :param otp_pin_length: The number of digits the otp has to consist of.

    :param hash_algorithm: A string value that signifies the hash algorithm
        used in calculating the hmac. Currently the values 'sha1', 'sha256',
        'sha512' are supported. If the parameter is left out the default
        depends on the token type. qrtoken uses sha256 as default, while
        hotp/totp uses sha1.

    :param cert_id: A certificate id that should be used during pairing.
        default is None.

    The function can raise several exceptions:

    :raises InvalidFunctionParameter: If the string given in token_type
        doesn't match a supported token type

    :raises InvalidFunctionParameter: If the string given in hash_algorithm
        doesn't match a supported hash algorithm

    :raises InvalidFunctionParameter: If public key has a different size
        than 32 bytes

    :raises InvalidFunctionParameter: If otp_pin_length value is not between
        1 and 127


    :return: Pairing URL string
    """

    # --------------------------------------------------------------------------

    # check the token type

    try:
        TOKEN_TYPE = token_types[token_type]
    except KeyError:
        allowed_types = ', '.join(token_types.keys())
        raise InvalidFunctionParameter('token_type',
                                       'Unsupported token type %s. Supported '
                                       'types for pairing are: %s' %
                                       (token_type, allowed_types))

    # --------------------------------------------------------------------------

    # initialize the flag bitmap

    flags = 0

    if cert_id is None:
        flags |= FLAG_PAIR_PK
    if serial is not None:
        flags |= FLAG_PAIR_SERIAL
    if callback_url is not None:
        flags |= FLAG_PAIR_CBURL
    if callback_sms_number is not None:
        flags |= FLAG_PAIR_CBSMS
    if hash_algorithm is not None:
        flags |= FLAG_PAIR_HMAC
    if otp_pin_length is not None:
        flags |= FLAG_PAIR_DIGITS

    # --------------------------------------------------------------------------

    #            ------------------------------
    #  fields   | version | type | flags | ... |
    #            ------------------------------
    #  size     |    1    |  1   |   4   |  ?  |
    #            ------------------------------

    data = struct.pack('<bbI', PAIR_VERSION, TOKEN_TYPE, flags)

    # --------------------------------------------------------------------------

    #            -------------------------------
    #  fields   | ... | server public key | ... |
    #            -------------------------------
    #  size     |  6  |        32         |  ?  |
    #            -------------------------------

    if len(server_public_key) != 32:
        raise InvalidFunctionParameter('server_public_key', 'Public key must '
                                       'be 32 bytes long')

    if flags & FLAG_PAIR_PK:
        data += server_public_key

    # --------------------------------------------------------------------------

    # Depending on flags additional data may be sent. If serial was provided
    # serial will be sent back. If callback url or callback sms was provided
    # the corresponding data will be added, too

    #            --------------------------------------------------------
    #  fields   | ... | serial | NUL | cb url | NUL | cb sms | NUL | ... |
    #            --------------------------------------------------------
    #  size     | 38  |   ?    |  1  |   ?    |  1  |   ?    |  1  |  ?  |
    #            --------------------------------------------------------

    if flags & FLAG_PAIR_SERIAL:
        data += serial.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBURL:
        data += callback_url.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBSMS:
        data += callback_sms_number.encode('utf8') + b'\x00'

    # --------------------------------------------------------------------------

    # Other optional values: allowed pin length of otp (number of digits)
    # and custom hash algorithm

    #            ---------------------------------------
    #  fields   | ... | otp pin length | hash_algorithm |
    #            ---------------------------------------
    #  size     |  ?  |       1        |       1        |
    #            ---------------------------------------

    if flags & FLAG_PAIR_DIGITS:
        if not(6 <= otp_pin_length <= 12):
            raise InvalidFunctionParameter('otp_pin_length', 'Pin length must '
                                           'be in the range 6..12')
        data += struct.pack('<b', otp_pin_length)

    if flags & FLAG_PAIR_HMAC:
        try:
            HASH_ALGO = hash_algorithms[hash_algorithm]
        except KeyError:
            allowed_values = ", ".join(hash_algorithms.keys())
            raise InvalidFunctionParameter('hash_algorithm',
                                           'Unsupported hash algorithm %s, '
                                           'allowed values are %s' %
                                           (hash_algorithm, allowed_values))
        data += struct.pack('<b', HASH_ALGO)

    # --------------------------------------------------------------------------

    # TODO missing token details for other protocols (hotp, hmac, etc)
    # * counter (u64le)
    # * tstart (u64le)
    # * tstep (u32le)

    # TODO: replace lseqr literal with global config value
    # or global constant

    if cert_id is not None:
        secret_key = get_qrtoken_secret_key(cert_id=cert_id)
        server_sig = crypto_sign_detached(data, secret_key)
        data += server_sig

    return 'lseqr://pair/' + encode_base64_urlsafe(data)
Beispiel #32
0
    def decrypt_and_verify_challenge(self, challenge_url, action):
        """
        Decrypts the data packed in the challenge url, verifies
        its content, returns the parsed data as a dictionary,
        calculates and returns the signature.

        The calling method must then send the signature
        back to the server. (The reason for this control flow
        is that the challenge data must be checked in different
        scenarios, e.g. when we have a pairing the data must be
        checked by the method that simulates the pairing)

        :param challenge_url: the challenge url as sent by the server
        :param action: a string identifier for the verification action
            (at the moment 'ACCEPT' or 'DENY')

        :returns: (challenge, signature)

            challenge has the keys

                * content_type - one of the three values CONTENT_TYPE_SIGNREQ,
                    CONTENT_TYPE_PAIRING or CONTENT_TYPE_LOGIN)
                    (all defined in this module)
                * transaction_id - used to identify the challenge
                    on the server
                * callback_url (optional) - the url to which the challenge
                    response should be set
                * user_token_id - used to identify the token in the
                    user database for which this challenge was created

            depending on the content type additional keys are present

                * for CONTENT_TYPE_PAIRING: serial
                * for CONTENT_TYPE_SIGNREQ: message
                * for CONTENT_TYPE_LOGIN: login, host

            signature is the generated user signature used to
            respond to the challenge
        """

        challenge_data_encoded = challenge_url[len(self.uri + '://chal/'):]
        challenge_data = decode_base64_urlsafe(challenge_data_encoded)

        # ------------------------------------------------------------------ --

        # parse and verify header information in the
        # encrypted challenge data

        header = challenge_data[0:5]
        version, user_token_id = struct.unpack('<bI', header)
        self.assertEqual(version, CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        # get token from client token database

        token = self.tokens[user_token_id]
        server_public_key = token['server_public_key']

        # ------------------------------------------------------------------ --

        # prepare decryption by seperating R from
        # ciphertext and server signature

        R = challenge_data[5:5 + 32]
        ciphertext = challenge_data[5 + 32:-64]
        server_signature = challenge_data[-64:]

        # check signature

        data = challenge_data[0:-64]
        crypto_sign_verify_detached(server_signature, data, server_public_key)

        # ------------------------------------------------------------------ --

        # key derivation

        secret_key_dh = dsa_to_dh_secret(self.secret_key)
        ss = calc_dh(secret_key_dh, R)
        U = SHA256.new(ss).digest()

        sk = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # decrypt and verify challenge

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        plaintext = cipher.decrypt(ciphertext)

        # ------------------------------------------------------------------ --

        # parse/check plaintext header

        # 1 - for content type
        # 8 - for transaction id
        # 8 - for time stamp
        offset = 1 + 8 + 8

        pt_header = plaintext[0:offset]
        (content_type, transaction_id,
         _time_stamp) = struct.unpack('<bQQ', pt_header)

        transaction_id = u64_to_transaction_id(transaction_id)

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge = {}
        challenge['content_type'] = content_type

        # ------------------------------------------------------------------ --

        # retrieve plaintext data depending on content_type

        if content_type == CONTENT_TYPE_PAIRING:

            serial, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['serial'] = serial

        elif content_type == CONTENT_TYPE_SIGNREQ:

            message, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['message'] = message

        elif content_type == CONTENT_TYPE_LOGIN:

            login, host, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['login'] = login
            challenge['host'] = host

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge['callback_url'] = callback_url
        challenge['transaction_id'] = transaction_id
        challenge['user_token_id'] = user_token_id

        # calculate signature

        sig_base = (struct.pack('<b', CHALLENGE_URL_VERSION) +
                    b'%s\0' % action + server_signature + plaintext)

        sig = crypto_sign_detached(sig_base, self.secret_key)
        encoded_sig = encode_base64_urlsafe(sig)

        return challenge, encoded_sig
Beispiel #33
0
secret_key = base64.b64decode(secret_key)

h = pyblake2.blake2b(data=b"hypercore", key=pub_key, digest_size=32)
h.hexdigest()

base64.b16encode(bytearray([43, 235, 230, 134, 54, 24, 197, 134, 8, 109, 37, 32, 170, 13, 19, 218, 102, 107, 0, 46, 90, 210, 177, 98, 35, 161, 91, 193, 85, 134, 241, 228, 0, 0, 0, 0, 0, 0, 0, 204])).lower()

h = pyblake2.blake2b(digest_size=32)
h.update([2])
h.update(bytearray([0x02]))
h.update(base64.b16decode("67179c243b387b7c7c420bd7bdc69d35c061b979bb365f9cc07b4527eeddeefa".upper()))
h.update(bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
h.update(bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42]))
h.hexdigest()

base64.b16encode(pysodium.crypto_sign_detached(h.digest(), secret_key)).lower()

h = pyblake2.blake2b(digest_size=32)
h.update(bytearray([0x02]))
h.update(base64.b16decode("ab27d45f509274ce0d08f4f09ba2d0e0d8df61a0c2a78932e81b5ef26ef398df".upper()))
h.update(bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
h.update(bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]))
h.hexdigest()

sk = base64.b16decode("53729c0311846cca9cc0eded07aaf9e6689705b6a0b1bb8c3a2a839b72fda3839718a1ff1c4ca79feac551c0c7212a65e4091278ec886b88be01ee4039682238".upper())
base64.b16encode(pysodium.crypto_sign_detached(h.digest(), sk)).lower()

lh = pyblake2.blake2b(digest_size=32)
lh.update(bytearray([0x00]))
lh.update(bytearray([0, 0, 0, 0, 0, 0, 0, 1]))
hex(ord('a'))
    def create_pairing_response(public_key: bytes,
                                secret_key: bytes,
                                token_info: Dict,
                                gda: str = 'DEADBEEF') -> str:
        """Creates a base64-encoded pairing response.

        :param public_key: the public key in bytes
        :param secret_key: the secret key in bytes
        :param token_info: the token_info dict
        :param user_token_id: the token id
        :param gda: the mobile device gda

        :returns base64 encoded pairing response
        """

        token_serial = token_info['serial']
        token_id = token_info.get('token_id', 1)
        server_public_key = token_info['server_public_key']
        partition = token_info['partition']

        # ------------------------------------------------------------------ --

        # assemble header and plaintext

        header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition)

        pairing_response = b''
        pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, token_id)

        pairing_response += public_key

        pairing_response += token_serial.encode('utf8') + b'\x00\x00'
        pairing_response += gda.encode('utf-8') + b'\x00'

        signature = crypto_sign_detached(pairing_response, secret_key)
        pairing_response += signature

        # ------------------------------------------------------------------ --

        # create public diffie hellman component
        # (used to decrypt and verify the reponse)

        r = os.urandom(32)
        R = calc_dh_base(r)

        # ------------------------------------------------------------------ --

        # derive encryption key and nonce

        server_public_key_dh = dsa_to_dh_public(server_public_key)
        ss = calc_dh(r, server_public_key_dh)
        U = SHA256.new(ss).digest()
        encryption_key = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # encrypt in EAX mode

        cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
        cipher.update(header)
        ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

        return encode_base64_urlsafe(header + R + ciphertext + tag)
Beispiel #35
0
    def create_challenge_url(self,
                             transaction_id,
                             content_type,
                             callback_url='',
                             message=None,
                             login=None,
                             host=None):

        """
        creates a challenge url (looking like lseqr://push/<base64string>),
        returns the url and the unencrypted challenge data

        :param transaction_id: The transaction id generated by LinOTP

        :param content_type: One of the types CONTENT_TYPE_SIGNREQ,
            CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN

        :param callback_url: callback url (optional), default is
            empty string

        :param message: the transaction message, that should be signed
            by the client. Only for content type CONTENT_TYPE_SIGNREQ

        :param login: the login name of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :param host: hostname of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :returns: tuple (challenge_url, sig_base), with challenge_url being
            the push url and sig_base the message, that is used for
            the client signature
        """

        serial = self.getSerial()

        # ------------------------------------------------------------------- --

        # sanity/format checks

        if content_type not in [CONTENT_TYPE_SIGNREQ,
                                CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN]:
            raise InvalidFunctionParameter('content_type', 'content_type must '
                                           'be CONTENT_TYPE_SIGNREQ, '
                                           'CONTENT_TYPE_PAIRING or '
                                           'CONTENT_TYPE_LOGIN.')

        # ------------------------------------------------------------------- --

        #  after the lseqr://push/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | sign |
        #            ---------------------------------------------------
        #           |          header         |          body           |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     |  64  |
        #            ---------------------------------------------------
        #

        # create header

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', CHALLENGE_URL_VERSION, user_token_id)

        # ------------------------------------------------------------------- --

        # create body

        r = urandom(32)
        R = calc_dh_base(r)

        b64_user_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_user_dsa_public_key)
        user_dh_public_key = dsa_to_dh_public(user_dsa_public_key)

        ss = calc_dh(r, user_dh_public_key)
        U = SHA256.new(ss).digest()
        zerome(ss)

        sk = U[0:16]
        nonce = U[16:32]
        zerome(U)

        # ------------------------------------------------------------------- --

        # create plaintext section

        # ------------------------------------------------------------------- --

        # generate plaintext header

        #            ------------------------------------------------
        #  fields   | content_type  | transaction_id | timestamp | ..
        #            ------------------------------------------------
        #  size     |       1       |        8       |     8     |  ?
        #            -------------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        plaintext = struct.pack('<bQQ', content_type, transaction_id,
                                int(time.time()))

        # ------------------------------------------------------------------- --

        utf8_callback_url = callback_url.encode('utf8')

        # enforce max url length as specified in protocol

        if len(utf8_callback_url) > 511:
            raise InvalidFunctionParameter('callback_url', 'max string '
                                           'length (encoded as utf8) is '
                                           '511')

        # ------------------------------------------------------------------- --

        # create data package depending on content type

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_PAIRING:

            #            -----------------------------------------
            #  fields   | header | serial | NUL | callback | NUL |
            #            -----------------------------------------
            #  size     |   9    |    ?   |  1  |     ?    |  1  |
            #            -----------------------------------------

            utf8_serial = serial.encode('utf8')

            if len(utf8_serial) > 63:
                raise ValueError('serial (encoded as utf8) can only be 63 '
                                 'characters long')

            plaintext += utf8_serial + b'\00' + utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_SIGNREQ:

            if message is None:
                raise InvalidFunctionParameter('message', 'message must be '
                                               'supplied for content type '
                                               'SIGNREQ')

            #            ------------------------------------------
            #  fields   | header | message | NUL | callback | NUL |
            #            ------------------------------------------
            #  size     |   9    |    ?    |  1  |     ?    |  1  |
            #            ------------------------------------------

            utf8_message = message.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_message) > 511:
                raise InvalidFunctionParameter('message', 'max string '
                                               'length (encoded as utf8) is '
                                               '511')

            plaintext += utf8_message + b'\00' + utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_LOGIN:

            if login is None:
                raise InvalidFunctionParameter('login', 'login must be '
                                               'supplied for content type '
                                               'LOGIN')
            if host is None:
                raise InvalidFunctionParameter('host', 'host must be '
                                               'supplied for content type '
                                               'LOGIN')

            #            -----------------------------------------------------
            #  fields   | header | login | NUL | host | NUL | callback | NUL |
            #            -----------------------------------------------------
            #  size     |   9    |   ?   |  1  |   ?  |  1  |     ?    |  1  |
            #            -----------------------------------------------------

            utf8_login = login.encode('utf8')
            utf8_host = host.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_login) > 127:
                raise InvalidFunctionParameter('login', 'max string '
                                               'length (encoded as utf8) is '
                                               '127')
            if len(utf8_host) > 255:
                raise InvalidFunctionParameter('host', 'max string '
                                               'length (encoded as utf8) is '
                                               '255')

            plaintext += utf8_login + b'\00'
            plaintext += utf8_host + b'\00'
            plaintext += utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        # encrypt inner layer

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        ciphertext = cipher.encrypt(plaintext)
        unsigned_raw_data = data_header + R + ciphertext

        # ------------------------------------------------------------------- --

        # create signature

        partition = self.getFromTokenInfo('partition')
        secret_key = get_secret_key(partition)
        signature = crypto_sign_detached(unsigned_raw_data, secret_key)
        raw_data = unsigned_raw_data + signature

        protocol_id = config.get('mobile_app_protocol_id', 'lseqr')
        url = protocol_id + '://push/' + encode_base64_urlsafe(raw_data)

        return url, (signature + plaintext)
Beispiel #36
0
    def decrypt_and_verify_challenge(self, challenge_url, action):

        """
        Decrypts the data packed in the challenge url, verifies
        its content, returns the parsed data as a dictionary,
        calculates and returns the signature.

        The calling method must then send the signature
        back to the server. (The reason for this control flow
        is that the challenge data must be checked in different
        scenarios, e.g. when we have a pairing the data must be
        checked by the method that simulates the pairing)

        :param challenge_url: the challenge url as sent by the server
        :param action: a string identifier for the verification action
            (at the moment 'ACCEPT' or 'DENY')

        :returns: (challenge, signature)

            challenge has the keys

                * content_type - one of the three values CONTENT_TYPE_SIGNREQ,
                    CONTENT_TYPE_PAIRING or CONTENT_TYPE_LOGIN)
                    (all defined in this module)
                * transaction_id - used to identify the challenge
                    on the server
                * callback_url (optional) - the url to which the challenge
                    response should be set
                * user_token_id - used to identify the token in the
                    user database for which this challenge was created

            depending on the content type additional keys are present

                * for CONTENT_TYPE_PAIRING: serial
                * for CONTENT_TYPE_SIGNREQ: message
                * for CONTENT_TYPE_LOGIN: login, host

            signature is the generated user signature used to
            respond to the challenge
        """

        challenge_data_encoded = challenge_url[len(self.uri + '://chal/'):]
        challenge_data = decode_base64_urlsafe(challenge_data_encoded)

        # ------------------------------------------------------------------ --

        # parse and verify header information in the
        # encrypted challenge data

        header = challenge_data[0:5]
        version, user_token_id = struct.unpack('<bI', header)
        self.assertEqual(version, CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        # get token from client token database

        token = self.tokens[user_token_id]
        server_public_key = token['server_public_key']

        # ------------------------------------------------------------------ --

        # prepare decryption by seperating R from
        # ciphertext and server signature

        R = challenge_data[5:5 + 32]
        ciphertext = challenge_data[5 + 32:-64]
        server_signature = challenge_data[-64:]

        # check signature

        data = challenge_data[0:-64]
        crypto_sign_verify_detached(server_signature, data, server_public_key)

        # ------------------------------------------------------------------ --

        # key derivation

        secret_key_dh = dsa_to_dh_secret(self.secret_key)
        ss = calc_dh(secret_key_dh, R)
        U = SHA256.new(ss).digest()

        sk = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # decrypt and verify challenge

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        plaintext = cipher.decrypt(ciphertext)

        # ------------------------------------------------------------------ --

        # parse/check plaintext header

        # 1 - for content type
        # 8 - for transaction id
        # 8 - for time stamp
        offset = 1 + 8 + 8

        pt_header = plaintext[0:offset]
        (content_type,
         transaction_id,
         _time_stamp) = struct.unpack('<bQQ', pt_header)

        transaction_id = u64_to_transaction_id(transaction_id)

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge = {}
        challenge['content_type'] = content_type

        # ------------------------------------------------------------------ --

        # retrieve plaintext data depending on content_type

        if content_type == CONTENT_TYPE_PAIRING:

            serial, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['serial'] = serial

        elif content_type == CONTENT_TYPE_SIGNREQ:

            message, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['message'] = message

        elif content_type == CONTENT_TYPE_LOGIN:

            login, host, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['login'] = login
            challenge['host'] = host

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge['callback_url'] = callback_url
        challenge['transaction_id'] = transaction_id
        challenge['user_token_id'] = user_token_id

        # calculate signature

        sig_base = (
            struct.pack('<b', CHALLENGE_URL_VERSION) +
            b'%s\0' % action +
            server_signature + plaintext)

        sig = crypto_sign_detached(sig_base, self.secret_key)
        encoded_sig = encode_base64_urlsafe(sig)

        return challenge, encoded_sig