Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: generate TOTP secret key from shared secret using ECDH #37

Closed
KEINOS opened this issue Apr 27, 2024 · 1 comment
Closed

Feature: generate TOTP secret key from shared secret using ECDH #37

KEINOS opened this issue Apr 27, 2024 · 1 comment

Comments

@KEINOS
Copy link
Owner

KEINOS commented Apr 27, 2024

ECDH can establish a shared secret between two asymmetric key pairs. (key agreement.)

We can use this ECDH shared secret as a secret key of TOTP to obtain common time-based ephemeral value between two parties.

A value that can be used as; a common SALT for hashing, cryptographic key for symmetric-key encryption and etc.

Here's a working pseudo code:

package main

import (
	"fmt"
	"log"

	"crypto/ecdh"
	"crypto/rand"

	"github.com/KEINOS/go-totp/totp"
	"github.com/zeebo/blake3"
)

func main() {
	// -----------------------------------------------------------------------------
	//  Generate ECDH key pair using curve25519 (X25519)
	// -----------------------------------------------------------------------------

	// Agreed curve to use between Alice and Bob
	paramCommon := ecdh.X25519()

	// Alice ECDH key pair
	alicePrivKey, alicePubKey, err := newECDH(paramCommon)
	panicOnErr(err)

	// Bob ECDH key pair
	bobPrivKey, bobPubKey, err := newECDH(paramCommon)
	panicOnErr(err)

	fmt.Printf("Alice Priv: %x\n", alicePrivKey.Bytes())
	fmt.Printf("Alice Pub : %x\n", alicePubKey.Bytes())
	fmt.Printf("Bob Priv  : %x\n", bobPrivKey.Bytes())
	fmt.Printf("Bob Pub   : %x\n", bobPubKey.Bytes())

	// -----------------------------------------------------------------------------
	//  Calculate shared secret
	// -----------------------------------------------------------------------------
	// (we assume that public keys were safely exchanged/retrieved and trustworthy)

	// Alice shared secret
	aliceSecret, err := alicePrivKey.ECDH(bobPubKey)
	panicOnErr(err)

	// Bob shared secret
	bobSecret, err := bobPrivKey.ECDH(alicePubKey)
	panicOnErr(err)

	fmt.Printf("Alice's Shared Secret: %x (len: %v)\n", aliceSecret, len(aliceSecret))
	fmt.Printf("Bob's Shared Secret  : %x (len: %v)\n", bobSecret, len(bobSecret))

	// -----------------------------------------------------------------------------
	//  Hash the ECDH shared secret to 128 byte length using constant context as
	//  salt and use it as the secret of TOTP key
	// -----------------------------------------------------------------------------
	const ctxSalt = "example.com shared TOTP secret v1"

	// Alice TOTP secret key
	aliceTOTPSec := hash1024(aliceSecret, ctxSalt)

	// Bob TOTP secret key
	bobTOTPSec := hash1024(aliceSecret, ctxSalt)

	fmt.Printf("Alice TOTP sec key: %x...%x (len: %v)\n", aliceTOTPSec[:16], aliceTOTPSec[112:], len(aliceTOTPSec))
	fmt.Printf("Bob TOTP sec key  : %x...%x (len: %v)\n", bobTOTPSec[:16], bobTOTPSec[112:], len(bobTOTPSec))

	// -----------------------------------------------------------------------------
	//  Instanticate totp.Key object using the converted TOTP secret key above
	// -----------------------------------------------------------------------------

	// Alice TOTP key
	aliceKey, err := newTOTP("Example.com", "[email protected]", aliceTOTPSec)
	panicOnErr(err)

	// Bob TOTP key
	bobKey, err := newTOTP("Example.com", "[email protected]", bobTOTPSec)
	panicOnErr(err)

	// -----------------------------------------------------------------------------
	//  Generate TOTP pass code
	// -----------------------------------------------------------------------------

	// Alice generates 6 digits passcode (valid for 30 seconds)
	alicePass, err := aliceKey.PassCode()
	panicOnErr(err)

	// Bob generates 6 digits passcode (valid for 30 seconds)
	bobPass, err := bobKey.PassCode()
	panicOnErr(err)

	fmt.Println("Alice pass:", alicePass)
	fmt.Println("Bob pass  :", bobPass)

	// -----------------------------------------------------------------------------
	//  Validate CODE between alice and bob
	// -----------------------------------------------------------------------------

	// Alice validates the passcode from Bob
	if aliceKey.Validate(bobPass) {
		fmt.Println("Alice: Passcode is valid")
	}

	// Bob validates the passcode from Alice
	if bobKey.Validate(alicePass) {
		fmt.Println("Bob  : Passcode is valid")
	}

	// Output:
	// Alice Priv: 07b0ff9dc25884eb20af588269697001f9a580f96a7d318f21e0aa466f22da0a
	// Alice Pub : bbf54e526eb5ffd40e2e4791b2715eef8f51fd8f1817c9b7a1d9739a038b0647
	// Bob Priv  : 418c8e9102f8d695bec35be99f53eda380f9d7990a5447bc66dff87226135c16
	// Bob Pub   : 0b0f1cccc640e8ef08a0043b37d3e8f7a4fc4d027ccb433b053cb18b8f43bc66
	// Alice's Shared Secret: b9c22821ff75038ffa4fa2dddfc4af06c3797df7b92f87ade36952c515dab424 (len: 32)
	// Bob's Shared Secret  : b9c22821ff75038ffa4fa2dddfc4af06c3797df7b92f87ade36952c515dab424 (len: 32)
	// Alice TOTP sec key: 5fad6143564261a75e9cedeeb7b9f653...790e6bcf035d552c2817ed60dd9440a5 (len: 128)
	// Bob TOTP sec key  : 5fad6143564261a75e9cedeeb7b9f653...790e6bcf035d552c2817ed60dd9440a5 (len: 128)
	// Alice pass: 49641015
	// Bob pass  : 49641015
	// Alice: Passcode is valid
	// Bob  : Passcode is valid
}

func newECDH(curveType ecdh.Curve) (*ecdh.PrivateKey, *ecdh.PublicKey, error) {
	privKey, err := curveType.GenerateKey(rand.Reader)
	if err != nil {
		return nil, nil, err
	}

	pubKey := privKey.PublicKey()

	return privKey, pubKey, nil
}

// hash1024 hashes totpSec with ctx to 128 byte (1024 bit) length.
//
// ctx (context strings) must be hardcoded constants, and the recommended format
// is "[application] [commit timestamp] [purpose]",
//
//	e.g., "example.com 2019-12-25 16:18:03 session tokens v1".
func hash1024(totpSec []byte, ctx string) []byte {
	const outLen = 128

	outHash := make([]byte, outLen)

	blake3.DeriveKey(
		ctx,     // context
		totpSec, // material
		outHash,
	)

	return outHash
}

func newTOTP(issuer, accountName string, totpSec []byte) (*totp.Key, error) {
	totpKey, err := totp.GenerateKeyCustom(totp.Options{
		Issuer:      issuer,
		AccountName: accountName,
		Algorithm:   totp.Algorithm("SHA256"), // use SHA256 for HMAC
		Period:      30,                       // valid for 30 sec
		SecretSize:  128,                      // len TOTP key
		Skew:        0,                        // ± 0 time tolerance
		Digits:      totp.DigitsEight,         // 8 digits pass code
	})
	if err != nil {
		return nil, err
	}

	totpKey.Secret = totp.Secret(totpSec)

	return totpKey, nil
}

func panicOnErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
@KEINOS
Copy link
Owner Author

KEINOS commented Apr 30, 2024

We can not implement ECDH unless we bump-up our go.mod Go version higher than 1.20.

https://github.com/KEINOS/go-totp/actions/runs/8887607444/job/24403155510#step:9:11

@KEINOS KEINOS closed this as completed in 9383003 Apr 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant