Merge pull request #44 from depools/feat/airgapped-state

feat: airgapped state
This commit is contained in:
Andrew Zavgorodny 2020-09-29 16:52:02 +03:00 committed by GitHub
commit 2f5419ffba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 667 additions and 135 deletions

View File

@ -12,18 +12,18 @@ mocks:
build-darwin:
@echo "Building dc4bc_d..."
GOOS=darwin GOARCH=amd64 go build -o dc4bc_d_darwin ./cmd/dc4bc_d/main.go
GOOS=darwin GOARCH=amd64 go build -o dc4bc_d_darwin ./cmd/dc4bc_d/
@echo "Building dc4bc_cli..."
GOOS=darwin GOARCH=amd64 go build -o dc4bc_cli_darwin ./cmd/dc4bc_cli/main.go
GOOS=darwin GOARCH=amd64 go build -o dc4bc_cli_darwin ./cmd/dc4bc_cli/
@echo "Building dc4bc_airgapped..."
GOOS=darwin GOARCH=amd64 go build -o dc4bc_airgapped_darwin ./cmd/airgapped/main.go
GOOS=darwin GOARCH=amd64 go build -o dc4bc_airgapped_darwin ./cmd/airgapped/
build-linux:
@echo "Building dc4bc_d..."
GOOS=linux GOARCH=amd64 go build -o dc4bc_d_linux ./cmd/dc4bc_d/main.go
GOOS=linux GOARCH=amd64 go build -o dc4bc_d_linux ./cmd/dc4bc_d/
@echo "Building dc4bc_cli..."
GOOS=linux GOARCH=amd64 go build -o dc4bc_cli_linux ./cmd/dc4bc_cli/main.go
GOOS=linux GOARCH=amd64 go build -o dc4bc_cli_linux ./cmd/dc4bc_cli/
@echo "Building dc4bc_airgapped..."
GOOS=linux GOARCH=amd64 go build -o dc4bc_airgapped_linux ./cmd/airgapped/main.go
GOOS=linux GOARCH=amd64 go build -o dc4bc_airgapped_linux ./cmd/airgapped/
.PHONY: mocks

View File

@ -1,7 +1,6 @@
package airgapped
import (
"crypto/rand"
"encoding/json"
"fmt"
"log"
@ -20,15 +19,12 @@ import (
"github.com/depools/dc4bc/fsm/state_machines/signing_proposal_fsm"
"github.com/depools/dc4bc/fsm/types/requests"
"github.com/depools/dc4bc/qr"
bls12381 "github.com/depools/kyber-bls12381"
"github.com/syndtr/goleveldb/leveldb"
)
const (
resultQRFolder = "result_qr_codes"
pubKeyDBKey = "public_key"
privateKeyDBKey = "private_key"
saltKey = "salt_key"
resultQRFolder = "result_qr_codes"
seedSize = 32
)
type AirgappedMachine struct {
@ -40,7 +36,8 @@ type AirgappedMachine struct {
encryptionKey []byte
pubKey kyber.Point
secKey kyber.Scalar
suite vss.Suite
baseSuite vss.Suite
baseSeed []byte
db *leveldb.DB
}
@ -61,12 +58,25 @@ func NewAirgappedMachine(dbPath string) (*AirgappedMachine, error) {
qrProcessor: qr.NewCameraProcessor(),
}
am.suite = bls12381.NewBLS12381Suite()
if am.db, err = leveldb.OpenFile(dbPath, nil); err != nil {
return nil, fmt.Errorf("failed to open db file %s for keys: %w", dbPath, err)
}
if err := am.loadBaseSeed(); err != nil {
return nil, fmt.Errorf("failed to loadBaseSeed: %w", err)
}
if _, err = am.db.Get([]byte(operationsLogDBKey), nil); err != nil {
if err == leveldb.ErrNotFound {
operationsLogBz, _ := json.Marshal(RoundOperationLog{})
if err := am.db.Put([]byte(operationsLogDBKey), operationsLogBz, nil); err != nil {
return nil, fmt.Errorf("failed to init Operation log: %w", err)
}
} else {
return nil, fmt.Errorf("failed to init Operation log (fatal): %w", err)
}
}
return am, nil
}
@ -78,8 +88,8 @@ func (am *AirgappedMachine) InitKeys() error {
}
// if keys were not generated yet
if err == leveldb.ErrNotFound {
am.secKey = am.suite.Scalar().Pick(am.suite.RandomStream())
am.pubKey = am.suite.Point().Mul(am.secKey, nil)
am.secKey = am.baseSuite.Scalar().Pick(am.baseSuite.RandomStream())
am.pubKey = am.baseSuite.Point().Mul(am.secKey, nil)
return am.SaveKeysToDB()
}
@ -107,100 +117,27 @@ func (am *AirgappedMachine) DropSensitiveData() {
am.encryptionKey = nil
}
// LoadKeysFromDB load DKG keys from LevelDB
func (am *AirgappedMachine) LoadKeysFromDB() error {
pubKeyBz, err := am.db.Get([]byte(pubKeyDBKey), nil)
func (am *AirgappedMachine) ReplayOperationsLog(dkgIdentifier string) error {
operationsLog, err := am.getOperationsLog(dkgIdentifier)
if err != nil {
if err == leveldb.ErrNotFound {
return err
return fmt.Errorf("failed to getOperationsLog: %w", err)
}
for _, operation := range operationsLog {
if _, err := am.HandleOperation(operation); err != nil {
return fmt.Errorf(
"failed to HandleOperation %s (this error is fatal, the state can not be recovered): %w",
operation.ID, err)
}
return fmt.Errorf("failed to get public key from db: %w", err)
}
privateKeyBz, err := am.db.Get([]byte(privateKeyDBKey), nil)
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to get private key from db: %w", err)
}
log.Println("Successfully replayed Operation log")
salt, err := am.db.Get([]byte(saltKey), nil)
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to read salt from db: %w", err)
}
decryptedPubKey, err := decrypt(am.encryptionKey, salt, pubKeyBz)
if err != nil {
return err
}
decryptedPrivateKey, err := decrypt(am.encryptionKey, salt, privateKeyBz)
if err != nil {
return err
}
am.pubKey = am.suite.Point()
if err = am.pubKey.UnmarshalBinary(decryptedPubKey); err != nil {
return fmt.Errorf("failed to unmarshal public key: %w", err)
}
am.secKey = am.suite.Scalar()
if err = am.secKey.UnmarshalBinary(decryptedPrivateKey); err != nil {
return fmt.Errorf("failed to unmarshal private key: %w", err)
}
return nil
}
// SaveKeysToDB save DKG keys to LevelDB
func (am *AirgappedMachine) SaveKeysToDB() error {
pubKeyBz, err := am.pubKey.MarshalBinary()
if err != nil {
return fmt.Errorf("failed to marshal pub key: %w", err)
}
privateKeyBz, err := am.secKey.MarshalBinary()
if err != nil {
return fmt.Errorf("failed to marshal private key: %w", err)
}
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return fmt.Errorf("failed to generate salt: %w", err)
}
encryptedPubKey, err := encrypt(am.encryptionKey, salt, pubKeyBz)
if err != nil {
return err
}
encryptedPrivateKey, err := encrypt(am.encryptionKey, salt, privateKeyBz)
if err != nil {
return err
}
tx, err := am.db.OpenTransaction()
if err != nil {
return fmt.Errorf("failed to open transcation for db: %w", err)
}
defer tx.Discard()
if err = tx.Put([]byte(pubKeyDBKey), encryptedPubKey, nil); err != nil {
return fmt.Errorf("failed to put pub key into db: %w", err)
}
if err = tx.Put([]byte(privateKeyDBKey), encryptedPrivateKey, nil); err != nil {
return fmt.Errorf("failed to put private key into db: %w", err)
}
if err = tx.Put([]byte(saltKey), salt, nil); err != nil {
return fmt.Errorf("failed to put salt into db: %w", err)
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("failed to commit tx for saving keys into db: %w", err)
}
return nil
func (am *AirgappedMachine) DropOperationsLog(dkgIdentifier string) error {
return am.dropRoundOperationLog(dkgIdentifier)
}
// getParticipantID returns our own participant id for the given DKG round
@ -224,7 +161,7 @@ func (am *AirgappedMachine) encryptDataForParticipant(dkgIdentifier, to string,
return nil, fmt.Errorf("failed to get pk for participant %s: %w", to, err)
}
encryptedData, err := ecies.Encrypt(am.suite, pk, data, am.suite.Hash)
encryptedData, err := ecies.Encrypt(am.baseSuite, pk, data, am.baseSuite.Hash)
if err != nil {
return nil, fmt.Errorf("failed to encrypt data: %w", err)
}
@ -233,7 +170,7 @@ func (am *AirgappedMachine) encryptDataForParticipant(dkgIdentifier, to string,
// decryptDataFromParticipant decrypts the data that was sent to us
func (am *AirgappedMachine) decryptDataFromParticipant(data []byte) ([]byte, error) {
decryptedData, err := ecies.Decrypt(am.suite, am.secKey, data, am.suite.Hash)
decryptedData, err := ecies.Decrypt(am.baseSuite, am.secKey, data, am.baseSuite.Hash)
if err != nil {
return nil, fmt.Errorf("failed to decrypt data: %w", err)
}
@ -242,6 +179,14 @@ func (am *AirgappedMachine) decryptDataFromParticipant(data []byte) ([]byte, err
// HandleOperation handles and processes an operation
func (am *AirgappedMachine) HandleOperation(operation client.Operation) (client.Operation, error) {
if err := am.storeOperation(operation); err != nil {
return client.Operation{}, fmt.Errorf("failed to storeOperation: %w", err)
}
return am.handleOperation(operation)
}
func (am *AirgappedMachine) handleOperation(operation client.Operation) (client.Operation, error) {
var (
err error
)

View File

@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
client "github.com/depools/dc4bc/client/types"
"github.com/depools/dc4bc/fsm/fsm"
"github.com/depools/dc4bc/fsm/state_machines/dkg_proposal_fsm"
@ -310,12 +312,259 @@ func TestAirgappedAllSteps(t *testing.T) {
fmt.Println("DKG succeeded, signature recovered")
}
func TestAirgappedMachine_Replay(t *testing.T) {
testDir := "/tmp/airgapped_test"
nodesCount := 2
threshold := 2
participants := make([]string, nodesCount)
for i := 0; i < nodesCount; i++ {
participants[i] = fmt.Sprintf("Participant#%d", i)
}
tr := &Transport{}
for i := 0; i < nodesCount; i++ {
am, err := NewAirgappedMachine(fmt.Sprintf("%s/%s-%d", testDir, testDB, i))
if err != nil {
t.Fatalf("failed to create airgapped machine: %v", err)
}
am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i)))
if err = am.InitKeys(); err != nil {
t.Fatalf(err.Error())
}
node := Node{
ParticipantID: i,
Participant: participants[i],
Machine: am,
}
tr.nodes = append(tr.nodes, &node)
}
defer os.RemoveAll(testDir)
var initReq responses.SignatureProposalParticipantInvitationsResponse
for _, n := range tr.nodes {
pubKey, err := n.Machine.pubKey.MarshalBinary()
if err != nil {
t.Fatalf("failed to marshal dkg pubkey: %v", err)
}
entry := &responses.SignatureProposalParticipantInvitationEntry{
ParticipantId: n.ParticipantID,
Addr: n.Participant,
Threshold: threshold,
DkgPubKey: pubKey,
}
initReq = append(initReq, entry)
}
op := createOperation(t, string(signature_proposal_fsm.StateAwaitParticipantsConfirmations), "", initReq)
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
_, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
})
// get commits
var getCommitsRequest responses.DKGProposalPubKeysParticipantResponse
for _, n := range tr.nodes {
pubKey, err := n.Machine.pubKey.MarshalBinary()
if err != nil {
t.Fatalf("%s: failed to marshal pubkey: %v", n.Participant, err)
}
entry := &responses.DKGProposalPubKeysParticipantEntry{
ParticipantId: n.ParticipantID,
Addr: n.Participant,
DkgPubKey: pubKey,
}
getCommitsRequest = append(getCommitsRequest, entry)
}
op = createOperation(t, string(dkg_proposal_fsm.StateDkgCommitsAwaitConfirmations), "", getCommitsRequest)
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
//deals
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.DKGProposalCommitParticipantResponse
for _, req := range n.commits {
p := responses.DKGProposalCommitParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
DkgCommit: req.Commit,
}
payload = append(payload, &p)
}
op := createOperation(t, string(dkg_proposal_fsm.StateDkgDealsAwaitConfirmations), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
// At this point something goes wrong and we have to restart the machines.
for _, node := range tr.nodes {
_ = node.Machine.db.Close()
}
participants = make([]string, nodesCount)
for i := 0; i < nodesCount; i++ {
participants[i] = fmt.Sprintf("Participant#%d", i)
}
newTr := &Transport{}
for i := 0; i < nodesCount; i++ {
am, err := NewAirgappedMachine(fmt.Sprintf("%s/%s-%d", testDir, testDB, i))
if err != nil {
t.Fatalf("failed to create airgapped machine: %v", err)
}
am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i)))
if err = am.InitKeys(); err != nil {
t.Fatalf(err.Error())
}
node := Node{
ParticipantID: i,
Participant: participants[i],
Machine: am,
deals: tr.nodes[i].deals,
}
newTr.nodes = append(newTr.nodes, &node)
}
defer os.RemoveAll(testDir)
for _, node := range newTr.nodes {
err := node.Machine.ReplayOperationsLog(DKGIdentifier)
require.NoError(t, err)
}
//oldTr := tr
tr = newTr
//responses
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.DKGProposalDealParticipantResponse
for _, req := range n.deals {
p := responses.DKGProposalDealParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
DkgDeal: req.Deal,
}
payload = append(payload, &p)
}
op := createOperation(t, string(dkg_proposal_fsm.StateDkgResponsesAwaitConfirmations), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
//master key
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.DKGProposalResponseParticipantResponse
for _, req := range n.responses {
p := responses.DKGProposalResponseParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
DkgResponse: req.Response,
}
payload = append(payload, &p)
}
op := createOperation(t, string(dkg_proposal_fsm.StateDkgMasterKeyAwaitConfirmations), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
// check that all master keys are equal
for _, n := range tr.nodes {
for i := 0; i < len(n.masterKeys); i++ {
if !bytes.Equal(n.masterKeys[0].MasterKey, n.masterKeys[i].MasterKey) {
t.Fatalf("master keys is not equal!")
}
}
}
msgToSign := []byte("i am a message")
//partialSigns
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
payload := responses.SigningPartialSignsParticipantInvitationsResponse{
SrcPayload: msgToSign,
}
op := createOperation(t, string(signing_proposal_fsm.StateSigningAwaitPartialSigns), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
//recover full signature
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.SigningProcessParticipantResponse
for _, req := range n.partialSigns {
p := responses.SigningProcessParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
PartialSign: req.PartialSign,
}
payload.Participants = append(payload.Participants, &p)
}
payload.SrcPayload = msgToSign
op := createOperation(t, string(signing_proposal_fsm.StateSigningPartialSignsCollected), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
fmt.Println("DKG succeeded, signature recovered")
}
func runStep(transport *Transport, cb func(n *Node, wg *sync.WaitGroup)) {
var wg = &sync.WaitGroup{}
for _, node := range transport.nodes {
wg.Add(1)
n := node
go cb(n, wg)
cb(n, wg)
}
wg.Wait()
}

View File

@ -118,7 +118,7 @@ func (am *AirgappedMachine) createPartialSign(msg []byte, dkgIdentifier string)
return nil, fmt.Errorf("failed to load blsKeyring: %w", err)
}
return tbls.Sign(am.suite.(pairing.Suite), blsKeyring.Share, msg)
return tbls.Sign(am.baseSuite.(pairing.Suite), blsKeyring.Share, msg)
}
// recoverFullSign recovers full threshold signature for a message
@ -129,7 +129,7 @@ func (am *AirgappedMachine) recoverFullSign(msg []byte, sigShares [][]byte, t, n
return nil, fmt.Errorf("failed to load blsKeyring: %w", err)
}
return tbls.Recover(am.suite.(pairing.Suite), blsKeyring.PubPoly, msg, sigShares, t, n)
return tbls.Recover(am.baseSuite.(pairing.Suite), blsKeyring.PubPoly, msg, sigShares, t, n)
}
// verifySign verifies a signature of a message
@ -139,5 +139,5 @@ func (am *AirgappedMachine) verifySign(msg []byte, fullSignature []byte, dkgIden
return fmt.Errorf("failed to load blsKeyring: %w", err)
}
return bls.Verify(am.suite.(pairing.Suite), blsKeyring.PubPoly.Commit(), msg, fullSignature)
return bls.Verify(am.baseSuite.(pairing.Suite), blsKeyring.PubPoly.Commit(), msg, fullSignature)
}

View File

@ -1,9 +1,12 @@
package airgapped
import (
"crypto/sha256"
"encoding/json"
"fmt"
bls "github.com/depools/kyber-bls12381"
"github.com/corestario/kyber"
dkgPedersen "github.com/corestario/kyber/share/dkg/pedersen"
client "github.com/depools/dc4bc/client/types"
@ -13,7 +16,6 @@ import (
"github.com/depools/dc4bc/fsm/types/requests"
"github.com/depools/dc4bc/fsm/types/responses"
"github.com/depools/dc4bc/storage"
bls12381 "github.com/depools/kyber-bls12381"
)
func createMessage(o client.Operation, data []byte) storage.Message {
@ -43,7 +45,7 @@ func (am *AirgappedMachine) handleStateAwaitParticipantsConfirmations(o *client.
pid := -1
for _, r := range payload {
pubkey := am.suite.Point()
pubkey := am.baseSuite.Point()
if err := pubkey.UnmarshalBinary(r.DkgPubKey); err != nil {
return fmt.Errorf("failed to unmarshal dkg pubkey: %w", err)
}
@ -60,11 +62,17 @@ func (am *AirgappedMachine) handleStateAwaitParticipantsConfirmations(o *client.
return fmt.Errorf("dkg instance %s already exists", o.DKGIdentifier)
}
dkgInstance := dkg.Init(am.suite, am.pubKey, am.secKey)
// Here we create a new seeded suite for the new DKG round with seed =
// sha256.Sum256(baseSeed + DKGIdentifier). We need this to avoid identical
// DKG rounds.
var (
dkgSeed = sha256.Sum256(append([]byte(o.DKGIdentifier), am.baseSeed...))
suite = bls.NewBLS12381Suite(dkgSeed[:])
)
dkgInstance := dkg.Init(suite, am.pubKey, am.secKey)
dkgInstance.Threshold = payload[0].Threshold //same for everyone
dkgInstance.N = len(payload)
am.dkgInstances[o.DKGIdentifier] = dkgInstance
req := requests.SignatureProposalParticipantRequest{
ParticipantId: pid,
CreatedAt: o.CreatedAt,
@ -101,14 +109,14 @@ func (am *AirgappedMachine) handleStateDkgCommitsAwaitConfirmations(o *client.Op
}
for _, entry := range payload {
pubKey := bls12381.NewBLS12381Suite().Point()
pubKey := am.baseSuite.Point()
if err = pubKey.UnmarshalBinary(entry.DkgPubKey); err != nil {
return fmt.Errorf("failed to unmarshal pubkey: %w", err)
}
dkgInstance.StorePubKey(entry.Addr, entry.ParticipantId, pubKey)
}
if err = dkgInstance.InitDKGInstance(); err != nil {
if err = dkgInstance.InitDKGInstance(am.baseSeed); err != nil {
return fmt.Errorf("failed to init dkg instance: %w", err)
}
@ -165,7 +173,7 @@ func (am *AirgappedMachine) handleStateDkgDealsAwaitConfirmations(o *client.Oper
}
dkgCommits := make([]kyber.Point, 0, len(commitsBz))
for _, commitBz := range commitsBz {
commit := am.suite.Point()
commit := am.baseSuite.Point()
if err = commit.UnmarshalBinary(commitBz); err != nil {
return fmt.Errorf("failed to unmarshal commit: %w", err)
}

242
airgapped/storage.go Normal file
View File

@ -0,0 +1,242 @@
package airgapped
import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log"
bls12381 "github.com/depools/kyber-bls12381"
client "github.com/depools/dc4bc/client/types"
"github.com/syndtr/goleveldb/leveldb"
)
const (
pubKeyDBKey = "public_key"
privateKeyDBKey = "private_key"
saltDBKey = "salt_key"
baseSeedKey = "base_seed_key"
operationsLogDBKey = "operations_log"
)
type RoundOperationLog map[string][]client.Operation
func (am *AirgappedMachine) loadBaseSeed() error {
seed, err := am.getBaseSeed()
if errors.Is(err, leveldb.ErrNotFound) {
log.Println("Base seed not initialized, generating a new one...")
seed = make([]byte, seedSize)
_, err = rand.Read(seed)
if err != nil {
return fmt.Errorf("failed to rand.Read: %w", err)
}
if err := am.storeBaseSeed(seed); err != nil {
return fmt.Errorf("failed to storeBaseSeed: %w", err)
}
log.Println("Successfully generated a new seed")
} else if err != nil {
return fmt.Errorf("failed to getBaseSeed: %w", err)
}
am.baseSeed = seed
am.baseSuite = bls12381.NewBLS12381Suite(am.baseSeed)
return nil
}
func (am *AirgappedMachine) storeBaseSeed(seed []byte) error {
if err := am.db.Put([]byte(baseSeedKey), seed, nil); err != nil {
return fmt.Errorf("failed to put baseSeed: %w", err)
}
return nil
}
func (am *AirgappedMachine) getBaseSeed() ([]byte, error) {
seed, err := am.db.Get([]byte(baseSeedKey), nil)
if err != nil {
return nil, fmt.Errorf("failed to get baseSeed: %w", err)
}
return seed, nil
}
func (am *AirgappedMachine) storeOperation(o client.Operation) error {
roundOperationsLog, err := am.getRoundOperationLog()
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to get operationsLogBz from db: %w", err)
}
operationsLog := roundOperationsLog[o.DKGIdentifier]
operationsLog = append(operationsLog, o)
roundOperationsLog[o.DKGIdentifier] = operationsLog
roundOperationsLogBz, err := json.Marshal(roundOperationsLog)
if err != nil {
return fmt.Errorf("failed to marshal operationsLog: %w", err)
}
if err := am.db.Put([]byte(operationsLogDBKey), roundOperationsLogBz, nil); err != nil {
return fmt.Errorf("failed to put updated operationsLog: %w", err)
}
return nil
}
func (am *AirgappedMachine) getOperationsLog(dkgIdentifier string) ([]client.Operation, error) {
roundOperationsLog, err := am.getRoundOperationLog()
if err != nil {
if err == leveldb.ErrNotFound {
return nil, err
}
return nil, fmt.Errorf("failed to get operationsLogBz from db: %w", err)
}
operationsLog, ok := roundOperationsLog[dkgIdentifier]
if !ok {
return nil, fmt.Errorf("operation log not found for %s", dkgIdentifier)
}
return operationsLog, nil
}
func (am *AirgappedMachine) dropRoundOperationLog(dkgIdentifier string) error {
roundOperationsLog, err := am.getRoundOperationLog()
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to get operationsLogBz from db: %w", err)
}
roundOperationsLog[dkgIdentifier] = []client.Operation{}
roundOperationsLogBz, err := json.Marshal(roundOperationsLog)
if err != nil {
return fmt.Errorf("failed to marshal operationsLog: %w", err)
}
if err := am.db.Put([]byte(operationsLogDBKey), roundOperationsLogBz, nil); err != nil {
return fmt.Errorf("failed to put updated operationsLog: %w", err)
}
return nil
}
func (am *AirgappedMachine) getRoundOperationLog() (RoundOperationLog, error) {
operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil)
if err != nil {
return nil, err
}
var roundOperationsLog RoundOperationLog
if err := json.Unmarshal(operationsLogBz, &roundOperationsLog); err != nil {
return nil, fmt.Errorf("failed to unmarshal stored operationsLog: %w", err)
}
return roundOperationsLog, nil
}
// LoadKeysFromDB load DKG keys from LevelDB
func (am *AirgappedMachine) LoadKeysFromDB() error {
pubKeyBz, err := am.db.Get([]byte(pubKeyDBKey), nil)
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to get public key from db: %w", err)
}
privateKeyBz, err := am.db.Get([]byte(privateKeyDBKey), nil)
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to get private key from db: %w", err)
}
salt, err := am.db.Get([]byte(saltDBKey), nil)
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to read salt from db: %w", err)
}
decryptedPubKey, err := decrypt(am.encryptionKey, salt, pubKeyBz)
if err != nil {
return err
}
decryptedPrivateKey, err := decrypt(am.encryptionKey, salt, privateKeyBz)
if err != nil {
return err
}
am.pubKey = am.baseSuite.Point()
if err = am.pubKey.UnmarshalBinary(decryptedPubKey); err != nil {
return fmt.Errorf("failed to unmarshal public key: %w", err)
}
am.secKey = am.baseSuite.Scalar()
if err = am.secKey.UnmarshalBinary(decryptedPrivateKey); err != nil {
return fmt.Errorf("failed to unmarshal private key: %w", err)
}
return nil
}
// SaveKeysToDB save DKG keys to LevelDB
func (am *AirgappedMachine) SaveKeysToDB() error {
pubKeyBz, err := am.pubKey.MarshalBinary()
if err != nil {
return fmt.Errorf("failed to marshal pub key: %w", err)
}
privateKeyBz, err := am.secKey.MarshalBinary()
if err != nil {
return fmt.Errorf("failed to marshal private key: %w", err)
}
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return fmt.Errorf("failed to generate salt: %w", err)
}
encryptedPubKey, err := encrypt(am.encryptionKey, salt, pubKeyBz)
if err != nil {
return err
}
encryptedPrivateKey, err := encrypt(am.encryptionKey, salt, privateKeyBz)
if err != nil {
return err
}
tx, err := am.db.OpenTransaction()
if err != nil {
return fmt.Errorf("failed to open transcation for db: %w", err)
}
defer tx.Discard()
if err = tx.Put([]byte(pubKeyDBKey), encryptedPubKey, nil); err != nil {
return fmt.Errorf("failed to put pub key into db: %w", err)
}
if err = tx.Put([]byte(privateKeyDBKey), encryptedPrivateKey, nil); err != nil {
return fmt.Errorf("failed to put private key into db: %w", err)
}
if err = tx.Put([]byte(saltDBKey), salt, nil); err != nil {
return fmt.Errorf("failed to put salt into db: %w", err)
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("failed to commit tx for saving keys into db: %w", err)
}
return nil
}

View File

@ -2,9 +2,10 @@ package airgapped
import (
"fmt"
"strings"
"github.com/depools/dc4bc/dkg"
"github.com/syndtr/goleveldb/leveldb/util"
"strings"
)
const (
@ -16,7 +17,7 @@ func makeBLSKeyKeyringDBKey(key string) string {
}
func (am *AirgappedMachine) saveBLSKeyring(dkgID string, blsKeyring *dkg.BLSKeyring) error {
salt, err := am.db.Get([]byte(saltKey), nil)
salt, err := am.db.Get([]byte(saltDBKey), nil)
if err != nil {
return fmt.Errorf("failed to read salt from db: %w", err)
}
@ -43,7 +44,7 @@ func (am *AirgappedMachine) loadBLSKeyring(dkgID string) (*dkg.BLSKeyring, error
err error
)
salt, err := am.db.Get([]byte(saltKey), nil)
salt, err := am.db.Get([]byte(saltDBKey), nil)
if err != nil {
return nil, fmt.Errorf("failed to read salt from db: %w", err)
}
@ -57,7 +58,7 @@ func (am *AirgappedMachine) loadBLSKeyring(dkgID string) (*dkg.BLSKeyring, error
return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err)
}
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(decryptedKeyring); err != nil {
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.baseSuite, decryptedKeyring); err != nil {
return nil, fmt.Errorf("failed to decode bls keyring")
}
return blsKeyring, nil
@ -69,7 +70,7 @@ func (am *AirgappedMachine) GetBLSKeyrings() (map[string]*dkg.BLSKeyring, error)
err error
)
salt, err := am.db.Get([]byte(saltKey), nil)
salt, err := am.db.Get([]byte(saltDBKey), nil)
if err != nil {
return nil, fmt.Errorf("failed to read salt from db: %w", err)
}
@ -85,7 +86,7 @@ func (am *AirgappedMachine) GetBLSKeyrings() (map[string]*dkg.BLSKeyring, error)
if err != nil {
return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err)
}
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(decryptedKeyring); err != nil {
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.baseSuite, decryptedKeyring); err != nil {
return nil, fmt.Errorf("failed to decode bls keyring: %w", err)
}
keyrings[strings.TrimLeft(string(key), blsKeyringPrefix)] = blsKeyring

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto/md5"
"crypto/rand"
"encoding/json"
"fmt"
"io/ioutil"
@ -110,7 +111,9 @@ func (n *node) run(t *testing.T) {
if err = json.Unmarshal(msg.Data, &pubKeyReq); err != nil {
t.Fatalf("failed to unmarshal pubKey request: %v", err)
}
pubKey := bls12381.NewBLS12381Suite().Point()
seed := make([]byte, 32)
_, _ = rand.Read(seed)
pubKey := bls12381.NewBLS12381Suite(seed).Point()
if err = pubKey.UnmarshalBinary(pubKeyReq.MasterKey); err != nil {
t.Fatalf("failed to unmarshal pubkey: %v", err)
}

View File

@ -7,6 +7,7 @@ import (
"fmt"
"log"
"os"
"os/signal"
"runtime"
"strings"
"syscall"
@ -52,6 +53,21 @@ func NewTerminal(machine *airgapped.AirgappedMachine) *terminal {
commandHandler: t.showFinishedDKGCommand,
description: "shows a list of finished dkg rounds",
})
t.addCommand("replay_operations_log", &terminalCommand{
commandHandler: t.replayOperationLogCommand,
description: "replays the operation log for a given dkg round",
})
t.addCommand("drop_operations_log", &terminalCommand{
commandHandler: t.dropOperationLogCommand,
description: "drops the operation log for a given dkg round",
})
t.addCommand("exit", &terminalCommand{
commandHandler: func() error {
log.Fatal("interrupted")
return nil
},
description: "stops the machine",
})
return &t
}
@ -104,6 +120,32 @@ func (t *terminal) showFinishedDKGCommand() error {
return nil
}
func (t *terminal) replayOperationLogCommand() error {
fmt.Print("> Enter the DKGRoundIdentifier: ")
dkgRoundIdentifier, err := t.reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read dkgRoundIdentifier: %w", err)
}
if err := t.airgapped.ReplayOperationsLog(dkgRoundIdentifier); err != nil {
return fmt.Errorf("failed to ReplayOperationsLog: %w", err)
}
return nil
}
func (t *terminal) dropOperationLogCommand() error {
fmt.Print("> Enter the DKGRoundIdentifier: ")
dkgRoundIdentifier, err := t.reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read dkgRoundIdentifier: %w", err)
}
if err := t.airgapped.DropOperationsLog(dkgRoundIdentifier); err != nil {
return fmt.Errorf("failed to DropOperationsLog: %w", err)
}
return nil
}
func (t *terminal) enterEncryptionPasswordIfNeeded() error {
t.airgapped.Lock()
defer t.airgapped.Unlock()
@ -153,7 +195,7 @@ func (t *terminal) run() error {
}
t.airgapped.Lock()
if err := handler.commandHandler(); err != nil {
fmt.Printf("failled to execute command %s: %v, \n", command, err)
fmt.Printf("failled to execute command %s: %v \n", command, err)
t.airgapped.Unlock()
continue
}
@ -194,6 +236,14 @@ func main() {
log.Fatalf("failed to init airgapped machine %v", err)
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for _ = range c {
fmt.Printf("Intercepting SIGINT, please type `exit` to stop the machine\n>>> ")
}
}()
t := NewTerminal(air)
go t.dropSensitiveData(passwordLifeDuration)
if err = t.run(); err != nil {

View File

@ -7,11 +7,12 @@ import (
"sort"
"sync"
"github.com/corestario/kyber/share"
"github.com/corestario/kyber"
"github.com/corestario/kyber/share"
dkg "github.com/corestario/kyber/share/dkg/pedersen"
vss "github.com/corestario/kyber/share/vss/pedersen"
"github.com/google/go-cmp/cmp"
"lukechampine.com/frand"
)
// TODO: dump necessary data on disk
@ -46,6 +47,26 @@ func Init(suite vss.Suite, pubKey kyber.Point, secKey kyber.Scalar) *DKG {
return &d
}
func (d *DKG) Equals(other *DKG) error {
for addr, commits := range d.commits {
otherCommits := other.commits[addr]
for idx := range commits {
if !commits[idx].Equal(otherCommits[idx]) {
return fmt.Errorf("commits from %s are not equal (idx %d): %v != %v", addr, idx, commits[idx], otherCommits[idx])
}
}
}
for addr, deal := range d.deals {
otherDeal := other.deals[addr]
if !cmp.Equal(deal.Deal, otherDeal.Deal) {
return fmt.Errorf("deals from %s are not equal: %+v != %+v", addr, deal.Deal, otherDeal.Deal)
}
}
return nil
}
func (d *DKG) GetPubKey() kyber.Point {
return d.pubKey
}
@ -90,7 +111,7 @@ func (d *DKG) calcParticipantID() int {
return -1
}
func (d *DKG) InitDKGInstance() (err error) {
func (d *DKG) InitDKGInstance(seed []byte) (err error) {
sort.Sort(d.pubkeys)
publicKeys := d.pubkeys.GetPKs()
@ -107,7 +128,9 @@ func (d *DKG) InitDKGInstance() (err error) {
d.responses = newMessageStore(int(math.Pow(float64(participantsCount)-1, 2)))
d.instance, err = dkg.NewDistKeyGenerator(d.suite, d.secKey, publicKeys, d.Threshold)
reader := frand.NewCustom(seed, 32, 20)
d.instance, err = dkg.NewDistKeyGenerator(d.suite, d.secKey, publicKeys, d.Threshold, reader)
if err != nil {
return err
}

View File

@ -7,10 +7,10 @@ import (
"fmt"
"github.com/corestario/kyber/pairing"
vss "github.com/corestario/kyber/share/vss/pedersen"
"github.com/corestario/kyber"
"github.com/corestario/kyber/share"
bls12381 "github.com/depools/kyber-bls12381"
)
type PK2Participant struct {
@ -147,7 +147,7 @@ func (b *BLSKeyring) Bytes() ([]byte, error) {
}
// LoadBLSKeyringFromBytes decode the form generated by Bytes()
func LoadBLSKeyringFromBytes(data []byte) (*BLSKeyring, error) {
func LoadBLSKeyringFromBytes(suite vss.Suite, data []byte) (*BLSKeyring, error) {
var (
err error
blsKeyringJson blsKeyringJSON
@ -158,20 +158,20 @@ func LoadBLSKeyringFromBytes(data []byte) (*BLSKeyring, error) {
commitments := make([]kyber.Point, 0, len(blsKeyringJson.Commitments))
for _, commitmentBz := range blsKeyringJson.Commitments {
commitment := bls12381.NewBLS12381Suite().Point()
commitment := suite.Point()
if err := commitment.UnmarshalBinary(commitmentBz); err != nil {
return nil, fmt.Errorf("failed to unmarshal commitment: %w", err)
}
commitments = append(commitments, commitment)
}
priShare, privDec := &share.PriShare{V: bls12381.NewBLS12381Suite().(pairing.Suite).G1().Scalar()}, gob.NewDecoder(bytes.NewBuffer(blsKeyringJson.Share))
priShare, privDec := &share.PriShare{V: suite.(pairing.Suite).G1().Scalar()}, gob.NewDecoder(bytes.NewBuffer(blsKeyringJson.Share))
if err := privDec.Decode(priShare); err != nil {
return nil, fmt.Errorf("failed to share: %v", err)
}
return &BLSKeyring{
PubPoly: share.NewPubPoly(bls12381.NewBLS12381Suite(), nil, commitments),
PubPoly: share.NewPubPoly(suite, nil, commitments),
Share: priShare,
}, nil
}

6
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/corestario/kyber v1.3.0
github.com/depools/kyber-bls12381 v0.0.0-20200831104422-978ac58f592e
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.2.0
github.com/google/uuid v1.1.1
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b
github.com/looplab/fsm v0.1.0
@ -19,6 +20,11 @@ require (
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/text v0.3.3 // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
lukechampine.com/frand v1.3.0
)
replace golang.org/x/crypto => github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5
replace github.com/corestario/kyber => ../kyber
replace github.com/depools/kyber-bls12381 => ../kyber-bls12381

9
go.sum
View File

@ -1,6 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -13,8 +15,6 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/corestario/kyber v1.3.0 h1:SEWofdorUUeAJTsa9WJmrUYFyWHSWyXLgqDTFFEIzes=
github.com/corestario/kyber v1.3.0/go.mod h1:kIWfWekm8kSJNti3Fo3DCV0GHEH050MWQrdvZdefbkk=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -47,7 +47,9 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@ -180,6 +182,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -216,3 +219,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/frand v1.3.0 h1:HFLrwEHr78+EqAfyp8OChgEzdYCVZzzj6Y+cGDQRhaI=
lukechampine.com/frand v1.3.0/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s=