├── ospackage ├── udev │ └── 99-cryptctl-auto-unlock.rules ├── svc │ ├── cryptctl-auto-unlock@.service │ ├── cryptctl-server.service │ └── cryptctl-client.service └── etc │ └── sysconfig │ ├── cryptctl-client │ └── cryptctl-server ├── .gitignore ├── keyserv ├── kmip_server_test.go ├── rpc_test.crt ├── rpc_test.key ├── mail_test.go ├── rpc_svc_test.go ├── mail.go ├── rpc_client_bench_test.go ├── kmip_test.go ├── rpc_client.go └── rpc_client_test.go ├── sys ├── daemon_test.go ├── exec_test.go ├── exec.go ├── daemon.go ├── term.go ├── sysconfig_test.go └── sysconfig.go ├── routine ├── openssl_test.go ├── openssl.go └── unlock.go ├── fs ├── crypt_test.go ├── crypt.go ├── fs_test.go ├── mnt.go ├── file_test.go ├── file.go └── fs.go ├── kmip ├── ttlv │ ├── encoding_test.go │ ├── types.go │ ├── sample.go │ └── dencode.go └── structure │ ├── serialisation_test.go │ ├── const.go │ ├── op_destroy.go │ ├── op_create.go │ └── op_get.go ├── README.md ├── main.go └── keydb ├── db_test.go └── record.go /ospackage/udev/99-cryptctl-auto-unlock.rules: -------------------------------------------------------------------------------- 1 | ACTION=="add" SUBSYSTEM=="block", TAG+="systemd", ENV{SYSTEMD_WANTS}+="cryptctl-auto-unlock@$env{ID_FS_UUID}.service" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | *.cgo1.go 11 | *.cgo2.c 12 | _cgo_defun.c 13 | _cgo_gotypes.go 14 | _cgo_export.* 15 | 16 | _testmain.go 17 | 18 | *.exe 19 | *.test 20 | *.prof 21 | cryptctl 22 | -------------------------------------------------------------------------------- /ospackage/svc/cryptctl-auto-unlock@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Disk encryption utility (cryptctl) - contact key server to unlock disk %i and keep the server informed 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/sbin/cryptctl auto-unlock %i 9 | User=root 10 | Group=root 11 | WorkingDirectory=/ -------------------------------------------------------------------------------- /ospackage/svc/cryptctl-server.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Disk encryption utility (cryptctl) - key server 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/sbin/cryptctl daemon 9 | User=root 10 | Group=root 11 | WorkingDirectory=/ 12 | PrivateTmp=true 13 | RestartSec=5 14 | Restart=on-abort 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /ospackage/svc/cryptctl-client.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Disk encryption utility (cryptctl) - key client 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/sbin/cryptctl client-daemon 9 | User=root 10 | Group=root 11 | WorkingDirectory=/ 12 | PrivateTmp=true 13 | RestartSec=5 14 | Restart=on-abort 15 | MountFlags=shared 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /keyserv/kmip_server_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keyserv 4 | 5 | import ( 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestGetNewDiskEncryptionKeyBits(t *testing.T) { 11 | key1 := GetNewDiskEncryptionKeyBits() 12 | key2 := GetNewDiskEncryptionKeyBits() 13 | if len(key1) != KMIPAESKeySizeBits/8 || reflect.DeepEqual(key1, key2) { 14 | t.Fatal(key1, key2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sys/daemon_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package sys 4 | 5 | import "testing" 6 | 7 | func TestSystemctl(t *testing.T) { 8 | if err := SystemctlEnableStart("does-not-exist"); err == nil { 9 | t.Fatal(err) 10 | } 11 | if err := SystemctlEnableRestart("does-not-exist"); err == nil { 12 | t.Fatal(err) 13 | } 14 | if err := SystemctlDisableStop("does-not-exist"); err == nil { 15 | t.Fatal(err) 16 | } 17 | if SystemctlIsRunning("does-not-exist") { 18 | t.Fatal("cannot be running") 19 | } 20 | if !SystemctlIsRunning("systemd-journald.service") { 21 | t.Fatal("journald is not running") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ospackage/etc/sysconfig/cryptctl-client: -------------------------------------------------------------------------------- 1 | ## Path: System/Disk Encryption/Network Key Client 2 | ## Description: Global settings for disk encryption (cryptctl) network key client 3 | 4 | ## Type: string 5 | ## Default: "" 6 | # 7 | # In the automatic routine that unlocks disks, contact this server (host name) to ask for encryption keys. 8 | # This host name must match the host name of TLS certificate presented by the key server. 9 | KEY_SERVER_HOST="" 10 | 11 | ## Type: integer 12 | ## Default: 3737 13 | # 14 | # In the automatic routine that unlocks disks, contact key server on this port number to ask for encryption keys. 15 | KEY_SERVER_PORT=3737 16 | 17 | ## Type: string 18 | ## Default: "" 19 | # 20 | # (Optional) path to PEM-encoded custom certificate authority that issued the TLS certificate for the key server. 21 | # Leave empty if the TLS certificate was issued by a well-known certificate authority. 22 | TLS_CA_PEM="" 23 | 24 | ## Type: string 25 | ## Default: "" 26 | # 27 | # (Optional) Location of PEM-encoded TLS certificate file to identify the client to server. 28 | TLS_CERT_PEM="" 29 | 30 | ## Type: string 31 | ## Default: "" 32 | # 33 | # (Optional) Location of PEM-encoded TLS certificate key file to identify the client to server. 34 | TLS_CERT_KEY_PEM="" 35 | -------------------------------------------------------------------------------- /keyserv/rpc_test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDhTCCAm2gAwIBAgIJAKL+BWMM6ScNMA0GCSqGSIb3DQEBBQUAMFkxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjA2MjQxMDEx 5 | MDRaFw00MTAyMTMxMDExMDRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l 6 | LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV 7 | BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9X 8 | eQ6CSkOBjo2rxDkedwsqApgd+VcxZSp2Dbeqthmcko4mKRRvt/vbxqnpLy8jc6/M 9 | VugN8PPBPQMjhulHkUYo6YPmSJmmyiqaNsCuWe3tPVO/jnN/m0DM4KAgSjUP4iSZ 10 | iZoJPfjwFnPH/3tvD8ISkJh9forG4LgXWwP8dy20vn1n8dNiWNg5LXU4dt9RSkT2 11 | Ob1/xg1pBmJikL1EvkRv0HnonOXILJSFEXShFhJr5zRJYaCo2BtTCGGy19EE5iDG 12 | d1fo+Hm38vFV5ixSuYKIvvy864cUQUwxlstKQHEFBrP3Qp10/PugvIKolVF0cGM6 13 | L1sI/2L7e5en0hoYzTkCAwEAAaNQME4wHQYDVR0OBBYEFONh0HZ/YU7Og48ahSLz 14 | j50/ifyqMB8GA1UdIwQYMBaAFONh0HZ/YU7Og48ahSLzj50/ifyqMAwGA1UdEwQF 15 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAFiuUfyQel9TwbbuDBd5mnX5Sur5KgIa 16 | MDrCfVRR3OGCLlrMakV0fexhZzWZujHoErTSMTSRrV6lxifqGm5JuVtG0S462whO 17 | usVkn3rCUVh/ahaL+yqpaO4n9Ve9oidWUwjHNwlUATvmp+6glNeY8e05U3Ol8TpL 18 | mpySmhWpqdONpVbnIRSkUz4BBmQ79cIq3g24DkMLMdBjO4WxZ1CaFBHMektKLKkF 19 | bub9P5a5WBt95mwvueZR+XET4TYZpkeBPRtQ4xPl0K5MB97HDTweR+ZUSpSrgMUA 20 | +HtUlGpiWGC5mBFzXGtghk37k1cyICjv/85judAceKZi9biNjbWwbeg= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /routine/openssl_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package routine 4 | 5 | import ( 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestGenerateSelfSignedCertificate(t *testing.T) { 14 | tmpDir, err := ioutil.TempDir("", "") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer os.RemoveAll(tmpDir) 19 | if err := GenerateSelfSignedCertificate("example.com", path.Join(tmpDir, "cert"), path.Join(tmpDir, "key")); err != nil { 20 | t.Fatal(err) 21 | } 22 | cert, err := ioutil.ReadFile(path.Join(tmpDir, "cert")) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | if !strings.Contains(string(cert), "BEGIN CERTIFICATE") { 27 | t.Fatal(string(cert)) 28 | } 29 | key, err := ioutil.ReadFile(path.Join(tmpDir, "key")) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | if !strings.Contains(string(key), "BEGIN PRIVATE KEY") { 34 | t.Fatal(string(key)) 35 | } 36 | // The function must not overwrite existing files 37 | if err := GenerateSelfSignedCertificate("example.com", path.Join(tmpDir, "cert"), path.Join(tmpDir, "key")); err == nil { 38 | t.Fatal("did not error") 39 | } 40 | // Empty common name is an error condition 41 | if err := GenerateSelfSignedCertificate("", path.Join(tmpDir, "1"), path.Join(tmpDir, "2")); err == nil { 42 | t.Fatal("did not error") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /keyserv/rpc_test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/V3kOgkpDgY6N 3 | q8Q5HncLKgKYHflXMWUqdg23qrYZnJKOJikUb7f728ap6S8vI3OvzFboDfDzwT0D 4 | I4bpR5FGKOmD5kiZpsoqmjbArlnt7T1Tv45zf5tAzOCgIEo1D+IkmYmaCT348BZz 5 | x/97bw/CEpCYfX6KxuC4F1sD/HcttL59Z/HTYljYOS11OHbfUUpE9jm9f8YNaQZi 6 | YpC9RL5Eb9B56JzlyCyUhRF0oRYSa+c0SWGgqNgbUwhhstfRBOYgxndX6Ph5t/Lx 7 | VeYsUrmCiL78vOuHFEFMMZbLSkBxBQaz90KddPz7oLyCqJVRdHBjOi9bCP9i+3uX 8 | p9IaGM05AgMBAAECggEAdDA1vm23ks51Nen7uYOaXhkggiaRZjUEbYhKRCFRerPs 9 | +oyJnXNJkZKfTEXg9QreEP5QN5Ffo2TQG7vTDIz81lG5mvKXW1ZApSYH4XD+AtBw 10 | 0Q4c/l2adPrz28g/x4Dhnb/uIq9CBowj1iK4LMgAFaUYUMDDupmRk7f6+Kyx4fo1 11 | xwbar5isW7EN4hUQ+BbKyzsJYe6T4H9TPVx7bvV3UPf8yK5idMBXI03LuRciVgA+ 12 | W5KGwsR6bJkri1OK+BVtVUdtKHXFZ81eL0BhgQyyS9lLDvHUjGsIYANe8jMQS5DI 13 | CMd4d1B+3cDIbFBIClDuv4+99h+MAky78mHuY64AkQKBgQDtwYp/ev/har2m4zON 14 | HiWOLl+EZNpDSCl2QSUH1OoCDTyMN6hD32VDBew1Vzh/iqAUUkPhR96yngyXv8l0 15 | le9TpNm94iAbc9K5zuSFL9xG9zE1QQh1GOWJHdr6TOxMzMBqMXBaEXmwOg+eOP1U 16 | fPH25oHOmGCEmd6Z433+P5MEvwKBgQDOBiw5g9K02IPt/WBrYY4cxe9s+9wm/j+D 17 | Y3LYRC7zpphhDDibszsUwxUzCslXU8ba9rQmYdOBRAaZkVImget3gBaZZlO5CmgA 18 | ksTYmfmhI9qEWpbtKmImrVLV48/zZKdLD3xgVV+wZhGhvddG7/v/DgjApg+fVnm0 19 | mkQEWLtUBwKBgBBch0lqj31VuSNo8z0829zC+DPGNPb4WlIW/ZNiZZAqlQYZNm0l 20 | THSmTbEGBY9RXN7JIn64UWz9T2SKADUTtFqPN6THkOoSuGetAzDfMEt561r81LYq 21 | NnGPKmibLo/Cb2Nfb5njJfqopDaBOX2883HIPxqWhd3aMOVqMFt0yItFAoGBALf6 22 | UyvZSCQu5UF1btD1gQ64wyIzl1lK2jTebgQqfzMdph2j6DlCSJQ10YyPKVVOftmy 23 | TRWpblKVCL/CQfYZNsi0HXpHIqSvYkiAyEAU4BLCDbT7oKORoaygQsS2d1EGpU4m 24 | Og9creK8gypIeSHj1MjjI3XF1VWYx3479FldU4upAoGAHRqhKXlsKeJGMOthNPC8 25 | dmB1J3BQBS5OfrPRSk1xzQCfYouFqs3VXOZe8atd2Uu7hJqThcgawu6NNqNGKYTp 26 | UPjGgyvioSkbYtEcUaM9sdV7Tm+uGHgxo/6+y5tKrLDovXONmHpnFd9TG/kDjy96 27 | FGtxcI/KlM1/bBDAndZ7XEI= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /sys/exec_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package sys 4 | 5 | import ( 6 | "bytes" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestExec(t *testing.T) { 12 | if exitStatus, stdout, stderr, err := Exec(nil, nil, nil, "echo", "hi", "there"); exitStatus != 0 || stdout != "hi there\n" || stderr != "" || err != nil { 13 | t.Fatal(exitStatus, stdout, stderr, err) 14 | } 15 | if exitStatus, stdout, stderr, err := Exec(nil, nil, nil, "sh", "-c", "echo hi >&2"); exitStatus != 0 || stdout != "" || stderr != "hi\n" || err != nil { 16 | t.Fatal(exitStatus, stdout, stderr, err) 17 | } 18 | // Custom stdin and stdout 19 | grepInput := `aaa 20 | bbbFINDMEbbb 21 | cccFINDMEccc 22 | ddd` 23 | var outBuf bytes.Buffer 24 | if exitStatus, stdout, stderr, err := Exec(bytes.NewReader([]byte(grepInput)), &outBuf, nil, "grep", "FINDME"); exitStatus != 0 || stdout != "" || stderr != "" || err != nil || outBuf.String() != "bbbFINDMEbbb\ncccFINDMEccc\n" { 25 | t.Fatal(exitStatus, stdout, outBuf.String(), stderr, err) 26 | } 27 | // Custom stdout 28 | var errBuf bytes.Buffer 29 | if exitStatus, stdout, stderr, err := Exec(nil, nil, &errBuf, "sh", "-c", "echo hi >&2"); exitStatus != 0 || stdout != "" || stderr != "" || err != nil || errBuf.String() != "hi\n" { 30 | t.Fatal(exitStatus, stdout, errBuf.String(), stderr, err) 31 | } 32 | // Special exit status 33 | if exitStatus, stdout, stderr, err := Exec(nil, nil, nil, "false"); exitStatus != 1 || stdout != "" || stderr != "" || err == nil { 34 | t.Fatal(exitStatus, stdout, stderr, err) 35 | } 36 | } 37 | 38 | func TestWalkProcs(t *testing.T) { 39 | var seen bool 40 | if err := WalkProcs(func(cmdLine []string) bool { 41 | if strings.Contains(cmdLine[0], "systemd") { 42 | seen = true 43 | return false 44 | } 45 | return true 46 | }); err != nil || !seen { 47 | t.Fatal(err, seen) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fs/crypt_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package fs 4 | 5 | import "testing" 6 | 7 | // The unit test simply makes sure that the functions do not crash, it does not set up an encrypted device node. 8 | func TestCryptSetup(t *testing.T) { 9 | if err := CryptFormat([]byte{}, "doesnotexist", "testuuid"); err == nil { 10 | t.Fatal("did not error") 11 | } 12 | if err := CryptOpen([]byte{}, "doesnotexist", "doesnotexist"); err == nil { 13 | t.Fatal("did not error") 14 | } 15 | if mapping, err := CryptStatus("doesnotexist"); err == nil || mapping.Device != "" { 16 | t.Fatal("did not error") 17 | } 18 | if err := CryptClose("doesnotexist"); err == nil { 19 | t.Fatal("did not error") 20 | } 21 | if err := CryptErase("doesnotexist"); err == nil { 22 | t.Fatal("did not error") 23 | } 24 | } 25 | 26 | func TestParseCryptStatus(t *testing.T) { 27 | sample1 := `/dev/mapper/howard-enc is active and is in use. 28 | type: LUKS1 29 | cipher: aes-xts-plain64 30 | keysize: 256 bits 31 | device: /dev/loop0 32 | loop: /my-loop-file 33 | offset: 4096 sectors 34 | size: 24571904 sectors 35 | mode: read/write 36 | ` 37 | expected := CryptMapping{ 38 | Type: "LUKS1", 39 | Cipher: "aes-xts-plain64", 40 | KeySize: 256, 41 | Device: "/dev/loop0", 42 | Loop: "/my-loop-file", 43 | } 44 | if parsed := ParseCryptStatus(sample1); parsed != expected || !parsed.IsValid() { 45 | t.Fatalf("%+v", parsed) 46 | } 47 | sample2 := `/dev/mapper/cryptopened is active. 48 | Oops, secure memory pool already initialized 49 | type: LUKS1 50 | cipher: aes-xts-plain64 51 | keysize: 256 bits 52 | device: /dev/vdc 53 | offset: 4096 sectors 54 | size: 18870272 sectors 55 | mode: read/write 56 | ` 57 | expected = CryptMapping{ 58 | Type: "LUKS1", 59 | Cipher: "aes-xts-plain64", 60 | KeySize: 256, 61 | Device: "/dev/vdc", 62 | Loop: "", 63 | } 64 | if parsed := ParseCryptStatus(sample2); parsed != expected || !parsed.IsValid() { 65 | t.Fatalf("%+v", parsed) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /routine/openssl.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package routine 4 | 5 | import ( 6 | "fmt" 7 | "github.com/SUSE/cryptctl/sys" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | ) 12 | 13 | const ( 14 | BIN_OPENSSL = "/usr/bin/openssl" 15 | ) 16 | 17 | // Invoke openssl command to make a self-signed certificate for this host. 18 | func GenerateSelfSignedCertificate(commonName, certFilePath, keyFilePath string) error { 19 | // Create a temporary openssl configuration file that tells how the certificate should look like 20 | confFile, err := ioutil.TempFile("", "cryptctl-openssl-conf") 21 | if err != nil { 22 | return fmt.Errorf("GenerateSelfSignedCertificate: failed to create temporary file - %v", err) 23 | } 24 | confFilePath := path.Join("", confFile.Name()) 25 | defer os.Remove(confFilePath) 26 | confFileContent := fmt.Sprintf(` 27 | [ req ] 28 | distinguished_name = req_distinguished_name 29 | req_extensions = v3_ca 30 | prompt = no 31 | [ req_distinguished_name ] 32 | countryName = FI 33 | localityName = Testing City 34 | organizationalUnitName = Testing Unit 35 | commonName = %s 36 | emailAddress = testing@example.com 37 | [ v3_ca ] 38 | subjectKeyIdentifier=hash 39 | authorityKeyIdentifier=keyid:always,issuer:always 40 | basicConstraints = critical, CA:true 41 | `, commonName) 42 | if err = ioutil.WriteFile(confFilePath, []byte(confFileContent), 0400); err != nil { 43 | return fmt.Errorf("GenerateSelfSignedCertificate: failed to write temporary file - %v", err) 44 | } 45 | // Generate certificate and key 46 | if _, statErr := os.Stat(certFilePath); !os.IsNotExist(statErr) { 47 | return fmt.Errorf("GenerateSelfSignedCertificate: certificate file \"%s\" probably already exists", certFilePath) 48 | } 49 | if _, statErr := os.Stat(keyFilePath); !os.IsNotExist(statErr) { 50 | return fmt.Errorf("GenerateSelfSignedCertificate: key file \"%s\" probably already exists", keyFilePath) 51 | } 52 | _, stdout, stderr, err := sys.Exec(nil, nil, nil, BIN_OPENSSL, "req", "-new", "-x509", 53 | "-config", confFilePath, 54 | "-newkey", "rsa:2048", "-days", "30", "-nodes", 55 | "-out", certFilePath, "-keyout", keyFilePath) 56 | if err != nil { 57 | return fmt.Errorf("GenerateSelfSignedCertificate: failed to call openssl - %v %s %s", err, stdout, stderr) 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /kmip/ttlv/encoding_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package ttlv 4 | 5 | import ( 6 | "encoding/hex" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestRoundUpTo8(t *testing.T) { 13 | if i := RoundUpTo8(1); i != 8 { 14 | t.Fatal(i) 15 | } 16 | if i := RoundUpTo8(8); i != 8 { 17 | t.Fatal(i) 18 | } 19 | if i := RoundUpTo8(9); i != 16 { 20 | t.Fatal(i) 21 | } 22 | } 23 | 24 | func TestCopyValue(t *testing.T) { 25 | if err := CopyPrimitive(nil, nil); err == nil { 26 | t.Fatal("did not error") 27 | } 28 | i1 := Integer{Value: 1} 29 | i2 := Integer{} 30 | if err := CopyPrimitive(&i2, &i1); err != nil || i2.Value != 1 { 31 | t.Fatal(err, i2) 32 | } 33 | long1 := LongInteger{Value: 2} 34 | long2 := LongInteger{} 35 | if err := CopyPrimitive(&long2, &long1); err != nil || long2.Value != 2 { 36 | t.Fatal(err, long2) 37 | } 38 | // Must not permit copying across different types 39 | if err := CopyPrimitive(&i2, &long1); err == nil || i2.Value != 1 { 40 | t.Fatal("did not error") 41 | } 42 | enum1 := Enumeration{Value: 3} 43 | enum2 := Enumeration{} 44 | if err := CopyPrimitive(&enum2, &enum1); err != nil || enum2.Value != 3 { 45 | t.Fatal(err, enum2) 46 | } 47 | dt1 := DateTime{Time: time.Unix(4, 0)} 48 | dt2 := DateTime{} 49 | if err := CopyPrimitive(&dt2, &dt1); err != nil || dt2.Time.Unix() != 4 { 50 | t.Fatal(err, dt2) 51 | } 52 | text1 := Text{Value: "5"} 53 | text2 := Text{} 54 | if err := CopyPrimitive(&text2, &text1); err != nil || text2.Value != "5" { 55 | t.Fatal(err, text2) 56 | } 57 | bytes1 := Bytes{Value: []byte{6}} 58 | bytes2 := Bytes{} 59 | if err := CopyPrimitive(&bytes2, &bytes1); err != nil || len(bytes2.Value) != 1 || bytes2.Value[0] != 6 { 60 | t.Fatal(err, bytes2) 61 | } 62 | } 63 | 64 | func TestEncodeDecode(t *testing.T) { 65 | for i, data := range [][]byte{ 66 | SampleCreateRequest, SampleCreateResponseSuccess, 67 | SampleGetRequest, SampleGetResponseSuccess, SampleGetResponseFailure, 68 | SampleDestroyRequest, SampleDestroyResponseSuccess, SampleDestroyResponseFailure, 69 | } { 70 | decoded, _, err := DecodeAny(data) 71 | t.Log(DebugTTLVItem(0, decoded)) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | encoded := EncodeAny(decoded) 76 | if !reflect.DeepEqual(data, encoded) { 77 | t.Fatalf("Mismatch in %d:\n%s\n\n%s\n", i, hex.Dump(data), hex.Dump(encoded)) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /keyserv/mail_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keyserv 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | ) 9 | 10 | func TestMailerValidateConfig(t *testing.T) { 11 | m := Mailer{Recipients: []string{"a@b.c"}, FromAddress: "me@a.example", AgentAddressPort: "a.example:25"} 12 | if err := m.ValidateConfig(); err != nil { 13 | t.Fatal(err) 14 | } 15 | m = Mailer{Recipients: []string{"a@b"}, FromAddress: "me@a", AgentAddressPort: "a.example:25"} 16 | if err := m.ValidateConfig(); err != nil { 17 | t.Fatal(err) 18 | } 19 | m = Mailer{Recipients: []string{}, FromAddress: "me@a.example", AgentAddressPort: "a.example:25"} 20 | if err := m.ValidateConfig(); err == nil { 21 | t.Fatal("did not error") 22 | } 23 | m = Mailer{Recipients: []string{"a@b.c"}, FromAddress: "", AgentAddressPort: "a.example:25"} 24 | if err := m.ValidateConfig(); err == nil { 25 | t.Fatal("did not error") 26 | } 27 | m = Mailer{Recipients: []string{"a@b.c"}, FromAddress: "me@a.example", AgentAddressPort: "a.example"} 28 | if err := m.ValidateConfig(); err == nil { 29 | t.Fatal("did not error") 30 | } 31 | m = Mailer{Recipients: []string{"a@b.c"}, FromAddress: "me@a.example", AgentAddressPort: "a.example:25a"} 32 | if err := m.ValidateConfig(); err == nil { 33 | t.Fatal("did not error") 34 | } 35 | m = Mailer{Recipients: []string{"a@b.c"}, FromAddress: "me@a.example", AgentAddressPort: ""} 36 | if err := m.ValidateConfig(); err == nil { 37 | t.Fatal("did not error") 38 | } 39 | } 40 | 41 | func TestMailerSend(t *testing.T) { 42 | m := Mailer{Recipients: []string{"a@b.c"}, FromAddress: "me@a.example", AgentAddressPort: "a.example:25"} 43 | if err := m.Send("abc", "123"); err == nil { 44 | t.Fatal("did not error") 45 | } 46 | if _, err := net.Dial("tcp", "localhost:25"); err != nil { 47 | t.Skip("an MTA on localhost would be required to continue this test") 48 | } 49 | m = Mailer{Recipients: []string{"root@localhost"}, FromAddress: "root@localhost", AgentAddressPort: "localhost:25"} 50 | if err := m.Send("cryptctl mailer test subject", "cryptctl mailer test text body"); err != nil { 51 | t.Fatal(err) 52 | } 53 | } 54 | 55 | func TestMailerReadFromSysconfig(t *testing.T) { 56 | m := Mailer{} 57 | mailConf := GetDefaultKeySvcConf() 58 | m.ReadFromSysconfig(mailConf) 59 | if len(m.Recipients) != 0 || m.FromAddress != "" || m.AgentAddressPort != "" { 60 | t.Fatal(m) 61 | } 62 | mailConf.SetStrArray("EMAIL_RECIPIENTS", []string{"a", "b"}) 63 | mailConf.Set("EMAIL_FROM_ADDRESS", "c") 64 | mailConf.Set("EMAIL_AGENT_AND_PORT", "d") 65 | m.ReadFromSysconfig(mailConf) 66 | if len(m.Recipients) != 2 || m.FromAddress != "c" || m.AgentAddressPort != "d" { 67 | t.Fatal(m) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sys/exec.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package sys 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "path/filepath" 14 | "syscall" 15 | ) 16 | 17 | /* 18 | Run an external program, wait till it finishes execution, eventually return its exit status. 19 | Optionally return the program output only if stdout/stderr are left nil. 20 | */ 21 | func Exec(stdin io.Reader, stdout, stderr io.Writer, programName string, programArgs ...string) (exitStatus int, 22 | stdoutStr, stderrStr string, execErr error) { 23 | cmd := exec.Command(programName, programArgs...) 24 | // Connect IO 25 | var myStdout, myStderr bytes.Buffer 26 | cmd.Stdin = stdin 27 | cmd.Stdout = stdout 28 | if stdout == nil { 29 | cmd.Stdout = &myStdout 30 | } 31 | cmd.Stderr = stderr 32 | if stderr == nil { 33 | cmd.Stderr = &myStderr 34 | } 35 | // Run process and wait 36 | if execErr = cmd.Run(); execErr != nil { 37 | // Figure out the exit status 38 | if exitErr, isExit := execErr.(*exec.ExitError); isExit { 39 | if status, isStatus := exitErr.Sys().(syscall.WaitStatus); isStatus { 40 | exitStatus = status.ExitStatus() 41 | } 42 | } 43 | stdoutStr = myStdout.String() 44 | stderrStr = myStderr.String() 45 | return 46 | } 47 | stdoutStr = myStdout.String() 48 | stderrStr = myStderr.String() 49 | return 50 | } 51 | 52 | // Lock all program memory into main memory to prevent sensitive data from leaking into swap. 53 | func LockMem() { 54 | if os.Geteuid() != 0 { 55 | fmt.Fprintln(os.Stderr, "Please run this cryptctl command with root privilege.") 56 | os.Exit(111) 57 | } 58 | if err := syscall.Mlockall(syscall.MCL_CURRENT | syscall.MCL_FUTURE); err != nil { 59 | fmt.Fprintln(os.Stderr, "Failed to lock memory - %v", err) 60 | os.Exit(111) 61 | } 62 | } 63 | 64 | /* 65 | Print the message to stderr and exit the program with status 1. 66 | The function does not return, however it is defined to have a return value to help with coding style. 67 | */ 68 | func ErrorExit(template string, stuff ...interface{}) int { 69 | fmt.Fprintf(os.Stderr, template+"\n", stuff...) 70 | os.Exit(1) 71 | return 1 72 | } 73 | 74 | // Run function on all running processes that are exposed via /proc. 75 | func WalkProcs(fun func(cmdLine []string) bool) error { 76 | entries, err := filepath.Glob("/proc/[0-9]*") 77 | if err != nil { 78 | return err 79 | } 80 | for _, entry := range entries { 81 | cmdline, err := ioutil.ReadFile(path.Join(entry, "cmdline")) 82 | if err != nil { 83 | // process is gone 84 | continue 85 | } 86 | cmdLineStr := make([]string, 0, 0) 87 | for _, seg := range bytes.Split(cmdline, []byte{0}) { 88 | cmdLineStr = append(cmdLineStr, string(seg)) 89 | } 90 | if !fun(cmdLineStr) { 91 | return nil 92 | } 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cryptctl 2 | cryptctl is a utility for setting up disk encryption using the popular well-established LUKS method. It generates random 3 | numbers to use as encryption keys, and safely keep the keys on a centralised key server. It can encrypt arbitrary 4 | directories into encrypted disk partitions. 5 | 6 | The key server stores all encryption keys in a database directory (by default /var/lib/cryptctl/keydb) and serves the 7 | keys via an RPC protocol over TCP (by default on port 3737) to client computers. The key server is the central component 8 | of encryption setup, hence it must be deployed with extra physical/network security measures; regular backup of the key 9 | database must be carried out to ensure its availability. Communication between key server and client computers is 10 | protected by TLS via a certificate, and authorised via a password specified by the system administrator during key 11 | server's initial setup. 12 | 13 | The encryption routine sets up encrypted file systems using using aes-xts-plain64 cipher, with a fixed-size (512-bit) 14 | key generated from cryptography random pool. Encrypted directories will always be mounted automatically upon system boot 15 | by retrieving their encryption keys from key server automatically; this operation tolerates temporary network failure or 16 | key server down time by making continuous attempts until success, for maximum of 24 hours. 17 | 18 | The system administrator can define an upper limit number of computers that can get hold of a key simultaneously. After 19 | a client computer successfully retrieves a key, it will keep reporting back to key server that it is online, and the 20 | key server closely tracks its IP, host name, and timestamp, in order to determine number of computers actively using 21 | the key; if the upper limit number of computers is reached, the key will no longer be handed out automatically; system 22 | administrator can always retrieve encryption keys by using key server's access password. 23 | 24 | cryptctl can optionally utilise an external key management appliance that understands KMIP v1.3 to store the actual disk 25 | encryption keys. Should you choose to use the external appliance, you may enter KMIP connectivity details such as host 26 | name, port, certificate, and user credentials during server initialisation sequence. If you do not wish to use the 27 | external appliance, cryptctl will store encryption keys in its own database. 28 | 29 | To experiment with cryptctl features, you may temporary deploy both key server and encrypted partition on the same 30 | computer; keep in mind that doing defeats the objective of separating key data from encrypted data, therefore always 31 | deploy key server stand-alone in QA and production scenarios. 32 | 33 | cryptctl is commercially supported by "SUSE Linux Enterprise Server For SAP Applications". 34 | 35 | ## Usage 36 | Build cryptctl with go 1.8 or newer versions. It solely depends on Go standard library, no 3rd party library is used. 37 | 38 | Install cryptctl binary along with configuration files and systemd services from `ospackage/` directory to both key 39 | server and client computers. Then, please carefully read the manual page `ospackage/man/cryptctl.8` for setup and usage 40 | instructions. 41 | 42 | ## RPM package 43 | A ready made RPM spec file and RPM package can be found here: 44 | https://build.opensuse.org/package/show/security/cryptctl 45 | 46 | ## License 47 | cryptctl is an open source free software, you may redistribute it and/or modify it under the terms of the GNU General 48 | Public License version 3 as published by the Free Software Foundation. 49 | 50 | See `LICENSE` file for the complete licensing terms and conditions. -------------------------------------------------------------------------------- /kmip/structure/serialisation_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package structure 4 | 5 | import ( 6 | "encoding/hex" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/kmip/ttlv" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestSerialiseSimpleStruct(t *testing.T) { 14 | // Hand-craft a SCredential 15 | ttlvStruct := &ttlv.Structure{ 16 | TTL: ttlv.TTL{ 17 | Tag: TagCredential, 18 | Typ: ttlv.TypStruct, 19 | }, 20 | Items: []ttlv.Item{ 21 | &ttlv.Enumeration{ 22 | TTL: ttlv.TTL{ 23 | Tag: TagCredentialType, 24 | Typ: ttlv.TypeEnum, 25 | }, 26 | Value: 1, 27 | }, 28 | &ttlv.Structure{ 29 | TTL: ttlv.TTL{ 30 | Tag: TagCredentialValue, 31 | Typ: ttlv.TypStruct, 32 | }, 33 | Items: []ttlv.Item{ 34 | &ttlv.Text{ 35 | TTL: ttlv.TTL{ 36 | Tag: TagUsername, 37 | Typ: ttlv.TypeText, 38 | }, 39 | Value: "user", 40 | }, 41 | &ttlv.Text{ 42 | TTL: ttlv.TTL{ 43 | Tag: TagPassword, 44 | Typ: ttlv.TypeText, 45 | }, 46 | Value: "pass", 47 | }, 48 | }, 49 | }, 50 | }, 51 | } 52 | // Length of TTLV items is automatically calculated during encoding process 53 | ttlvBytes := ttlv.EncodeAny(ttlvStruct) 54 | // Deserialise into a SCredential 55 | cred := SCredential{} 56 | if err := cred.DeserialiseFromTTLV(ttlvStruct); err != nil { 57 | t.Fatal(err) 58 | } 59 | t.Logf("Encoded:\n%s", ttlv.DebugTTLVItem(0, ttlvStruct)) 60 | t.Logf("Deserialised:\n%+v", cred) 61 | 62 | // Reverse the process 63 | ttlvStructRecovered := cred.SerialiseToTTLV() 64 | t.Logf("Recovered:\n%s", ttlv.DebugTTLVItem(0, ttlvStructRecovered)) 65 | // Serialise the reversed structure and match byte binary 66 | ttlvBytesRecovered := ttlv.EncodeAny(ttlvStructRecovered) 67 | fmt.Println(hex.Dump(ttlvBytes)) 68 | fmt.Println(hex.Dump(ttlvBytesRecovered)) 69 | if !reflect.DeepEqual(ttlvBytes, ttlvBytesRecovered) { 70 | t.Fatal("mismatch in binary representation") 71 | } 72 | } 73 | 74 | func TestSerialiseStruct(t *testing.T) { 75 | structs := []SerialisedItem{ 76 | &SCreateRequest{}, &SCreateResponse{}, 77 | &SGetRequest{}, &SGetResponse{}, &SGetResponse{}, 78 | &SDestroyRequest{}, &SDestroyResponse{}, &SDestroyResponse{}, 79 | } 80 | binaries := [][]byte{ 81 | ttlv.SampleCreateRequest, ttlv.SampleCreateResponseSuccess, 82 | ttlv.SampleGetRequest, ttlv.SampleGetResponseSuccess, ttlv.SampleGetResponseFailure, 83 | ttlv.SampleDestroyRequest, ttlv.SampleDestroyResponseSuccess, ttlv.SampleDestroyResponseFailure, 84 | } 85 | ttlvs := make([]ttlv.Item, len(binaries)) 86 | 87 | for i, bin := range binaries { 88 | fmt.Printf("====================\n%d\n====================\n", i) 89 | var err error 90 | // bin -> ttlv 91 | ttlvs[i], _, err = ttlv.DecodeAny(bin) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | fmt.Printf("Input binary:\n%s\n", hex.Dump(bin)) 96 | fmt.Printf("Input TTLV:\n%s\n", ttlv.DebugTTLVItem(0, ttlvs[i])) 97 | // ttlv -> struct 98 | err = structs[i].DeserialiseFromTTLV(ttlvs[i]) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | /* 103 | debugJson, err := json.MarshalIndent(structs[i], "", " ") 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | fmt.Printf("Input struct:\n%s\n", string(debugJson)) 108 | */ 109 | // reverse the operations above 110 | recoveredTTLV := structs[i].SerialiseToTTLV() 111 | fmt.Printf("Recovered TTLV:\n%s\n", ttlv.DebugTTLVItem(0, recoveredTTLV)) 112 | recoveredBin := ttlv.EncodeAny(recoveredTTLV) 113 | fmt.Printf("Recovered binary:\n%s\n", hex.Dump(recoveredBin)) 114 | if !reflect.DeepEqual(bin, recoveredBin) { 115 | t.Fatal("mismatch in binary representation") 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /keyserv/rpc_svc_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keyserv 4 | 5 | import ( 6 | "crypto/sha512" 7 | "encoding/hex" 8 | "path" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestHashPassword(t *testing.T) { 14 | salt := [sha512.Size]byte{ 15 | 0, 0, 0, 0, 0, 0, 0, 0, 16 | 0, 0, 0, 0, 0, 0, 0, 0, 17 | 0, 0, 0, 0, 0, 0, 0, 0, 18 | 0, 0, 0, 0, 0, 0, 0, 0, 19 | 0, 0, 0, 0, 0, 0, 0, 0, 20 | 0, 0, 0, 0, 0, 0, 0, 0, 21 | 0, 0, 0, 0, 0, 0, 0, 0, 22 | 0, 0, 0, 0, 0, 0, 0, 1, 23 | } 24 | hash := HashPassword(salt, "pass") 25 | 26 | /* 27 | The hash result is verified by openssl: 28 | 29 | together := make([]byte, sha512.Size + 4) 30 | copy(together, salt[:]) 31 | copy(together[sha512.Size:], []byte("pass")) 32 | _, out, _, err := sys.Exec(bytes.NewReader(together), nil, nil, "openssl", "sha512", "-binary") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | fmt.Println([]byte(out)) 37 | */ 38 | 39 | if hash != [sha512.Size]byte{ 40 | 190, 95, 250, 155, 21, 243, 233, 118, 41 | 22, 89, 166, 222, 173, 141, 32, 238, 42 | 90, 23, 90, 45, 118, 146, 138, 167, 43 | 41, 193, 77, 223, 139, 238, 192, 82, 44 | 15, 208, 110, 7, 67, 139, 254, 210, 45 | 182, 211, 16, 39, 244, 118, 68, 104, 46 | 228, 23, 175, 140, 112, 149, 59, 116, 47 | 58, 90, 141, 114, 104, 209, 219, 36, 48 | } { 49 | t.Fatal(hash) 50 | } 51 | } 52 | 53 | func TestNewSalt(t *testing.T) { 54 | salt := NewSalt() 55 | all0 := true 56 | for _, b := range salt { 57 | if b != 0 { 58 | all0 = false 59 | } 60 | } 61 | if all0 { 62 | t.Fatal(salt) 63 | } 64 | salt2 := NewSalt() 65 | if reflect.DeepEqual(salt, salt2) { 66 | t.Fatal("not random") 67 | } 68 | } 69 | 70 | func TestServiceReadFromSysconfig(t *testing.T) { 71 | sysconf := GetDefaultKeySvcConf() 72 | svcConf := CryptServiceConfig{} 73 | if err := svcConf.ReadFromSysconfig(sysconf); err == nil { 74 | t.Fatal("did not error") 75 | } 76 | // Fill in blanks in the default configuration and load once more 77 | hash := HashPassword(NewSalt(), "") 78 | sysconf.Set(SRV_CONF_PASS_HASH, hex.EncodeToString(hash[:])) 79 | salt := NewSalt() 80 | sysconf.Set(SRV_CONF_PASS_SALT, hex.EncodeToString(salt[:])) 81 | sysconf.Set(SRV_CONF_TLS_CERT, "/etc/os-release") 82 | sysconf.Set(SRV_CONF_TLS_KEY, "/etc/os-release") 83 | sysconf.Set(SRV_CONF_LISTEN_ADDR, "1.1.1.1") 84 | sysconf.Set(SRV_CONF_LISTEN_PORT, "1234") 85 | sysconf.Set(SRV_CONF_KEYDB_DIR, "/abc") 86 | sysconf.Set(SRV_CONF_MAIL_CREATION_SUBJ, "a") 87 | sysconf.Set(SRV_CONF_MAIL_CREATION_TEXT, "b") 88 | sysconf.Set(SRV_CONF_MAIL_RETRIEVAL_SUBJ, "c") 89 | sysconf.Set(SRV_CONF_MAIL_RETRIEVAL_TEXT, "d") 90 | if err := svcConf.ReadFromSysconfig(sysconf); err == nil { 91 | t.Fatal("did not error on bad tls") 92 | } 93 | sysconf.Set("TLS_CERT_PEM", path.Join(PkgInGopath, "keyserv", "rpc_test.crt")) 94 | sysconf.Set("TLS_CERT_KEY_PEM", path.Join(PkgInGopath, "keyserv", "rpc_test.key")) 95 | if err := svcConf.ReadFromSysconfig(sysconf); err != nil { 96 | t.Fatal(err) 97 | } 98 | if !reflect.DeepEqual(svcConf, CryptServiceConfig{ 99 | PasswordHash: hash, 100 | PasswordSalt: salt, 101 | CertPEM: path.Join(PkgInGopath, "keyserv", "rpc_test.crt"), 102 | KeyPEM: path.Join(PkgInGopath, "keyserv", "rpc_test.key"), 103 | Address: "1.1.1.1", 104 | Port: 1234, 105 | KeyDBDir: "/abc", 106 | KeyCreationSubject: "a", 107 | KeyCreationGreeting: "b", 108 | KeyRetrievalSubject: "c", 109 | KeyRetrievalGreeting: "d", 110 | KMIPAddresses: []string{}, 111 | KMIPTLSDoVerify: true, 112 | }) { 113 | t.Fatalf("%+v", svcConf) 114 | } 115 | } 116 | 117 | // RPC functions are tested by CryptClient test cases. 118 | -------------------------------------------------------------------------------- /kmip/structure/const.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package structure 4 | 5 | import ( 6 | "encoding/hex" 7 | "github.com/SUSE/cryptctl/kmip/ttlv" 8 | "log" 9 | ) 10 | 11 | var AllDefinedTags = map[string]ttlv.Tag{} // String encoded hex value of tag VS tag 12 | 13 | // Place a tag into AllDefinedTags map for faster look-up. 14 | func RegisterDefinedTag(str string) (ret ttlv.Tag) { 15 | decoded, err := hex.DecodeString(str) 16 | if err != nil { 17 | log.Panicf("RegisterDefinedTag: failed to decode hex tag string - %v", err) 18 | } 19 | copy(ret[:], decoded) 20 | AllDefinedTags[str] = ret 21 | return ret 22 | } 23 | 24 | // Create request 25 | var TagRequestMessage = RegisterDefinedTag("420078") 26 | var TagRequestHeader = RegisterDefinedTag("420077") 27 | var TagProtocolVersion = RegisterDefinedTag("420069") 28 | var TagProtocolVersionMajor = RegisterDefinedTag("42006a") 29 | 30 | const ValProtocolVersionMajorKMIP1_3 = 1 31 | 32 | var TagProtocolVersionMinor = RegisterDefinedTag("42006b") 33 | 34 | const ValProtocolVersionMinorKMIP1_3 = 2 35 | 36 | var TagAuthentication = RegisterDefinedTag("42000c") 37 | var TagCredential = RegisterDefinedTag("420023") 38 | var TagCredentialType = RegisterDefinedTag("420024") 39 | 40 | const ValCredentialTypeUsernamePassword = 1 41 | 42 | var TagCredentialValue = RegisterDefinedTag("420025") 43 | var TagUsername = RegisterDefinedTag("420099") 44 | var TagPassword = RegisterDefinedTag("4200a1") 45 | 46 | var TagBatchCount = RegisterDefinedTag("42000d") 47 | var TagBatchItem = RegisterDefinedTag("42000f") 48 | var TagOperation = RegisterDefinedTag("42005c") 49 | 50 | const ValOperationCreate = 1 51 | 52 | var TagRequestPayload = RegisterDefinedTag("420079") 53 | var TagObjectType = RegisterDefinedTag("420057") 54 | 55 | const ValAttributeNameKeyName = "Name" 56 | 57 | const ValObjectTypeSymmetricKey = 2 58 | 59 | var TagTemplateAttribute = RegisterDefinedTag("420091") 60 | var TagAttribute = RegisterDefinedTag("420008") 61 | var TagAttributeName = RegisterDefinedTag("42000a") 62 | 63 | const ValAttributeNameCryptoAlg = "Cryptographic Algorithm" 64 | const ValAttributeNameCryptoLen = "Cryptographic Length" 65 | const ValAttributeNameCryptoUsageMask = "Cryptographic Usage Mask" 66 | const MaskCryptoUsageEncrypt = 4 67 | const MaskCryptoUsageDecrypt = 8 68 | 69 | var TagAttributeValue = RegisterDefinedTag("42000b") 70 | var TagNameType = RegisterDefinedTag("420054") 71 | 72 | const ValNameTypeText = 1 73 | 74 | var TagNameValue = RegisterDefinedTag("420055") 75 | 76 | // Create response 77 | var TagResponseMessage = RegisterDefinedTag("42007b") 78 | var TagResponseHeader = RegisterDefinedTag("42007a") 79 | var TagTimestamp = RegisterDefinedTag("420092") 80 | var TagResultStatus = RegisterDefinedTag("42007f") 81 | 82 | const ValResultStatusSuccess = 0 83 | const ValResultStatusFailed = 1 84 | const ValResultStatusPending = 2 85 | const ValResultStatusUndone = 3 86 | 87 | const ValResultReasonNotFound = 1 88 | 89 | var TagResponsePayload = RegisterDefinedTag("42007c") 90 | var TagUniqueID = RegisterDefinedTag("420094") 91 | 92 | // Get request 93 | const ValOperationGet = 10 94 | 95 | // Get response 96 | var TagSymmetricKey = RegisterDefinedTag("42008f") 97 | var TagKeyBlock = RegisterDefinedTag("420040") 98 | var TagFormatType = RegisterDefinedTag("420042") 99 | var TagKeyValue = RegisterDefinedTag("420045") 100 | var TagKeyMaterial = RegisterDefinedTag("420043") 101 | var TagCryptoAlgorithm = RegisterDefinedTag("420028") 102 | var TagCryptoLen = RegisterDefinedTag("42002a") 103 | var TagResultReason = RegisterDefinedTag("42007e") 104 | var TagResultMessage = RegisterDefinedTag("42007d") 105 | 106 | const ValKeyFormatTypeRaw = 1 107 | const ValCryptoAlgoAES = 3 108 | 109 | // Destroy request 110 | const ValOperationDestroy = 20 111 | 112 | // Destroy response - nothing more 113 | -------------------------------------------------------------------------------- /keyserv/mail.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keyserv 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/sys" 9 | "net/smtp" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | SRV_CONF_MAIL_RECIPIENTS = "EMAIL_RECIPIENTS" 16 | SRV_CONF_MAIL_FROM_ADDR = "EMAIL_FROM_ADDRESS" 17 | SRV_CONF_MAIL_AGENT_AND_PORT = "EMAIL_AGENT_AND_PORT" 18 | SRV_CONF_MAIL_AGENT_USERNAME = "EMAIL_AGENT_USERNAME" 19 | SRV_CONF_MAIL_AGENT_PASSWORD = "EMAIL_AGENT_PASSWORD" 20 | ) 21 | 22 | // Return true only if both at-sign and full-stop are in the string. 23 | func IsMailAddressComplete(addr string) bool { 24 | return strings.Contains(addr, "@") 25 | } 26 | 27 | // Parameters for sending notification emails. 28 | type Mailer struct { 29 | Recipients []string // List of Email addresses that receive notifications 30 | FromAddress string // FROM address of the notifications 31 | AgentAddressPort string // Address and port number of mail transportation agent for sending notifications 32 | AuthUsername string // (Optional) Username for plain authentication, if the SMTP server requires it. 33 | AuthPassword string // (Optional) Password for plain authentication, if the SMTP server requires it. 34 | } 35 | 36 | // Return true only if all mail parameters are present. 37 | func (mail *Mailer) ValidateConfig() error { 38 | errs := make([]error, 0, 0) 39 | // Validate recipient addresses 40 | if mail.Recipients == nil || len(mail.Recipients) == 0 { 41 | errs = append(errs, errors.New("Recipient address is empty")) 42 | } else { 43 | for _, addr := range mail.Recipients { 44 | if !IsMailAddressComplete(addr) { 45 | errs = append(errs, fmt.Errorf("Recipient address \"%s\" must contain an at-sign", addr)) 46 | } 47 | } 48 | } 49 | // Validate from address 50 | if mail.FromAddress == "" { 51 | errs = append(errs, errors.New("Mail-from address is empty")) 52 | } else if !IsMailAddressComplete(mail.FromAddress) { 53 | errs = append(errs, fmt.Errorf("Mail-from address \"%s\" must contain an at-sign", mail.FromAddress)) 54 | } 55 | // Validate MTA 56 | if mail.AgentAddressPort == "" { 57 | errs = append(errs, errors.New("Mail agent (address and port) is empty")) 58 | } else { 59 | colon := strings.Index(mail.AgentAddressPort, ":") 60 | if colon == -1 { 61 | errs = append(errs, fmt.Errorf("Mail agent \"%s\" must contain address and port number", mail.FromAddress)) 62 | } else if _, err := strconv.Atoi(mail.AgentAddressPort[colon+1:]); err != nil { 63 | errs = append(errs, fmt.Errorf("Failed to parse integer from port number from \"%s\"", mail.FromAddress)) 64 | } 65 | } 66 | if len(errs) == 0 { 67 | return nil 68 | } 69 | return fmt.Errorf("%v", errs) 70 | } 71 | 72 | // Deliver an email to all recipients. 73 | func (mail *Mailer) Send(subject, text string) error { 74 | if mail.Recipients == nil || len(mail.Recipients) == 0 { 75 | return fmt.Errorf("No recipient specified for mail \"%s\"", subject) 76 | } 77 | var auth smtp.Auth 78 | if mail.AuthUsername != "" { 79 | auth = smtp.PlainAuth("", mail.AuthUsername, mail.AuthPassword, mail.AgentAddressPort) 80 | } 81 | // Construct appropriate mail headers 82 | mailBody := fmt.Sprintf("MIME-Version: 1.0\r\nContent-type: text/plain; charset=utf-8\r\nFrom: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s", 83 | mail.FromAddress, strings.Join(mail.Recipients, ", "), subject, text) 84 | return smtp.SendMail(mail.AgentAddressPort, auth, mail.FromAddress, mail.Recipients, []byte(mailBody)) 85 | } 86 | 87 | // Read mail settings from keys in sysconfig file. 88 | func (mail *Mailer) ReadFromSysconfig(sysconf *sys.Sysconfig) { 89 | mail.Recipients = sysconf.GetStringArray(SRV_CONF_MAIL_RECIPIENTS, []string{}) 90 | mail.FromAddress = sysconf.GetString(SRV_CONF_MAIL_FROM_ADDR, "") 91 | mail.AgentAddressPort = sysconf.GetString(SRV_CONF_MAIL_AGENT_AND_PORT, "") 92 | mail.AuthUsername = sysconf.GetString(SRV_CONF_MAIL_AGENT_USERNAME, "") 93 | mail.AuthPassword = sysconf.GetString(SRV_CONF_MAIL_AGENT_PASSWORD, "") 94 | } 95 | -------------------------------------------------------------------------------- /kmip/structure/op_destroy.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package structure 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/kmip/ttlv" 9 | ) 10 | 11 | // KMIP request message 420078 12 | type SDestroyRequest struct { 13 | SRequestHeader SRequestHeader // IBatchCount is assumed to be 1 in serialisation operations 14 | SRequestBatchItem SRequestBatchItem // payload is SRequestPayloadDestroy 15 | } 16 | 17 | func (destroyReq SDestroyRequest) SerialiseToTTLV() ttlv.Item { 18 | destroyReq.SRequestHeader.IBatchCount.Value = 1 19 | ret := ttlv.NewStructure(TagRequestMessage, destroyReq.SRequestHeader.SerialiseToTTLV(), destroyReq.SRequestBatchItem.SerialiseToTTLV()) 20 | return ret 21 | } 22 | func (destroyReq *SDestroyRequest) DeserialiseFromTTLV(in ttlv.Item) error { 23 | if err := DecodeStructItem(in, TagRequestMessage, TagRequestHeader, &destroyReq.SRequestHeader); err != nil { 24 | return err 25 | } 26 | if val := destroyReq.SRequestHeader.IBatchCount.Value; val != 1 { 27 | return fmt.Errorf("SDestroyRequest.DeserialiseFromTTLV: was expecting exactly 1 item, but received %d instead.", val) 28 | } 29 | destroyReq.SRequestBatchItem = SRequestBatchItem{SRequestPayload: &SRequestPayloadDestroy{}} 30 | if err := DecodeStructItem(in, TagRequestMessage, TagBatchItem, &destroyReq.SRequestBatchItem); err != nil { 31 | return err 32 | } 33 | if destroyReq.SRequestBatchItem.EOperation.Value != ValOperationDestroy { 34 | return errors.New("SDestroyRequest.DeserialiseFromTTLV: input is not a destroy request") 35 | } 36 | return nil 37 | } 38 | 39 | // 420079 - request payload from a delete request 40 | type SRequestPayloadDestroy struct { 41 | TUniqueID ttlv.Text // 420094 42 | } 43 | 44 | func (deletePayload SRequestPayloadDestroy) SerialiseToTTLV() ttlv.Item { 45 | deletePayload.TUniqueID.Tag = TagUniqueID 46 | return ttlv.NewStructure(TagRequestPayload, &deletePayload.TUniqueID) 47 | } 48 | func (deletePayload *SRequestPayloadDestroy) DeserialiseFromTTLV(in ttlv.Item) error { 49 | if err := DecodeStructItem(in, TagRequestPayload, TagUniqueID, &deletePayload.TUniqueID); err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | 55 | // KMIP response message 42007b 56 | type SDestroyResponse struct { 57 | SResponseHeader SResponseHeader // IBatchCount is assumed to be 1 in serialisation operations 58 | SResponseBatchItem SResponseBatchItem // payload is SResponsePayloadDestroy 59 | } 60 | 61 | func (destroyResp SDestroyResponse) SerialiseToTTLV() ttlv.Item { 62 | destroyResp.SResponseHeader.IBatchCount.Value = 1 63 | ret := ttlv.NewStructure(TagResponseMessage, destroyResp.SResponseHeader.SerialiseToTTLV(), destroyResp.SResponseBatchItem.SerialiseToTTLV()) 64 | return ret 65 | } 66 | func (destroyResp *SDestroyResponse) DeserialiseFromTTLV(in ttlv.Item) error { 67 | if err := DecodeStructItem(in, TagResponseMessage, TagResponseHeader, &destroyResp.SResponseHeader); err != nil { 68 | return err 69 | } 70 | if val := destroyResp.SResponseHeader.IBatchCount.Value; val != 1 { 71 | return fmt.Errorf("SDestroyResponse.DeserialiseFromTTLV: was expecting exactly 1 item, but received %d instead.", val) 72 | } 73 | destroyResp.SResponseBatchItem = SResponseBatchItem{SResponsePayload: &SResponsePayloadDestroy{}} 74 | if err := DecodeStructItem(in, TagResponseMessage, TagBatchItem, &destroyResp.SResponseBatchItem); err != nil { 75 | return err 76 | } 77 | if destroyResp.SResponseBatchItem.EOperation.Value != ValOperationDestroy { 78 | return errors.New("SDestroyResponse.DeserialiseFromTTLV: input is not a destroy response") 79 | } 80 | return nil 81 | } 82 | 83 | // 42007c - response payload from a destroy response 84 | type SResponsePayloadDestroy struct { 85 | TUniqueID ttlv.Text // 420094 86 | } 87 | 88 | func (deletePayload SResponsePayloadDestroy) SerialiseToTTLV() ttlv.Item { 89 | deletePayload.TUniqueID.Tag = TagUniqueID 90 | return ttlv.NewStructure(TagResponsePayload, &deletePayload.TUniqueID) 91 | } 92 | func (deletePayload *SResponsePayloadDestroy) DeserialiseFromTTLV(in ttlv.Item) error { 93 | if err := DecodeStructItem(in, TagResponsePayload, TagUniqueID, &deletePayload.TUniqueID); err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /sys/daemon.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package sys 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net" 9 | "os" 10 | "os/exec" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | // Make a best effort at determining this computer's host name (FQDN preferred) and IP address. 17 | func GetHostnameAndIP() (hostname string, ip string) { 18 | var err error 19 | if hostname, err = os.Hostname(); err != nil { 20 | log.Printf("GetHostname: cannot determine system host name - %v", err) // non-fatal 21 | } 22 | // Determine FQDN and IP address if possible 23 | hostnameAddresses, err := net.LookupIP(hostname) 24 | if err == nil { 25 | var addressText string 26 | for _, hostnameAddress := range hostnameAddresses { 27 | ipAddrBytes, err := hostnameAddress.MarshalText() 28 | addressText = string(ipAddrBytes) 29 | if err != nil { 30 | continue 31 | } 32 | fqdn, err := net.LookupAddr(string(addressText)) 33 | if err == nil && len(fqdn) > 0 { 34 | hostname = fqdn[0] 35 | if ip == "" { 36 | ip = string(addressText) 37 | } 38 | break 39 | } 40 | } 41 | // Even if FQDN cannot be determined, the IP address should still be recorded. 42 | if ip == "" { 43 | ip = addressText 44 | } 45 | } 46 | hostname = strings.TrimSuffix(hostname, ".") 47 | ip = strings.TrimSuffix(ip, ".") 48 | return 49 | } 50 | 51 | // Call systemctl start on the service. 52 | func SystemctlStart(svc string) error { 53 | if out, err := exec.Command("systemctl", "start", svc).CombinedOutput(); err != nil { 54 | return fmt.Errorf("Failed to start service \"%s\" - %v %s", svc, err, out) 55 | } 56 | return nil 57 | } 58 | 59 | // Cal systemctl enable and then systemctl start on the service. 60 | func SystemctlEnableStart(svc string) error { 61 | if out, err := exec.Command("systemctl", "enable", svc).CombinedOutput(); err != nil { 62 | return fmt.Errorf("Failed to enable service \"%s\" - %v %s", svc, err, out) 63 | } 64 | if out, err := exec.Command("systemctl", "start", svc).CombinedOutput(); err != nil { 65 | return fmt.Errorf("Failed to start service \"%s\" - %v %s", svc, err, out) 66 | } 67 | return nil 68 | } 69 | 70 | // Cal systemctl enable and then systemctl start on thing. Panic on error. 71 | func SystemctlEnableRestart(svc string) error { 72 | if out, err := exec.Command("systemctl", "enable", svc).CombinedOutput(); err != nil { 73 | return fmt.Errorf("Failed to enable service \"%s\" - %v %s", svc, err, out) 74 | } 75 | if out, err := exec.Command("systemctl", "restart", svc).CombinedOutput(); err != nil { 76 | return fmt.Errorf("Failed to restart service \"%s\" - %v %s", svc, err, out) 77 | } 78 | return nil 79 | } 80 | 81 | // Cal systemctl to get main PID of a service. Return 0 on failure. 82 | func SystemctlGetMainPID(svc string) (mainPID int) { 83 | out, err := exec.Command("systemctl", "show", "-p", "MainPID", svc).CombinedOutput() 84 | if err != nil { 85 | return 0 86 | } 87 | idNum := regexp.MustCompile("[0-9]+").FindString(string(out)) 88 | if idNum == "" { 89 | return 0 90 | } 91 | mainPID, err = strconv.Atoi(idNum) 92 | if err != nil { 93 | return 0 94 | } 95 | return 96 | } 97 | 98 | // SystemctlStop uses systemctl command to disable and stop a service. 99 | func SystemctlDisableStop(svc string) error { 100 | if out, err := exec.Command("systemctl", "disable", svc).CombinedOutput(); err != nil { 101 | return fmt.Errorf("Failed to disable service \"%s\" - %v %s", svc, err, out) 102 | } 103 | if out, err := exec.Command("systemctl", "stop", svc).CombinedOutput(); err != nil { 104 | return fmt.Errorf("Failed to stop service \"%s\" - %v %s", svc, err, out) 105 | } 106 | return nil 107 | } 108 | 109 | // SystemctlStop uses systemctl command to stop a service. 110 | func SystemctlStop(svc string) error { 111 | if out, err := exec.Command("systemctl", "stop", svc).CombinedOutput(); err != nil { 112 | return fmt.Errorf("Failed to stop service \"%s\" - %v %s", svc, err, out) 113 | } 114 | return nil 115 | } 116 | 117 | // Return true only if systemctl suggests that the thing is running. 118 | func SystemctlIsRunning(svc string) bool { 119 | if _, err := exec.Command("systemctl", "is-active", svc).CombinedOutput(); err == nil { 120 | return true 121 | } 122 | return false 123 | } 124 | -------------------------------------------------------------------------------- /sys/term.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package sys 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | var TermEcho bool = true // keep track of the latest change to terminal echo made by SetTermEcho function 17 | 18 | // Enable or disable terminal echo. 19 | func SetTermEcho(echo bool) { 20 | term := &syscall.Termios{} 21 | stdout := os.Stdout.Fd() 22 | _, _, err := syscall.Syscall(syscall.SYS_IOCTL, stdout, syscall.TCGETS, uintptr(unsafe.Pointer(term))) 23 | if err != 0 { 24 | log.Printf("SetTermEcho: syscall failed - %v", err) 25 | } 26 | if echo { 27 | term.Lflag |= syscall.ECHO 28 | } else { 29 | term.Lflag &^= syscall.ECHO 30 | } 31 | _, _, err = syscall.Syscall(syscall.SYS_IOCTL, stdout, uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(term))) 32 | if err != 0 { 33 | log.Printf("SetTermEcho: syscall failed - %v", err) 34 | } 35 | TermEcho = echo 36 | } 37 | 38 | /* 39 | Print a prompt in stdout and return a trimmed line read from stdin. 40 | If mandatory switch is turned on, the function will keep asking for an input if default hint is unavailable. 41 | */ 42 | func Input(mandatory bool, defaultHint string, format string, values ...interface{}) string { 43 | if defaultHint == "" { 44 | fmt.Printf(format+": ", values...) 45 | } else { 46 | fmt.Printf(format+" ["+defaultHint+"]: ", values...) 47 | } 48 | for { 49 | str, err := bufio.NewReader(os.Stdin).ReadString('\n') 50 | if err != nil { 51 | log.Panicf("Input: failed to read from stadard input - %v", err) 52 | } 53 | str = strings.TrimSpace(str) 54 | if str == "" && mandatory && defaultHint == "" { 55 | if !TermEcho { 56 | fmt.Println() 57 | } 58 | fmt.Print("Please enter a value: ") 59 | os.Stdout.Sync() 60 | continue 61 | } 62 | return str 63 | } 64 | } 65 | 66 | // Disable terminal echo and read a password input from stdin, then re-enable terminal echo. 67 | func InputPassword(mandatory bool, defaultHint string, format string, values ...interface{}) string { 68 | SetTermEcho(false) 69 | defer SetTermEcho(true) 70 | ret := Input(mandatory, defaultHint, format, values...) 71 | fmt.Println() // because the new-line character was not echoed by password entry 72 | return ret 73 | } 74 | 75 | // Print a prompt in stdout and return an integer read from stdin. 76 | func InputInt(mandatory bool, defaultHint, lowerLimit, upperLimit int, format string, values ...interface{}) int { 77 | for { 78 | valStr := Input(mandatory, strconv.Itoa(defaultHint), format, values...) 79 | if valStr == "" { 80 | return defaultHint 81 | } 82 | valInt, err := strconv.Atoi(valStr) 83 | if err != nil { 84 | fmt.Println("Please enter a whole number.") 85 | continue 86 | } 87 | if valInt < lowerLimit || valInt > upperLimit { 88 | fmt.Printf("Please enter a number between %d and %d.\n", lowerLimit, upperLimit) 89 | continue 90 | } 91 | return valInt 92 | } 93 | } 94 | 95 | // Print a prompt in stdout and return a boolean value read from stdin. 96 | func InputBool(defaultHint bool, format string, values ...interface{}) bool { 97 | defaultStr := "yes" 98 | if !defaultHint { 99 | defaultStr = "no" 100 | } 101 | for { 102 | answer := Input(false, defaultStr, format, values...) 103 | switch strings.TrimSpace(strings.ToLower(answer)) { 104 | case "y": 105 | fallthrough 106 | case "yes": 107 | fallthrough 108 | case "ja": 109 | return true 110 | case "n": 111 | fallthrough 112 | case "no": 113 | fallthrough 114 | case "nein": 115 | return false 116 | case "": 117 | return defaultHint 118 | default: 119 | fmt.Print("Please enter \"yes\" or \"no\": ") 120 | os.Stdout.Sync() 121 | continue 122 | } 123 | } 124 | } 125 | 126 | // Print a prompt in stdout and return a file path (must exist be absolute) read from stdin. 127 | func InputAbsFilePath(mandatory bool, defaultHint string, format string, values ...interface{}) string { 128 | for { 129 | val := Input(mandatory, defaultHint, format, values...) 130 | if val == "" { 131 | return defaultHint 132 | } 133 | if val[0] != '/' { 134 | fmt.Println("Please enter an absolute path led by a slash.") 135 | continue 136 | } 137 | if _, err := os.Stat(val); err != nil { 138 | fmt.Printf("The location \"%s\" cannot be read, please double check your input.\n", val) 139 | continue 140 | } 141 | return val 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /sys/sysconfig_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package sys 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | var sysconfSampleText = `## Path: Productivity/Other 12 | ## Description: Limits for system tuning profile "sap-netweaver". 13 | ## ServiceRestart: tuned 14 | 15 | ## Type: integer 16 | ## Default: 8388608 17 | # 18 | # The lower tuning limit of the size of tmpfs mounted on /dev/shm in KiloBytes. 19 | # It should not be smaller than 8388608 (8GB). 20 | # 21 | TMPFS_SIZE_MIN=8388608 22 | UTF_TEST="在续《植战僵大尸2》出1年后推,系作《植物列新大战尸全明星》已于2015年9月17日登陆平iOS台" 23 | 24 | ## Type: regexp(^@(sapsys|sdba|dba)[[:space:]]+(-|hard|soft)[[:space:]]+(nofile)[[:space:]]+[[:digit:]]+) 25 | ## Default: "" 26 | # 27 | # Maximum number of open files for SAP application groups sapsys, sdba, and dba. 28 | # Consult with manual page limits.conf(5) for the correct syntax. 29 | # 30 | LIMIT_1="@sapsys soft nofile 65536" 31 | LIMIT_2="@sapsys hard nofile 65536" 32 | BOOL_TEST_YES="yes" 33 | BOOL_TEST_TRUE="true" 34 | BOOL_TEST_EMPTY="" 35 | BOOL_TEST_NO="no" 36 | BOOL_TEST_FALSE="false" 37 | STRARY_TEST=" foo bar abc " 38 | INTARY_TEST=" 12 34 abc 56 " 39 | ` 40 | 41 | var sysconfigMatchText = `## Path: Productivity/Other 42 | ## Description: Limits for system tuning profile "sap-netweaver". 43 | ## ServiceRestart: tuned 44 | 45 | ## Type: integer 46 | ## Default: 8388608 47 | # 48 | # The lower tuning limit of the size of tmpfs mounted on /dev/shm in KiloBytes. 49 | # It should not be smaller than 8388608 (8GB). 50 | # 51 | TMPFS_SIZE_MIN="8388608" 52 | UTF_TEST="在续《植战僵大尸2》出1年后推,系作《植物列新大战尸全明星》已于2015年9月17日登陆平iOS台" 53 | 54 | ## Type: regexp(^@(sapsys|sdba|dba)[[:space:]]+(-|hard|soft)[[:space:]]+(nofile)[[:space:]]+[[:digit:]]+) 55 | ## Default: "" 56 | # 57 | # Maximum number of open files for SAP application groups sapsys, sdba, and dba. 58 | # Consult with manual page limits.conf(5) for the correct syntax. 59 | # 60 | LIMIT_1="new value" 61 | LIMIT_2="@sapsys hard nofile 65536" 62 | BOOL_TEST_YES="yes" 63 | BOOL_TEST_TRUE="true" 64 | BOOL_TEST_EMPTY="" 65 | BOOL_TEST_NO="no" 66 | BOOL_TEST_FALSE="false" 67 | STRARY_TEST="foo bar" 68 | INTARY_TEST="12 34" 69 | newkey="orange" 70 | STRARY_TEST2="foo bar" 71 | ` 72 | 73 | func TestSysconfig(t *testing.T) { 74 | // Parse the sample text 75 | conf, err := ParseSysconfig(sysconfSampleText) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | // Read keys 80 | if val := conf.GetString("LIMIT_1", ""); val != "@sapsys soft nofile 65536" { 81 | t.Fatal(val) 82 | } 83 | if val := conf.GetString("TMPFS_SIZE_MIN", ""); val != "8388608" { 84 | t.Fatal(val) 85 | } 86 | if val := conf.GetInt("TMPFS_SIZE_MIN", 0); val != 8388608 { 87 | t.Fatal(val) 88 | } 89 | if val := conf.GetUint64("TMPFS_SIZE_MIN", 0); val != 8388608 { 90 | t.Fatal(val) 91 | } 92 | if val := conf.GetString("KEY_DOES_NOT_EXIST", "DEFAULT"); val != "DEFAULT" { 93 | t.Fatal(val) 94 | } 95 | if val := conf.GetInt("KEY_DOES_NOT_EXIST", 12); val != 12 { 96 | t.Fatal(val) 97 | } 98 | // Read array keys 99 | if val := conf.GetStringArray("STRARY_TEST", nil); !reflect.DeepEqual(val, []string{"foo", "bar", "abc"}) { 100 | t.Fatal(val) 101 | } 102 | if val := conf.GetIntArray("INTARY_TEST", nil); !reflect.DeepEqual(val, []int{12, 34, 56}) { 103 | t.Fatal(val) 104 | } 105 | // Read boolean keys 106 | if val := conf.GetBool("BOOL_TEST_YES", false); !val { 107 | t.Fatal(val) 108 | } 109 | if val := conf.GetBool("BOOL_TEST_TRUE", false); !val { 110 | t.Fatal(val) 111 | } 112 | if val := conf.GetBool("BOOL_TEST_EMPTY", true); !val { 113 | t.Fatal(val) 114 | } 115 | if val := conf.GetBool("BOOL_TEST_EMPTY", false); val { 116 | t.Fatal(val) 117 | } 118 | if val := conf.GetBool("BOOL_TEST_NO", true); val { 119 | t.Fatal(val) 120 | } 121 | if val := conf.GetBool("BOOL_TEST_FALSE", true); val { 122 | t.Fatal(val) 123 | } 124 | // Write keys 125 | conf.Set("LIMIT_1", "new value") 126 | conf.Set("newkey", "orange") 127 | if val := conf.GetString("LIMIT_1", ""); val != "new value" { 128 | t.Fatal(val) 129 | } 130 | if val := conf.GetInt("newkey", 12); val != 12 { 131 | t.Fatal(val) 132 | } 133 | if val := conf.GetString("newkey", ""); val != "orange" { 134 | t.Fatal(val) 135 | } 136 | // Write array keys 137 | conf.SetIntArray("INTARY_TEST", []int{12, 34}) 138 | conf.SetStrArray("STRARY_TEST", []string{"foo", "bar"}) 139 | conf.SetStrArray("STRARY_TEST2", []string{"foo", "bar"}) 140 | // The converted back text should carry "new value" for LIMIT_1 and newkey. 141 | if txt := conf.ToText(); txt != sysconfigMatchText { 142 | fmt.Println("==================") 143 | fmt.Println(txt) 144 | fmt.Println("==================") 145 | t.Fatal("failed to convert back into text") 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /keyserv/rpc_client_bench_test.go: -------------------------------------------------------------------------------- 1 | package keyserv 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkSaveKey(b *testing.B) { 9 | client, _, tearDown := StartTestServer(b) 10 | defer tearDown(b) 11 | // Retrieve server's password salt 12 | salt, err := client.GetSalt() 13 | if err != nil { 14 | b.Fatal(err) 15 | } 16 | // Run all transactions in a single goroutine 17 | oldMaxprocs := runtime.GOMAXPROCS(-1) 18 | defer runtime.GOMAXPROCS(oldMaxprocs) 19 | runtime.GOMAXPROCS(1) 20 | b.ResetTimer() 21 | // The benchmark will run all RPC operations consecutively 22 | for i := 0; i < b.N; i++ { 23 | if _, err := client.CreateKey(CreateKeyReq{ 24 | PlainPassword: TEST_RPC_PASS, 25 | Hostname: "localhost", 26 | UUID: "aaa", 27 | MountPoint: "/a", 28 | MountOptions: []string{"ro", "noatime"}, 29 | MaxActive: -1, 30 | AliveIntervalSec: 1, 31 | AliveCount: 4, 32 | }); err != nil { 33 | b.Fatal(err) 34 | } 35 | } 36 | b.StopTimer() 37 | } 38 | 39 | func BenchmarkAutoRetrieveKey(b *testing.B) { 40 | client, _, tearDown := StartTestServer(b) 41 | defer tearDown(b) 42 | // Retrieve server's password salt 43 | salt, err := client.GetSalt() 44 | if err != nil { 45 | b.Fatal(err) 46 | } 47 | // Run all transactions in a single goroutine 48 | oldMaxprocs := runtime.GOMAXPROCS(-1) 49 | defer runtime.GOMAXPROCS(oldMaxprocs) 50 | runtime.GOMAXPROCS(1) 51 | if _, err := client.CreateKey(CreateKeyReq{ 52 | PlainPassword: TEST_RPC_PASS, 53 | Hostname: "localhost", 54 | UUID: "aaa", 55 | MountPoint: "/a", 56 | MountOptions: []string{"ro", "noatime"}, 57 | MaxActive: -1, 58 | AliveIntervalSec: 1, 59 | AliveCount: 4, 60 | }); err != nil { 61 | b.Fatal(err) 62 | } 63 | b.ResetTimer() 64 | // The benchmark will run all RPC operations consecutively 65 | for i := 0; i < b.N; i++ { 66 | if resp, err := client.AutoRetrieveKey(AutoRetrieveKeyReq{ 67 | UUIDs: []string{"aaa"}, 68 | Hostname: "localhost", 69 | }); err != nil || len(resp.Granted) != 1 { 70 | b.Fatal(err, resp) 71 | } 72 | } 73 | b.StopTimer() 74 | } 75 | 76 | func BenchmarkManualRetrieveKey(b *testing.B) { 77 | client, _, tearDown := StartTestServer(b) 78 | defer tearDown(b) 79 | // Retrieve server's password salt 80 | salt, err := client.GetSalt() 81 | if err != nil { 82 | b.Fatal(err) 83 | } 84 | // Run all transactions in a single goroutine 85 | oldMaxprocs := runtime.GOMAXPROCS(-1) 86 | defer runtime.GOMAXPROCS(oldMaxprocs) 87 | runtime.GOMAXPROCS(1) 88 | if _, err := client.CreateKey(CreateKeyReq{ 89 | PlainPassword: TEST_RPC_PASS, 90 | Hostname: "localhost", 91 | UUID: "aaa", 92 | MountPoint: "/a", 93 | MountOptions: []string{"ro", "noatime"}, 94 | MaxActive: -1, 95 | AliveIntervalSec: 1, 96 | AliveCount: 4, 97 | }); err != nil { 98 | b.Fatal(err) 99 | } 100 | b.ResetTimer() 101 | // The benchmark will run all RPC operations consecutively 102 | for i := 0; i < b.N; i++ { 103 | if resp, err := client.ManualRetrieveKey(ManualRetrieveKeyReq{ 104 | PlainPassword: TEST_RPC_PASS, 105 | UUIDs: []string{"aaa"}, 106 | Hostname: "localhost", 107 | }); err != nil || len(resp.Granted) != 1 { 108 | b.Fatal(err, resp) 109 | } 110 | } 111 | b.StopTimer() 112 | } 113 | 114 | func BenchmarkReportAlive(b *testing.B) { 115 | client, _, tearDown := StartTestServer(b) 116 | defer tearDown(b) 117 | // Retrieve server's password salt 118 | salt, err := client.GetSalt() 119 | if err != nil { 120 | b.Fatal(err) 121 | } 122 | // Run all benchmark operations in a single goroutine to know the real performance 123 | oldMaxprocs := runtime.GOMAXPROCS(-1) 124 | defer runtime.GOMAXPROCS(oldMaxprocs) 125 | runtime.GOMAXPROCS(1) 126 | if _, err := client.CreateKey(CreateKeyReq{ 127 | PlainPassword: TEST_RPC_PASS, 128 | Hostname: "localhost", 129 | UUID: "aaa", 130 | MountPoint: "/a", 131 | MountOptions: []string{"ro", "noatime"}, 132 | MaxActive: -1, 133 | AliveIntervalSec: 1, 134 | AliveCount: 4, 135 | }); err != nil { 136 | b.Fatal(err) 137 | } 138 | // Retrieve the key so that this computer becomes eligible to send alive messages 139 | if resp, err := client.ManualRetrieveKey(ManualRetrieveKeyReq{ 140 | PlainPassword: TEST_RPC_PASS, 141 | UUIDs: []string{"aaa"}, 142 | Hostname: "localhost", 143 | }); err != nil || len(resp.Granted) != 1 { 144 | b.Fatal(err, resp) 145 | } 146 | b.ResetTimer() 147 | // The benchmark will run all RPC operations consecutively 148 | for i := 0; i < b.N; i++ { 149 | if rejected, err := client.ReportAlive(ReportAliveReq{ 150 | UUIDs: []string{"aaa"}, 151 | Hostname: "localhost", 152 | }); err != nil || len(rejected) > 0 { 153 | b.Fatal(err, rejected) 154 | } 155 | } 156 | b.StopTimer() 157 | } 158 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 3 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "github.com/SUSE/cryptctl/command" 9 | "github.com/SUSE/cryptctl/sys" 10 | "os" 11 | "os/signal" 12 | "runtime" 13 | "syscall" 14 | ) 15 | 16 | func PrintHelpAndExit(exitStatus int) { 17 | fmt.Println(`cryptctl: encrypt and decrypt file systems using network key server. 18 | Copyright (C) 2017 SUSE Linux GmbH, Germany 19 | This program comes with ABSOLUTELY NO WARRANTY.This is free software, and you 20 | are welcome to redistribute it under certain conditions; see file "LICENSE". 21 | 22 | Maintain a key server: 23 | cryptctl init-server Set up this computer as a new key server. 24 | cryptctl list-keys Show all encryption keys. 25 | cryptctl show-key UUID Display pending-commands and details of a key. 26 | cryptctl edit-key UUID Edit stored key information. 27 | cryptctl send-command Record a pending mount/umount command for a disk. 28 | cryptctl clear-commands Clear all pending commands of a disk. 29 | 30 | Encrypt/unlock file systems: 31 | cryptctl encrypt Set up a new file system for encryption. 32 | cryptctl online-unlock Forcibly unlock all file systems via key server. 33 | cryptctl offline-unlock Unlock a file system via a key record file. 34 | `) 35 | os.Exit(exitStatus) 36 | } 37 | 38 | func main() { 39 | // Print stack trace of all goroutines on SIGQUIT for debugging 40 | osSignal := make(chan os.Signal, 1) 41 | signal.Notify(osSignal, syscall.SIGQUIT) 42 | go func() { 43 | for { 44 | <-osSignal 45 | outBuf := make([]byte, 2048) 46 | for { 47 | // Keep re-collecting stack traces until the buffer is large enough to hold all of them 48 | sizeWritten := runtime.Stack(outBuf, false) 49 | if len(outBuf) >= sizeWritten { 50 | fmt.Fprint(os.Stderr, string(outBuf)) 51 | break 52 | } 53 | outBuf = make([]byte, 2*len(outBuf)) 54 | } 55 | } 56 | }() 57 | 58 | if len(os.Args) == 1 { 59 | PrintHelpAndExit(0) 60 | } 61 | 62 | switch os.Args[1] { 63 | case "help": 64 | PrintHelpAndExit(0) 65 | case "daemon": 66 | // Server - run key service daemon 67 | if err := command.KeyRPCDaemon(); err != nil { 68 | sys.ErrorExit("%v", err) 69 | } 70 | case "init-server": 71 | // Server - complete the initial setup 72 | if err := command.InitKeyServer(); err != nil { 73 | sys.ErrorExit("%v", err) 74 | } 75 | case "list-keys": 76 | // Server - print all key records sorted according to last access 77 | if err := command.ListKeys(); err != nil { 78 | sys.ErrorExit("%v", err) 79 | } 80 | case "edit-key": 81 | // Server - let user edit key details such as mount point and mount options 82 | if len(os.Args) < 3 { 83 | sys.ErrorExit("Please specify UUID of the key that you wish to edit.") 84 | } 85 | if err := command.EditKey(os.Args[2]); err != nil { 86 | sys.ErrorExit("%v", err) 87 | } 88 | case "show-key": 89 | // Server - show key record details except key content 90 | if len(os.Args) < 3 { 91 | sys.ErrorExit("Please specify UUID of the key that you wish to see.") 92 | } 93 | if err := command.ShowKey(os.Args[2]); err != nil { 94 | sys.ErrorExit("%v", err) 95 | } 96 | case "send-command": 97 | if err := command.SendCommand(); err != nil { 98 | sys.ErrorExit("%v", err) 99 | } 100 | case "clear-commands": 101 | if err := command.ClearPendingCommands(); err != nil { 102 | sys.ErrorExit("%v", err) 103 | } 104 | case "client-daemon": 105 | // Client - run daemon that primarily polls and reacts to pending commands issued by RPC server 106 | if err := command.ClientDaemon(); err != nil { 107 | sys.ErrorExit("%v", err) 108 | } 109 | case "encrypt": 110 | // Client - set up a new encrypted disk 111 | if err := command.EncryptFS(); err != nil { 112 | sys.ErrorExit("%v", err) 113 | } 114 | case "auto-unlock": 115 | // Client - automatically unlock a file system without using a password 116 | if len(os.Args) < 3 { 117 | sys.ErrorExit("UUID is missing from command line parameters") 118 | } 119 | if err := command.AutoOnlineUnlockFS(os.Args[2]); err != nil { 120 | sys.ErrorExit("%v", err) 121 | } 122 | case "online-unlock": 123 | // Client - manually unlock all file systems using a key server and password 124 | if err := command.ManOnlineUnlockFS(); err != nil { 125 | sys.ErrorExit("%v", err) 126 | } 127 | case "offline-unlock": 128 | // Client - manually unlock a single file system using a key record file 129 | if err := command.ManOfflineUnlockFS(); err != nil { 130 | sys.ErrorExit("%v", err) 131 | } 132 | case "erase": 133 | // Client - erase encryption headers for the encrypted disk 134 | if err := command.EraseKey(); err != nil { 135 | sys.ErrorExit("%v", err) 136 | } 137 | default: 138 | PrintHelpAndExit(1) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /kmip/ttlv/types.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package ttlv 4 | 5 | import ( 6 | "bytes" 7 | "encoding/hex" 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | const ( 13 | TypStruct = 0x1 14 | TypInt = 0x2 15 | TypLong = 0x3 16 | TypeEnum = 0x5 17 | TypeText = 0x7 18 | TypeBytes = 0x8 19 | TypeDateTime = 0x9 20 | LenTTL = 3 + 1 + 4 // 3 bytes of tag, 1 byte of type, 4 bytes of length 21 | ) 22 | 23 | // All TTLV items implement this interface. 24 | type Item interface { 25 | GetTTL() TTL // Return TTL component of TTLV item. 26 | GetLength() int // Calculate value length and give the length to TTL. Apart from Structure, the length does not count padding. 27 | ResetTyp() // Reset Typ byte in TTL structure to the one corresponding to implementation. 28 | } 29 | 30 | type Tag [3]byte // The tag of TTLV consists of three bytes 31 | 32 | // Syntactic sugar to return byte slice equivalent of the tag. 33 | func (tag Tag) ByteSlice() []byte { 34 | return tag[:] 35 | } 36 | 37 | // Bytes, type, and value length excluding padding. 38 | type TTL struct { 39 | Tag Tag 40 | Typ byte 41 | Length int 42 | } 43 | 44 | // Return tag, type, and length in a string. 45 | func (com TTL) TTLString() string { 46 | return fmt.Sprintf("TAG %s TYP %d LEN %d", hex.EncodeToString(com.Tag[:]), com.Typ, com.Length) 47 | } 48 | 49 | // Write tag bytes and type byte into the buffer. 50 | func (com TTL) WriteTTTo(out *bytes.Buffer) { 51 | out.Write(com.Tag[:]) 52 | out.WriteByte(com.Typ) 53 | } 54 | 55 | // TTLV structure. Length of value is sum of item lengths including padding. 56 | type Structure struct { 57 | TTL 58 | Items []Item 59 | } 60 | 61 | func (st *Structure) GetTTL() TTL { 62 | return st.TTL 63 | } 64 | 65 | func (st *Structure) GetLength() int { 66 | newLen := 0 67 | for _, item := range st.Items { 68 | // Structure length counts individual item's TTL 69 | newLen += LenTTL 70 | // Item value length does not include padding 71 | itemLen := item.GetLength() 72 | // But structure length counts padding 73 | newLen += RoundUpTo8(itemLen) 74 | } 75 | st.Length = newLen 76 | return newLen 77 | } 78 | 79 | func (st *Structure) ResetTyp() { 80 | st.Typ = TypStruct 81 | } 82 | 83 | // Construct a new structure with the specified tag, place the items inside the structure as well. Each item must be a pointer to Item. 84 | func NewStructure(tag Tag, items ...Item) *Structure { 85 | ret := &Structure{TTL: TTL{Tag: tag, Typ: TypStruct}, Items: make([]Item, 0, 8)} 86 | for _, item := range items { 87 | ret.Items = append(ret.Items, item) 88 | } 89 | return ret 90 | } 91 | 92 | // TTLV integer. Length of value is 4. Representation comes with 4 additional bytes of padding. 93 | type Integer struct { 94 | TTL 95 | Value int32 96 | } 97 | 98 | func (i *Integer) GetTTL() TTL { 99 | return i.TTL 100 | } 101 | 102 | func (i *Integer) GetLength() int { 103 | i.Length = 4 104 | return 4 105 | } 106 | func (i *Integer) ResetTyp() { 107 | i.Typ = TypInt 108 | } 109 | 110 | // TTLV long integer. Length of value is 8. 111 | type LongInteger struct { 112 | TTL 113 | Value int64 114 | } 115 | 116 | func (li *LongInteger) GetTTL() TTL { 117 | return li.TTL 118 | } 119 | 120 | func (li *LongInteger) GetLength() int { 121 | li.Length = 8 122 | return 8 123 | } 124 | func (li *LongInteger) ResetTyp() { 125 | li.Typ = TypLong 126 | } 127 | 128 | // TTLV enumeration. Length of value is 4. Representation comes with 4 additional bytes of padding. 129 | type Enumeration struct { 130 | TTL 131 | Value int32 132 | } 133 | 134 | func (enum *Enumeration) GetTTL() TTL { 135 | return enum.TTL 136 | } 137 | 138 | func (enum *Enumeration) GetLength() int { 139 | enum.Length = 4 140 | return 4 141 | } 142 | func (enum *Enumeration) ResetTyp() { 143 | enum.Typ = TypeEnum 144 | } 145 | 146 | // TTLV date time - seconds since Unix epoch represented as LongInteger. Length of value is 8. 147 | type DateTime struct { 148 | TTL 149 | Time time.Time 150 | } 151 | 152 | func (dt *DateTime) GetTTL() TTL { 153 | return dt.TTL 154 | } 155 | 156 | func (dt *DateTime) GetLength() int { 157 | dt.Length = 8 158 | return 8 159 | } 160 | func (dt *DateTime) ResetTyp() { 161 | dt.Typ = TypeDateTime 162 | } 163 | 164 | // TTLV text string. Length of value is actual string length, but representation is padded to align with 8 bytes. 165 | type Text struct { 166 | TTL 167 | Value string 168 | } 169 | 170 | func (text *Text) GetTTL() TTL { 171 | return text.TTL 172 | } 173 | 174 | func (text *Text) GetLength() int { 175 | text.Length = len(text.Value) 176 | return text.Length 177 | } 178 | func (text *Text) ResetTyp() { 179 | text.Typ = TypeText 180 | } 181 | 182 | // TTLV byte array. Length of value is actual array length, but representation is padded to align with 8 bytes. 183 | type Bytes struct { 184 | TTL 185 | Value []byte 186 | } 187 | 188 | func (bytes *Bytes) GetTTL() TTL { 189 | return bytes.TTL 190 | } 191 | 192 | func (bytes *Bytes) GetLength() int { 193 | bytes.Length = len(bytes.Value) 194 | return bytes.Length 195 | } 196 | func (bytes *Bytes) ResetTyp() { 197 | bytes.Typ = TypeBytes 198 | } 199 | -------------------------------------------------------------------------------- /kmip/structure/op_create.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package structure 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/kmip/ttlv" 9 | ) 10 | 11 | // KMIP request message 420078 12 | type SCreateRequest struct { 13 | SRequestHeader SRequestHeader // IBatchCount is assumed to be 1 in serialisation operations 14 | SRequestBatchItem SRequestBatchItem // payload is SRequestPayloadCreate 15 | } 16 | 17 | func (createReq SCreateRequest) SerialiseToTTLV() ttlv.Item { 18 | createReq.SRequestHeader.IBatchCount.Value = 1 19 | ret := ttlv.NewStructure(TagRequestMessage, createReq.SRequestHeader.SerialiseToTTLV(), createReq.SRequestBatchItem.SerialiseToTTLV()) 20 | return ret 21 | } 22 | func (createReq *SCreateRequest) DeserialiseFromTTLV(in ttlv.Item) error { 23 | if err := DecodeStructItem(in, TagRequestMessage, TagRequestHeader, &createReq.SRequestHeader); err != nil { 24 | return err 25 | } 26 | if val := createReq.SRequestHeader.IBatchCount.Value; val != 1 { 27 | return fmt.Errorf("SCreateRequest.DeserialiseFromTTLV: was expecting exactly 1 item, but received %d instead.", val) 28 | } 29 | createReq.SRequestBatchItem = SRequestBatchItem{SRequestPayload: &SRequestPayloadCreate{}} 30 | if err := DecodeStructItem(in, TagRequestMessage, TagBatchItem, &createReq.SRequestBatchItem); err != nil { 31 | return err 32 | } 33 | if createReq.SRequestBatchItem.EOperation.Value != ValOperationCreate { 34 | return errors.New("SCreateRequest.DeserialiseFromTTLV: input is not a create request") 35 | } 36 | return nil 37 | } 38 | 39 | // 420079 40 | type SRequestPayloadCreate struct { 41 | EObjectType ttlv.Enumeration // 420057 42 | STemplateAttribute STemplateAttribute // 420091 43 | } 44 | 45 | func (createPayload SRequestPayloadCreate) SerialiseToTTLV() ttlv.Item { 46 | createPayload.EObjectType.Tag = TagObjectType 47 | return ttlv.NewStructure(TagRequestPayload, &createPayload.EObjectType, createPayload.STemplateAttribute.SerialiseToTTLV()) 48 | } 49 | func (createPayload *SRequestPayloadCreate) DeserialiseFromTTLV(in ttlv.Item) error { 50 | if err := DecodeStructItem(in, TagRequestPayload, TagObjectType, &createPayload.EObjectType); err != nil { 51 | return err 52 | } else if err := DecodeStructItem(in, TagRequestPayload, TagTemplateAttribute, &createPayload.STemplateAttribute); err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | // 42000b of a create request's payload attribute called "Name" 59 | type SCreateRequestNameAttributeValue struct { 60 | TKeyName ttlv.Text // 420055 61 | EKeyType ttlv.Enumeration // 420054 62 | } 63 | 64 | func (nameAttr SCreateRequestNameAttributeValue) SerialiseToTTLV() ttlv.Item { 65 | nameAttr.TKeyName.Tag = TagNameValue 66 | nameAttr.EKeyType.Tag = TagNameType 67 | return ttlv.NewStructure(TagAttributeValue, &nameAttr.TKeyName, &nameAttr.EKeyType) 68 | } 69 | func (nameAttr *SCreateRequestNameAttributeValue) DeserialiseFromTTLV(in ttlv.Item) error { 70 | if err := DecodeStructItem(in, TagAttribute, TagNameValue, &nameAttr.TKeyName); err != nil { 71 | return err 72 | } else if err := DecodeStructItem(in, TagAttribute, TagNameType, &nameAttr.EKeyType); err != nil { 73 | return err 74 | } 75 | return nil 76 | } 77 | 78 | // KMIP response message 42007b 79 | type SCreateResponse struct { 80 | SResponseHeader SResponseHeader // IBatchCount is assumed to be 1 in serialisation operations 81 | SResponseBatchItem SResponseBatchItem 82 | } 83 | 84 | func (createResp SCreateResponse) SerialiseToTTLV() ttlv.Item { 85 | createResp.SResponseHeader.IBatchCount.Value = 1 86 | ret := ttlv.NewStructure(TagResponseMessage, createResp.SResponseHeader.SerialiseToTTLV(), createResp.SResponseBatchItem.SerialiseToTTLV()) 87 | return ret 88 | } 89 | func (createResp *SCreateResponse) DeserialiseFromTTLV(in ttlv.Item) error { 90 | if err := DecodeStructItem(in, TagResponseMessage, TagResponseHeader, &createResp.SResponseHeader); err != nil { 91 | return err 92 | } 93 | if val := createResp.SResponseHeader.IBatchCount.Value; val != 1 { 94 | return fmt.Errorf("SCreateResponse.DeserialiseFromTTLV: was expecting exactly 1 item, but received %d instead.", val) 95 | } 96 | createResp.SResponseBatchItem = SResponseBatchItem{SResponsePayload: &SResponsePayloadCreate{}} 97 | if err := DecodeStructItem(in, TagResponseMessage, TagBatchItem, &createResp.SResponseBatchItem); err != nil { 98 | return err 99 | } 100 | if createResp.SResponseBatchItem.EOperation.Value != ValOperationCreate { 101 | return errors.New("SCreateResponse.DeserialiseFromTTLV: input is not a create response") 102 | } 103 | return nil 104 | } 105 | 106 | // 42007c - response payload from a create response 107 | type SResponsePayloadCreate struct { 108 | EObjectType ttlv.Enumeration // 420057 109 | TUniqueID ttlv.Text // 420094 110 | } 111 | 112 | func (createPayload SResponsePayloadCreate) SerialiseToTTLV() ttlv.Item { 113 | createPayload.EObjectType.Tag = TagObjectType 114 | createPayload.TUniqueID.Tag = TagUniqueID 115 | return ttlv.NewStructure(TagResponsePayload, &createPayload.EObjectType, &createPayload.TUniqueID) 116 | } 117 | func (createPayload *SResponsePayloadCreate) DeserialiseFromTTLV(in ttlv.Item) error { 118 | if err := DecodeStructItem(in, TagResponsePayload, TagObjectType, &createPayload.EObjectType); err != nil { 119 | return err 120 | } else if err := DecodeStructItem(in, TagResponsePayload, TagUniqueID, &createPayload.TUniqueID); err != nil { 121 | return err 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /fs/crypt.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package fs 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/sys" 9 | "os" 10 | "path" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | BIN_CRYPTSETUP = "/sbin/cryptsetup" 17 | LUKS_CIPHER = "aes-xts-plain64:PBKDF2-sha512" 18 | LUKS_HASH = "sha512" 19 | LUKS_KEY_SIZE_S = "512" 20 | LUKS_KEY_SIZE_I = 512 21 | ) 22 | 23 | // Call cryptsetup luksFormat on the block device node. 24 | func CryptFormat(key []byte, blockDev, uuid string) error { 25 | if err := CheckBlockDevice(blockDev); err != nil { 26 | return err 27 | } 28 | _, stdout, stderr, err := sys.Exec(bytes.NewReader(key), nil, nil, 29 | BIN_CRYPTSETUP, "--batch-mode", "--cipher", LUKS_CIPHER, "--hash", LUKS_HASH, "--key-size", LUKS_KEY_SIZE_S, 30 | "luksFormat", "--key-file=-", blockDev, "--uuid="+uuid) 31 | if err != nil { 32 | return fmt.Errorf("CryptFormat: failed to format \"%s\" - %v %s %s", blockDev, err, stdout, stderr) 33 | } 34 | return nil 35 | } 36 | 37 | // Call cryptsetup luksOpen on the block device node. 38 | func CryptOpen(key []byte, blockDev, name string) error { 39 | if err := CheckBlockDevice(blockDev); err != nil { 40 | return err 41 | } 42 | _, err := os.Stat(path.Join("/dev/mapper", name)) 43 | if err == nil { 44 | return fmt.Errorf("CryptOpen: \"%s\" appears to have already been unlocked as \"%s\"", blockDev, name) 45 | } 46 | _, stdout, stderr, err := sys.Exec(bytes.NewReader(key), nil, nil, 47 | BIN_CRYPTSETUP, "--batch-mode", "luksOpen", "--key-file=-", blockDev, name) 48 | if err != nil { 49 | return fmt.Errorf("CryptOpen: failed to open \"%s\" as \"%s\" - %v %s %s", blockDev, name, err, stdout, stderr) 50 | } 51 | return nil 52 | } 53 | 54 | // Call cryptsetup erase on the block device node. 55 | func CryptErase(blockDev string) error { 56 | if err := CheckBlockDevice(blockDev); err != nil { 57 | return err 58 | } 59 | _, stdout, stderr, err := sys.Exec(nil, nil, nil, BIN_CRYPTSETUP, "--batch-mode", "luksErase", blockDev) 60 | if err != nil { 61 | return fmt.Errorf("CryptErase: failed to erase \"%s\" - %v %s %s", blockDev, err, stdout, stderr) 62 | } 63 | /* 64 | luksErase only erases key slots, but there is still some information left on the disk. 65 | Write 1 MBytes of zeros into the beginning of the disk so that not even little bit of LUKS information is left. 66 | */ 67 | blkDev, err := os.OpenFile(blockDev, os.O_WRONLY, 0644) 68 | if err != nil { 69 | return fmt.Errorf("CryptErase: failed to open \"%s\" - %v", blockDev, err) 70 | } 71 | var zeros [1024 * 1024]byte 72 | written, err := blkDev.Write(zeros[:]) 73 | if written == 0 { 74 | return fmt.Errorf("CryptErase: failed to write into \"%s\" - %v", blockDev, err) 75 | } else if err := blkDev.Sync(); err != nil { 76 | return fmt.Errorf("CryptErase: failed to sync \"%s\" - %v", blockDev, err) 77 | } else if err := blkDev.Close(); err != nil { 78 | return fmt.Errorf("CryptErase: failed to close \"%s\" - %v", blockDev, err) 79 | } 80 | return nil 81 | } 82 | 83 | // Call cryptsetup luksClose on the mapped device node. 84 | func CryptClose(name string) error { 85 | _, stdout, stderr, err := sys.Exec(nil, nil, nil, 86 | BIN_CRYPTSETUP, "--batch-mode", "luksClose", name) 87 | if err != nil { 88 | return fmt.Errorf("CryptClose: failed to close \"%s\" - %v %s %s", name, err, stdout, stderr) 89 | } 90 | return nil 91 | } 92 | 93 | // Represent a cryptsetup mapping currently effective on the system. 94 | type CryptMapping struct { 95 | Type string 96 | Cipher string 97 | KeySize int 98 | Device string 99 | Loop string 100 | } 101 | 102 | // Return true only if all fields (except Loop) are assigned. 103 | func (mapping CryptMapping) IsValid() bool { 104 | return mapping.Type != "" && mapping.Cipher != "" && mapping.KeySize > 0 && mapping.Device != "" 105 | } 106 | 107 | // Return cryptsetup status (a device mapper device) parsed from the text. 108 | func ParseCryptStatus(txt string) (mapping CryptMapping) { 109 | // The output is very simple to parse 110 | for _, line := range strings.Split(txt, "\n") { 111 | line = strings.TrimSpace(line) 112 | if typeLine := strings.TrimPrefix(line, "type:"); typeLine != line { 113 | mapping.Type = strings.TrimSpace(typeLine) 114 | } else if cipherLine := strings.TrimPrefix(line, "cipher:"); cipherLine != line { 115 | mapping.Cipher = strings.TrimSpace(cipherLine) 116 | } else if keySizeLine := strings.TrimPrefix(line, "keysize:"); keySizeLine != line { 117 | var err error 118 | mapping.KeySize, err = strconv.Atoi(strings.TrimSpace(strings.TrimRight(keySizeLine, "bits"))) 119 | if err != nil { 120 | panic(fmt.Errorf("CryptStatus: failed to parse key size output on line \"%s\"", line)) 121 | } 122 | } else if deviceLine := strings.TrimPrefix(line, "device:"); deviceLine != line { 123 | mapping.Device = strings.TrimSpace(deviceLine) 124 | } else if loopLine := strings.TrimPrefix(line, "loop:"); loopLine != line { 125 | mapping.Loop = strings.TrimSpace(loopLine) 126 | } 127 | } 128 | return 129 | } 130 | 131 | // Get luks device status. An error will be returned if the mapping status cannot be retrieved. 132 | func CryptStatus(name string) (mapping CryptMapping, err error) { 133 | _, stdout, _, _ := sys.Exec(nil, nil, nil, BIN_CRYPTSETUP, "status", name) 134 | mapping = ParseCryptStatus(stdout) 135 | if !mapping.IsValid() { 136 | err = fmt.Errorf("CryptStatus: failed to retrieve a valid output for \"%s\", gathered information is: %+v", name, mapping) 137 | } 138 | return 139 | } 140 | -------------------------------------------------------------------------------- /fs/fs_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package fs 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestParseBlockDevs(t *testing.T) { 12 | sample := ` 13 | UUID="" NAME="sda" TYPE="disk" FSTYPE="" MOUNTPOINT="" SIZE="42949672960" PKNAME="" 14 | UUID="5719d731-61a1-485e-98c9-49969d66c210" NAME="sda1" TYPE="part" FSTYPE="ext4" MOUNTPOINT="/" SIZE="42943138304" PKNAME="" 15 | UUID="68a72d63-b256-450e-b648-44782057153e" NAME="loop0" TYPE="loop" FSTYPE="crypto_LUKS" MOUNTPOINT="" SIZE="12582912000" PKNAME="loop0" 16 | UUID="7d5ad550-8e81-45a9-895f-90bff713c63c" NAME="dm00" TYPE="crypt" FSTYPE="ext4" MOUNTPOINT="/home/howard" SIZE="12580814848" PKNAME="loop0" 17 | 18 | UUID="" NAME="sr0" TYPE="rom" FSTYPE="" MOUNTPOINT="" SIZE="1073741312" PKNAME="" 19 | UUID="" NAME="vda" TYPE="disk" FSTYPE="" MOUNTPOINT="" SIZE="68719476736" PKNAME="" 20 | 21 | UUID="e3e82520-5123-490c-a01f-1b6226e770c2" NAME="vda1" TYPE="part" FSTYPE="swap" MOUNTPOINT="[SWAP]" SIZE="2153775104" PKNAME="loop0" 22 | UUID="2a2e9ce7-6cd2-48ca-b932-37800eef51a2" NAME="vda2" TYPE="part" FSTYPE="xfs" MOUNTPOINT="/" SIZE="66564653056" PKNAME="loop0" 23 | UUID="" NAME="vdb" TYPE="disk" FSTYPE="" MOUNTPOINT="" SIZE="8589934592" PKNAME="loop0" 24 | 25 | UUID="9edcdeb9-86bd-4602-be5d-7a45a29fefc0" NAME="vdc" TYPE="disk" FSTYPE="crypto_LUKS" MOUNTPOINT="" SIZE="9663676416" PKNAME="loop0" 26 | UUID="80c51aec-15e1-42ea-8520-1d6c707cd8e6" NAME="dm00" TYPE="crypt" FSTYPE="ext4" MOUNTPOINT="/mnt" SIZE="9661579264" PKNAME="loop0" 27 | ` 28 | ret := ParseBlockDevs(sample) 29 | expected := BlockDevices{ 30 | BlockDevice{UUID: "", Path: "/dev/sda", Type: "disk", FileSystem: "", MountPoint: "", SizeByte: 42949672960, PKName: "", Name: "sda"}, 31 | BlockDevice{UUID: "5719d731-61a1-485e-98c9-49969d66c210", Path: "/dev/sda1", Type: "part", FileSystem: "ext4", MountPoint: "/", SizeByte: 42943138304, PKName: "", Name: "sda1"}, 32 | BlockDevice{UUID: "68a72d63-b256-450e-b648-44782057153e", Path: "/dev/loop0", Type: "loop", FileSystem: "crypto_LUKS", MountPoint: "", SizeByte: 12582912000, PKName: "loop0", Name: "loop0"}, 33 | BlockDevice{UUID: "7d5ad550-8e81-45a9-895f-90bff713c63c", Path: "/dev/mapper/dm00", Type: "crypt", FileSystem: "ext4", MountPoint: "/home/howard", SizeByte: 12580814848, PKName: "loop0", Name: "dm00"}, 34 | 35 | BlockDevice{UUID: "", Path: "/dev/sr0", Type: "rom", FileSystem: "", MountPoint: "", SizeByte: 1073741312, PKName: "", Name: "sr0"}, 36 | BlockDevice{UUID: "", Path: "/dev/vda", Type: "disk", FileSystem: "", MountPoint: "", SizeByte: 68719476736, PKName: "", Name: "vda"}, 37 | 38 | BlockDevice{UUID: "e3e82520-5123-490c-a01f-1b6226e770c2", Path: "/dev/vda1", Type: "part", FileSystem: "swap", MountPoint: "[SWAP]", SizeByte: 2153775104, PKName: "loop0", Name: "vda1"}, 39 | BlockDevice{UUID: "2a2e9ce7-6cd2-48ca-b932-37800eef51a2", Path: "/dev/vda2", Type: "part", FileSystem: "xfs", MountPoint: "/", SizeByte: 66564653056, PKName: "loop0", Name: "vda2"}, 40 | BlockDevice{UUID: "", Path: "/dev/vdb", Type: "disk", FileSystem: "", MountPoint: "", SizeByte: 8589934592, PKName: "loop0", Name: "vdb"}, 41 | 42 | BlockDevice{UUID: "9edcdeb9-86bd-4602-be5d-7a45a29fefc0", Path: "/dev/vdc", Type: "disk", FileSystem: "crypto_LUKS", MountPoint: "", SizeByte: 9663676416, PKName: "loop0", Name: "vdc"}, 43 | BlockDevice{UUID: "80c51aec-15e1-42ea-8520-1d6c707cd8e6", Path: "/dev/mapper/dm00", Type: "crypt", FileSystem: "ext4", MountPoint: "/mnt", SizeByte: 9661579264, PKName: "loop0", Name: "dm00"}, 44 | } 45 | if !reflect.DeepEqual(ret, expected) { 46 | for i, _ := range ret { 47 | if !reflect.DeepEqual(ret[i], expected[i]) { 48 | fmt.Printf("%+v\n", ret[i]) 49 | fmt.Printf("%+v\n", expected[i]) 50 | } 51 | } 52 | t.Fatalf("mismatch\n%+v\n%+v\n", ret, expected) 53 | } 54 | 55 | if dev, err := ret.GetByCriteria("", "/dev/sda1", "", "", "", "", ""); dev != expected[1] { 56 | t.Fatal(dev, err) 57 | } 58 | if dev, err := ret.GetByCriteria("", "", "loop", "", "", "", ""); dev != expected[2] { 59 | t.Fatal(dev, err) 60 | } 61 | if dev, err := ret.GetByCriteria("", "/dev/mapper/dm00", "", "ext4", "", "", ""); dev != expected[3] { 62 | t.Fatal(dev, err) 63 | } 64 | if dev, err := ret.GetByCriteria("", "", "", "", "[SWAP]", "", ""); dev != expected[6] { 65 | t.Fatal(dev, err) 66 | } 67 | if dev, err := ret.GetByCriteria("80c51aec-15e1-42ea-8520-1d6c707cd8e6", "/dev/mapper/dm00", "crypt", "ext4", "/mnt", "loop0", "dm00"); dev != expected[10] { 68 | t.Fatal(dev, err) 69 | } 70 | if expected[8].IsLUKSEncrypted() { 71 | t.Fatal("encrypted - wrong") 72 | } 73 | if !expected[9].IsLUKSEncrypted() { 74 | t.Fatal("not encrypted - wrong") 75 | } 76 | } 77 | 78 | func TestGetBlockDevices(t *testing.T) { 79 | devs := GetBlockDevices() 80 | if len(devs) == 0 { 81 | t.Fatal("did not get any block devs") 82 | } 83 | if err := CheckBlockDevice(devs[0].Path); err != nil { 84 | t.Fatal(err) 85 | } else if err := CheckBlockDevice("/dev/does not exist"); err == nil { 86 | t.Fatal("did not error") 87 | } 88 | if blk0, found := GetBlockDevice(devs[0].Path); !found || !reflect.DeepEqual(blk0, devs[0]) { 89 | t.Fatal(blk0, found) 90 | } 91 | if blk1, found := GetBlockDevice("does not exist"); found { 92 | t.Fatal(blk1, found) 93 | } 94 | } 95 | 96 | func TestFormat(t *testing.T) { 97 | if err := Format("/dev/does not exist", "ext4"); err == nil { 98 | t.Fatal("did not error") 99 | } 100 | } 101 | 102 | func TestFreeSpace(t *testing.T) { 103 | if size, err := FreeSpace("/etc/os-release"); err != nil || size < 3 { 104 | t.Fatal(err, size) 105 | } 106 | } 107 | 108 | func TestGetSystemdMountNameForDir(t *testing.T) { 109 | in := `/root/a-b c!@` 110 | out := `root-a\x2db\x20c\x21\x40.mount` 111 | if ret := GetSystemdMountNameForDir(in); ret != out { 112 | t.Fatal(ret) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /fs/mnt.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package fs 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | ) 16 | 17 | var mountOptionSeparator = regexp.MustCompile("[[:space:]]*,[[:space:]]*") // split mount options by commas 18 | var consecutiveSpaces = regexp.MustCompile("[[:space:]]+") // split fields by consecutive spaces 19 | 20 | // Represent a mount point entry in /etc/mtab. 21 | type MountPoint struct { 22 | DeviceNode string 23 | MountPoint string 24 | FileSystem string 25 | Options []string 26 | Dump int 27 | Fsck int 28 | } 29 | 30 | // Return true only if two mount points are identical in all attributes. 31 | func (mount1 MountPoint) Equals(mount2 MountPoint) bool { 32 | return reflect.DeepEqual(mount1, mount2) 33 | } 34 | 35 | // Return the total size of the file system in Bytes. 36 | func (mount MountPoint) GetFileSystemSizeByte() (int64, error) { 37 | fs := syscall.Statfs_t{} 38 | err := syscall.Statfs(mount.MountPoint, &fs) 39 | if err != nil { 40 | return 0, err 41 | } 42 | return fs.Bsize * int64(fs.Blocks), nil 43 | } 44 | 45 | // Remove btrfs subvolume among mount options. The MountPoint is modified in-place. 46 | func (mount *MountPoint) DiscardBtrfsSubvolume() { 47 | newOptions := make([]string, 0, len(mount.Options)) 48 | for _, opt := range mount.Options { 49 | // Discard "subvolid=" and "subvol=" 50 | if !strings.HasPrefix(opt, "subvol") { 51 | newOptions = append(newOptions, opt) 52 | } 53 | } 54 | mount.Options = newOptions 55 | } 56 | 57 | // A list of mount points. 58 | type MountPoints []MountPoint 59 | 60 | // Find the first mount point that satisfies the given criteria. If a criteria is empty, it is ignored. 61 | func (mounts MountPoints) GetByCriteria(deviceNode, mountPoint, fileSystem string) (MountPoint, bool) { 62 | for _, mount := range mounts { 63 | if (deviceNode == "" || mount.DeviceNode == deviceNode) && 64 | (mountPoint == "" || mount.MountPoint == mountPoint) && 65 | (fileSystem == "" || mount.FileSystem == fileSystem) { 66 | return mount, true 67 | } 68 | } 69 | return MountPoint{}, false 70 | } 71 | 72 | // Find the all mount points that satisfy the given criteria. If a criteria is empty, it is ignored. 73 | func (mounts MountPoints) GetManyByCriteria(deviceNode, mountPoint, fileSystem string) (ret MountPoints) { 74 | ret = make([]MountPoint, 0, 0) 75 | for _, mount := range mounts { 76 | if (deviceNode == "" || mount.DeviceNode == deviceNode) && 77 | (mountPoint == "" || mount.MountPoint == mountPoint) && 78 | (fileSystem == "" || mount.FileSystem == fileSystem) { 79 | ret = append(ret, mount) 80 | } 81 | } 82 | return 83 | } 84 | 85 | // Find mount point for an arbitrary directory or file specified by an absolute path. 86 | func (mounts MountPoints) GetMountPointOfPath(fileOrDirPath string) (MountPoint, bool) { 87 | if !filepath.IsAbs(fileOrDirPath) { 88 | return MountPoint{}, false 89 | } 90 | inputSegments := strings.Split(filepath.Clean(fileOrDirPath), fmt.Sprintf("%c", os.PathSeparator)) 91 | // Special case for a single path segment - remove the tail empty element 92 | if inputSegments[len(inputSegments)-1] == "" { 93 | inputSegments = inputSegments[:len(inputSegments)-1] 94 | } 95 | var bestMatch MountPoint 96 | var bestMatchLen int 97 | for _, mp := range mounts { 98 | mpSegments := strings.Split(filepath.Clean(mp.MountPoint), fmt.Sprintf("%c", os.PathSeparator)) 99 | // Special case for a single path segment - remove the tail empty element 100 | if mpSegments[len(mpSegments)-1] == "" { 101 | mpSegments = mpSegments[:len(mpSegments)-1] 102 | } 103 | if len(mpSegments) > len(inputSegments) { 104 | continue 105 | } 106 | if reflect.DeepEqual(inputSegments[0:len(mpSegments)], mpSegments) && len(mpSegments) > bestMatchLen { 107 | if len(mpSegments) == bestMatchLen && bestMatch.MountPoint != "" { 108 | // Return nothing in the unlikely case of two mount points owning the same directory/file 109 | return MountPoint{}, false 110 | } 111 | bestMatch = mp 112 | bestMatchLen = len(mpSegments) 113 | } 114 | } 115 | return bestMatch, bestMatch.MountPoint != "" 116 | } 117 | 118 | // Return all mount points defined in the input text except rootfs. Panic on malformed entry. 119 | func ParseMountPoints(txt string) (mounts MountPoints) { 120 | mounts = make([]MountPoint, 0, 8) 121 | for _, line := range strings.Split(txt, "\n") { 122 | fields := consecutiveSpaces.Split(strings.TrimSpace(line), -1) 123 | if len(fields) == 0 || len(fields[0]) == 0 || fields[0][0] == '#' { 124 | continue // skip comments and empty lines 125 | } 126 | if len(fields) != 6 { 127 | panic(fmt.Sprintf("ParseMountPoints: incorrect number of fields in '%s'", line)) 128 | } 129 | mountPoint := MountPoint{ 130 | DeviceNode: fields[0], 131 | MountPoint: fields[1], 132 | FileSystem: fields[2], 133 | } 134 | if mountPoint.FileSystem == "rootfs" { 135 | // rootfs most likely originates from btrfs and masks the real mount point of / 136 | continue 137 | } 138 | // Split mount options 139 | mountPoint.Options = mountOptionSeparator.Split(fields[3], -1) 140 | var err error 141 | if mountPoint.Dump, err = strconv.Atoi(fields[4]); err != nil { 142 | panic(fmt.Sprintf("ParseMountPoints: not an integer in '%s'", line)) 143 | } 144 | if mountPoint.Fsck, err = strconv.Atoi(fields[4]); err != nil { 145 | panic(fmt.Sprintf("ParseMountPoints: not an integer in '%s'", line)) 146 | } 147 | mounts = append(mounts, mountPoint) 148 | } 149 | return 150 | } 151 | 152 | // Return all mount points that appear in /etc/mtab. Panic on error. 153 | func ParseMtab() MountPoints { 154 | mounts, err := ioutil.ReadFile("/etc/mtab") 155 | if err != nil { 156 | panic(fmt.Errorf("ParseMtabMounts: failed to open /etc/mtab - %v", err)) 157 | } 158 | return ParseMountPoints(string(mounts)) 159 | } 160 | -------------------------------------------------------------------------------- /fs/file_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package fs 4 | 5 | import ( 6 | "bytes" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "syscall" 11 | "testing" 12 | ) 13 | 14 | const ( 15 | TEST_DIR_MODE = 0741 // A very odd mode good for test case 16 | TEST_FILE_MODE = 0531 // A very odd mode good for test case 17 | TEST_FILE_CONTENT = "good" 18 | ) 19 | 20 | // Create a new file and its parent directories (if missing) and write some dummy content into it. 21 | func MakeTestFile(paths ...string) { 22 | oldUmask := syscall.Umask(0) 23 | if err := os.MkdirAll(path.Join(paths[0:len(paths)-1]...), TEST_DIR_MODE); err != nil { 24 | panic(err) 25 | } else if err := ioutil.WriteFile(path.Join(paths...), []byte(TEST_FILE_CONTENT), TEST_FILE_MODE); err != nil { 26 | panic(err) 27 | } 28 | syscall.Umask(oldUmask) 29 | } 30 | 31 | /* 32 | Make sure that all intermediate directories have appropriate permission, then make sure that the file itself has 33 | appropriate permission and previously test content. 34 | */ 35 | func FilePermContentTest(t *testing.T, paths ...string) { 36 | for i := 2; i < len(paths)-1; i++ { 37 | st, err := os.Stat(path.Join(paths[0:i]...)) 38 | if err != nil { 39 | panic(err) 40 | } 41 | if st.Mode().Perm() != TEST_DIR_MODE { 42 | t.Fatalf("%s incorrect mode - %v vs %v", path.Join(paths[0:i]...), st.Mode().Perm(), TEST_DIR_MODE) 43 | } 44 | } 45 | content, err := ioutil.ReadFile(path.Join(paths...)) 46 | if err != nil || string(content) != TEST_FILE_CONTENT { 47 | t.Fatal(err, string(content)) 48 | } 49 | } 50 | 51 | func TestMirrorFiles(t *testing.T) { 52 | // Mirror the wrong paths 53 | if err := MirrorFiles("/", "/tmp", nil); err == nil { 54 | t.Fatal("did not error") 55 | } else if err := MirrorFiles("/var", "/var/lib", nil); err == nil { 56 | t.Fatal("did not error") 57 | } else if err := MirrorFiles("//var/lib", "//./var//", nil); err == nil { 58 | t.Fatal("did not error") 59 | } else if err := MirrorFiles("", "/abc", nil); err == nil { 60 | t.Fatal("did not error") 61 | } else if err := MirrorFiles("/abc", "", nil); err == nil { 62 | t.Fatal("did not error") 63 | } else if err := MirrorFiles("/./", "//.", nil); err == nil { 64 | t.Fatal("did not error") 65 | } else if err := MirrorFiles("a/b", "c/d", nil); err == nil { 66 | t.Fatal("did not error") 67 | } 68 | 69 | // Mirror good files 70 | tmpDir, err := ioutil.TempDir("", "cryptctltest") 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | defer os.RemoveAll(tmpDir) 75 | MakeTestFile(tmpDir, "Source", "Dir A", "Subdir 1", "File 1") 76 | MakeTestFile(tmpDir, "Source", "Dir A", "Subdir 1", "File 2") 77 | MakeTestFile(tmpDir, "Source", "Dir A", "Subdir 2", "File 1") 78 | MakeTestFile(tmpDir, "Source", "Dir A", "Subdir 2", "File 2") 79 | MakeTestFile(tmpDir, "Source", "Dir B", "Subdir 1", "File 1") 80 | MakeTestFile(tmpDir, "Source", "Dir B", "Subdir 1", "File 2") 81 | MakeTestFile(tmpDir, "Source", "Dir B", "Subdir 2", "File 1") 82 | MakeTestFile(tmpDir, "Source", "Dir B", "Subdir 2", "File 2") 83 | 84 | testMirror := func() { 85 | var out bytes.Buffer 86 | if err := MirrorFiles(path.Join(tmpDir, "Source"), path.Join(tmpDir, "Destination"), &out); err != nil { 87 | t.Fatal(err) 88 | } 89 | if len(out.String()) < 100 { 90 | t.Fatal("output is too short", out.String()) 91 | } 92 | if usage, err := FileSpaceUsage(tmpDir); err != nil || usage != int64(2*8*len(TEST_FILE_CONTENT)) { 93 | t.Fatal(err, usage) 94 | } 95 | } 96 | testFileContent := func() { 97 | FilePermContentTest(t, tmpDir, "Destination", "Dir A", "Subdir 1", "File 1") 98 | FilePermContentTest(t, tmpDir, "Destination", "Dir A", "Subdir 1", "File 2") 99 | FilePermContentTest(t, tmpDir, "Destination", "Dir A", "Subdir 2", "File 1") 100 | FilePermContentTest(t, tmpDir, "Destination", "Dir A", "Subdir 2", "File 2") 101 | FilePermContentTest(t, tmpDir, "Destination", "Dir B", "Subdir 1", "File 1") 102 | FilePermContentTest(t, tmpDir, "Destination", "Dir B", "Subdir 1", "File 2") 103 | FilePermContentTest(t, tmpDir, "Destination", "Dir B", "Subdir 2", "File 1") 104 | FilePermContentTest(t, tmpDir, "Destination", "Dir B", "Subdir 2", "File 2") 105 | } 106 | // No matter now many times the sequence is run, only one mirror should exist in the destination 107 | for i := 0; i < 10; i++ { 108 | testMirror() 109 | testFileContent() 110 | } 111 | } 112 | 113 | func TestSecureErase(t *testing.T) { 114 | filePath := "/tmp/cryptctl-erase-test" 115 | fileSize := ERASE_BLOCK_SIZE * 7 / 2 116 | defer os.Remove(filePath) 117 | // Create a sample file 3.5x the size of erasure block 118 | fh, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | byte1 := []byte{1} 123 | for i := 0; i < fileSize; i++ { 124 | fh.Write(byte1) 125 | } 126 | if err := fh.Close(); err != nil { 127 | t.Fatal(err) 128 | } 129 | // No more than 10 consecutive byte 1s shall appear after erasure 130 | if err := SecureErase(filePath, false); err != nil { 131 | t.Fatal(err) 132 | } 133 | fh, err = os.OpenFile(filePath, os.O_RDONLY, 0600) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | for i := 0; i < fileSize; i += 10 { 138 | buf := make([]byte, 10) 139 | if _, err := fh.Read(buf); err != nil { 140 | t.Fatal(err) 141 | } 142 | byte1 := true 143 | for _, b := range buf { 144 | if b != 1 { 145 | byte1 = false 146 | } 147 | } 148 | if byte1 { 149 | t.Fatal("too many byte 1s", i) 150 | } 151 | } 152 | // Erase and delete 153 | 154 | } 155 | 156 | func TestIsFile(t *testing.T) { 157 | if FileContains("/", "haha") == nil { 158 | t.Fatal("did not error") 159 | } 160 | if err := FileContains("/etc/os-release", "certainly does not exist"); err == nil { 161 | t.Fatal("did not error") 162 | } 163 | if err := FileContains("/etc/os-release", "NAME"); err != nil { 164 | t.Fatal(err) 165 | } 166 | } 167 | 168 | func TestIsDir(t *testing.T) { 169 | if err := IsDir("/"); err != nil { 170 | t.Fatal(err) 171 | } 172 | if IsDir("/etc/os-release") == nil { 173 | t.Fatal("did not error") 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /ospackage/etc/sysconfig/cryptctl-server: -------------------------------------------------------------------------------- 1 | ## Path: System/Disk Encryption/Encryption Key Server 2 | ## Description: Global settings for disk encryption (cryptctl) server daemon 3 | ## ServiceRestart: cryptctl-server 4 | 5 | ## Type: string 6 | ## Default: "" 7 | # 8 | # Salted hash of password-based authentication of incoming RPC calls. The parameter is constructed automatically 9 | # by the initial setup routine of cryptctl server, hence avoid editing this parameter manually. 10 | AUTH_PASSWORD_HASH="" 11 | 12 | ## Type: string 13 | ## Default: "" 14 | # 15 | # Salt of password-based authentication of incoming RPC calls. The parameter is constructed automatically by the 16 | # initial setup routine of cryptctl server, hence avoid editing this parameter manually. 17 | AUTH_PASSWORD_SALT="" 18 | 19 | ## Type: string 20 | ## Default: "" 21 | # 22 | # (Optional) path to PEM-encoded custom certificate authority that issued the TLS certificate for the key server. 23 | # Leave empty if the TLS certificate was issued by a well-known certificate authority. 24 | TLS_CA_PEM="" 25 | 26 | ## Type: string 27 | ## Default: "" 28 | # 29 | # Location of PEM-encoded TLS certificate file, this is mandatory. 30 | TLS_CERT_PEM="" 31 | 32 | ## Type: string 33 | ## Default: "" 34 | # 35 | # Location of PEM-encoded TLS certificate key file that corresponds to the certificate, this is mandatory. 36 | TLS_CERT_KEY_PEM="" 37 | 38 | ## Type: string 39 | ## Default: "no" 40 | # 41 | # Whether the server will validate client's certificate before accepting its request. 42 | TLS_VALIDATE_CLIENT="no" 43 | 44 | ## Type: string 45 | ## Default: "0.0.0.0" 46 | # 47 | # Address of the network interface to listen on for incoming key requests. 48 | LISTEN_ADDRESS="0.0.0.0" 49 | 50 | ## Type: integer 51 | ## Default: 3737 52 | # 53 | # Port to listen on for incoming key requests. 54 | LISTEN_PORT=3737 55 | 56 | ## Type: string 57 | ## Default: "/var/lib/cryptctl/keydb" 58 | # 59 | # Storage location for disk encryption keys and records. 60 | # Existing keys and records will not be automatically moved to new location if you modify this parameter. 61 | KEY_DB_DIR="/var/lib/cryptctl/keydb" 62 | 63 | ## Type: string 64 | ## Default: "" 65 | # 66 | # Notification emails' recipient addresses, separated by spaces. 67 | # If all Email parameters are filled in, notifications will be sent upon key creation/retrieval events. 68 | EMAIL_RECIPIENTS="" 69 | 70 | ## Type: string 71 | ## Default: "" 72 | # 73 | # Notification emails' From address. 74 | # If all Email parameters are filled in, notifications will be sent upon key creation/retrieval events. 75 | EMAIL_FROM_ADDRESS="" 76 | 77 | ## Type: string 78 | ## Default: "" 79 | # 80 | # Mail agent address and port number in the format of "address:port". The address must be fully qualified domain name. 81 | # If this parameter is set, mail notifications will be sent upon key creation/retrieval events. 82 | EMAIL_AGENT_AND_PORT="" 83 | 84 | ## Type: string 85 | ## Default: "" 86 | # 87 | # Mail agent plain authentication username (optional). 88 | EMAIL_AGENT_USERNAME="" 89 | 90 | ## Type: string 91 | ## Default: "" 92 | # 93 | # Mail agent plain authentication password (optional). 94 | EMAIL_AGENT_PASSWORD="" 95 | 96 | ## Type: string 97 | ## Default: "A new file system has been encrypted" 98 | # 99 | # Subject shown in notification emails sent by key creation events. 100 | EMAIL_KEY_CREATION_SUBJECT="A new file system has been encrypted" 101 | 102 | ## Type: string 103 | ## Default: "The key server now has encryption key for the following file system:" 104 | # 105 | # A greeting message shown in notification emails sent by key creation events. 106 | EMAIL_KEY_CREATION_GREETING="The key server now has encryption key for the following file system:" 107 | 108 | ## Type: string 109 | ## Default: "An encrypted file system has been accessed" 110 | # 111 | # Subject shown in notification emails sent by key retrieval events. 112 | EMAIL_KEY_RETRIEVAL_SUBJECT="An encrypted file system has been accessed" 113 | 114 | ## Type: string 115 | ## Default: "The key server has given out the following encryption key:" 116 | # 117 | # A greeting message shown in notification emails sent by key retrieval events. 118 | EMAIL_KEY_RETRIEVAL_GREETING="The key server has given out the following encryption key:" 119 | 120 | ## Type: string 121 | ## Default: "" 122 | # 123 | # If key server should act as KMIP proxy, this is the KMIP hostname:port list, separated by space. 124 | # (e.g. host1:port1 host2:port2 ...) 125 | KMIP_SERVER_ADDRESSES="" 126 | 127 | ## Type: integer 128 | ## Default: "" 129 | # 130 | # If key server should act as KMIP proxy, this is the KMIP service port. 131 | KMIP_SERVER_PORT="" 132 | 133 | ## Type: string 134 | ## Default: "" 135 | # 136 | # If key server should act as KMIP proxy, this is the KMIP access user name. 137 | KMIP_SERVER_USER="" 138 | 139 | ## Type: string 140 | ## Default: "" 141 | # 142 | # If key server should act as KMIP proxy, this is the KMIP access password. 143 | KMIP_SERVER_PASS="" 144 | 145 | ## Type: string 146 | ## Default: "" 147 | # 148 | # If key server should act as KMIP proxy, this is the KMIP server CA certificate. 149 | KMIP_CA_PEM="" 150 | 151 | ## Type: string 152 | ## Default: "" 153 | # 154 | # If key server should act as KMIP proxy, this is the KMIP client certificate. 155 | KMIP_TLS_CERT_PEM="" 156 | 157 | ## Type: string 158 | ## Default: "" 159 | # 160 | # If key server should act as KMIP proxy, this is the KMIP client certificate key. 161 | KMIP_TLS_CERT_KEY_PEM="" 162 | 163 | ## Type: boolean 164 | ## Default: "yes" 165 | # 166 | # For security reasons, you are strongly recommended to leave the setting at its default "yes". 167 | # If set to "no", cryptctl server will reduce its security measures by not verifying KMIP server's TLS certificate. 168 | # Remember to restart cryptctl-server.service after changing any value of this file. 169 | KMIP_TLS_DO_VERIFY="yes" 170 | 171 | ## Type: boolean 172 | ## Default: "no" 173 | # 174 | # For security reason it is not recommended to allow hashed password authentication. 175 | # For compatibility reasen this can be set yes until all clients are updated 176 | ALLOW_HASH_AUTH="no" 177 | -------------------------------------------------------------------------------- /keyserv/kmip_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keyserv 4 | 5 | import ( 6 | "encoding/hex" 7 | "github.com/SUSE/cryptctl/keydb" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "reflect" 12 | "strconv" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestKMIP(t *testing.T) { 18 | keydbDir, err := ioutil.TempDir("", "cryptctl-kmip-test") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer os.RemoveAll(keydbDir) 23 | db, err := keydb.OpenDB(keydbDir) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | var server *KMIPServer 29 | var serverHasShutdown bool 30 | server, err = NewKMIPServer(db, path.Join(PkgInGopath, "keyserv", "rpc_test.crt"), path.Join(PkgInGopath, "keyserv", "rpc_test.key")) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | if server.Listen(); err != nil { 35 | t.Fatal(err) 36 | } 37 | go func() { 38 | server.HandleConnections() 39 | serverHasShutdown = true 40 | }() 41 | caCert, err := ioutil.ReadFile(path.Join(PkgInGopath, "keyserv", "rpc_test.crt")) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | // Expect server to start in a second 46 | time.Sleep(1 * time.Second) 47 | client, err := NewKMIPClient([]string{ 48 | "bad-server:1234", 49 | "localhost:" + strconv.Itoa(server.GetPort()), 50 | }, "username-does-not-matter", string(server.PasswordChallenge), caCert, 51 | path.Join(PkgInGopath, "keyserv", "rpc_test.crt"), path.Join(PkgInGopath, "keyserv", "rpc_test.key")) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | // Create two keys 56 | if id, err := client.CreateKey("test key 1"); err != nil || id != "1" { 57 | t.Fatal(err, id) 58 | } 59 | if id, err := client.CreateKey("test key 2"); err != nil || id != "2" { 60 | t.Fatal(err, id) 61 | } 62 | // Retrieve both keys and non-existent key 63 | received1, err := client.GetKey("1") 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | received2, err := client.GetKey("2") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | if _, err := client.GetKey("does not exist"); err == nil { 72 | t.Fatal("did not error") 73 | } 74 | if reflect.DeepEqual(received1, received2) || len(received1) == 0 { 75 | t.Fatal(hex.Dump(received1), hex.Dump(received2)) 76 | } 77 | // Destroy and retrieve again 78 | if err := client.DestroyKey("1"); err != nil { 79 | t.Fatal(err) 80 | } 81 | if err := client.DestroyKey("does not exist"); err == nil { 82 | t.Fatal("did not error") 83 | } 84 | // Expect server to shut down within a second 85 | server.Shutdown() 86 | time.Sleep(1 * time.Second) 87 | if !serverHasShutdown { 88 | t.Fatal("did not shutdown") 89 | } 90 | // Calling shutdown multiple times should not cause panic 91 | server.Shutdown() 92 | server.Shutdown() 93 | } 94 | 95 | func TestKMIPAgainstPyKMIP(t *testing.T) { 96 | /* 97 | A PyKMIP server can be started using the python code below: 98 | import time 99 | from kmip.services.server import * 100 | 101 | server = KmipServer( 102 | hostname='0.0.0.0', 103 | port=5696, 104 | certificate_path='/etc/pykmip/server.crt', 105 | key_path='/etc/pykmip/server.key', 106 | ca_path='/etc/pykmip/ca.crt', 107 | auth_suite='Basic', 108 | config_path=None, 109 | log_path='/etc/pykmip/server.log', 110 | policy_path='/etc/pykmip/policy.json' 111 | ) 112 | 113 | print("server about to start") 114 | server.start() 115 | print("server started") 116 | server.serve() 117 | print("connection served") 118 | time.sleep(100) 119 | */ 120 | t.Skip("Start PyKMIP server manually and remove this skip statement to run this test case") 121 | client, err := NewKMIPClient([]string{"127.0.0.1:5696"}, "testuser", "testpass", nil, "/etc/pykmip/kmipclient.com.crt", "/etc/pykmip/kmipclient.com.key") 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | client.TLSConfig.InsecureSkipVerify = true 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | // Create two keys 130 | var id1, id2 string 131 | if id1, err = client.CreateKey("test key 1"); err != nil || id1 == "" { 132 | t.Fatal(err, id1) 133 | } 134 | if id2, err = client.CreateKey("test key 2"); err != nil || id2 == "" { 135 | t.Fatal(err, id2) 136 | } 137 | // Retrieve both keys and non-existent key 138 | received1, err := client.GetKey(id1) 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | received2, err := client.GetKey(id2) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | if _, err := client.GetKey("does not exist"); err == nil { 147 | t.Fatal("did not error") 148 | } 149 | if reflect.DeepEqual(received1, received2) || len(received1) == 0 { 150 | t.Fatal(hex.Dump(received1), hex.Dump(received2)) 151 | } 152 | if err := client.DestroyKey(id1); err != nil { 153 | t.Fatal(err) 154 | } 155 | if err := client.DestroyKey(id2); err != nil { 156 | t.Fatal(err) 157 | } 158 | if err := client.DestroyKey("does not exist"); err == nil { 159 | t.Fatal("did not error") 160 | } 161 | } 162 | 163 | func TestKMIPAgainstHpeEskm(t *testing.T) { 164 | t.Skip("Acquire HPE ESKM credentials and remove this skip statement to run this test case") 165 | client, err := NewKMIPClient([]string{"SERVER:5696"}, "USERNAME", "PASSWORD", nil, "PATH_TO_CRT", "PATH_TO_KEY") 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | client.TLSConfig.InsecureSkipVerify = true 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | // Create two keys 174 | var id1, id2 string 175 | if id1, err = client.CreateKey("test key 1"); err != nil || id1 == "" { 176 | t.Fatal(err, id1) 177 | } 178 | if id2, err = client.CreateKey("test key 2"); err != nil || id2 == "" { 179 | t.Fatal(err, id2) 180 | } 181 | // Retrieve both keys and non-existent key 182 | received1, err := client.GetKey(id1) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | received2, err := client.GetKey(id2) 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | if _, err := client.GetKey("does not exist"); err == nil { 191 | t.Fatal("did not error") 192 | } 193 | if reflect.DeepEqual(received1, received2) || len(received1) == 0 { 194 | t.Fatal(hex.Dump(received1), hex.Dump(received2)) 195 | } 196 | if err := client.DestroyKey(id1); err != nil { 197 | t.Fatal(err) 198 | } 199 | if err := client.DestroyKey(id2); err != nil { 200 | t.Fatal(err) 201 | } 202 | if err := client.DestroyKey("does not exist"); err == nil { 203 | t.Fatal("did not error") 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /kmip/structure/op_get.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package structure 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/kmip/ttlv" 9 | ) 10 | 11 | // KMIP request message 420078 12 | type SGetRequest struct { 13 | SRequestHeader SRequestHeader // IBatchCount is assumed to be 1 in serialisation operations 14 | SRequestBatchItem SRequestBatchItem // payload is SRequestPayloadGet 15 | } 16 | 17 | func (getReq *SGetRequest) SerialiseToTTLV() ttlv.Item { 18 | getReq.SRequestHeader.IBatchCount.Value = 1 19 | ret := ttlv.NewStructure(TagRequestMessage, getReq.SRequestHeader.SerialiseToTTLV(), getReq.SRequestBatchItem.SerialiseToTTLV()) 20 | return ret 21 | } 22 | func (getReq *SGetRequest) DeserialiseFromTTLV(in ttlv.Item) error { 23 | if err := DecodeStructItem(in, TagRequestMessage, TagRequestHeader, &getReq.SRequestHeader); err != nil { 24 | return err 25 | } 26 | if val := getReq.SRequestHeader.IBatchCount.Value; val != 1 { 27 | return fmt.Errorf("SGetRequest.DeserialiseFromTTLV: was expecting exactly 1 item, but received %d instead.", val) 28 | } 29 | getReq.SRequestBatchItem = SRequestBatchItem{SRequestPayload: &SRequestPayloadGet{}} 30 | if err := DecodeStructItem(in, TagRequestMessage, TagBatchItem, &getReq.SRequestBatchItem); err != nil { 31 | return err 32 | } 33 | if getReq.SRequestBatchItem.EOperation.Value != ValOperationGet { 34 | return errors.New("SGetRequest.DeserialiseFromTTLV: input is not a get request") 35 | } 36 | return nil 37 | } 38 | 39 | // 420079 - request payload from a get request 40 | type SRequestPayloadGet struct { 41 | TUniqueID ttlv.Text // 420094 42 | } 43 | 44 | func (getPayload *SRequestPayloadGet) SerialiseToTTLV() ttlv.Item { 45 | getPayload.TUniqueID.Tag = TagUniqueID 46 | return ttlv.NewStructure(TagRequestPayload, &getPayload.TUniqueID) 47 | } 48 | func (getPayload *SRequestPayloadGet) DeserialiseFromTTLV(in ttlv.Item) error { 49 | if err := DecodeStructItem(in, TagRequestPayload, TagUniqueID, &getPayload.TUniqueID); err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | 55 | // KMIP response message 42007b 56 | type SGetResponse struct { 57 | SResponseHeader SResponseHeader // IBatchCount is assumed to be 1 in serialisation operations 58 | SResponseBatchItem SResponseBatchItem // payload is SResponsePayloadGet 59 | } 60 | 61 | func (getResp *SGetResponse) SerialiseToTTLV() ttlv.Item { 62 | getResp.SResponseHeader.IBatchCount.Value = 1 63 | ret := ttlv.NewStructure(TagResponseMessage, getResp.SResponseHeader.SerialiseToTTLV(), getResp.SResponseBatchItem.SerialiseToTTLV()) 64 | return ret 65 | } 66 | func (getResp *SGetResponse) DeserialiseFromTTLV(in ttlv.Item) error { 67 | if err := DecodeStructItem(in, TagResponseMessage, TagResponseHeader, &getResp.SResponseHeader); err != nil { 68 | return err 69 | } 70 | if val := getResp.SResponseHeader.IBatchCount.Value; val != 1 { 71 | return fmt.Errorf("SGetResponse.DeserialiseFromTTLV: was expecting exactly 1 item, but received %d instead.", val) 72 | } 73 | getResp.SResponseBatchItem = SResponseBatchItem{SResponsePayload: &SResponsePayloadGet{}} 74 | if err := DecodeStructItem(in, TagResponseMessage, TagBatchItem, &getResp.SResponseBatchItem); err != nil { 75 | return err 76 | } 77 | if getResp.SResponseBatchItem.EOperation.Value != ValOperationGet { 78 | return errors.New("SGetResponse.DeserialiseFromTTLV: input is not a get response") 79 | } 80 | return nil 81 | } 82 | 83 | // 42007c - response payload from a get response 84 | type SResponsePayloadGet struct { 85 | EObjectType ttlv.Enumeration // 420057 86 | TUniqueID ttlv.Text // 420094 87 | SSymmetricKey SSymmetricKey // 42008f 88 | } 89 | 90 | func (getPayload *SResponsePayloadGet) SerialiseToTTLV() ttlv.Item { 91 | getPayload.EObjectType.Tag = TagObjectType 92 | getPayload.TUniqueID.Tag = TagUniqueID 93 | return ttlv.NewStructure(TagResponsePayload, &getPayload.EObjectType, &getPayload.TUniqueID, getPayload.SSymmetricKey.SerialiseToTTLV()) 94 | } 95 | func (getPayload *SResponsePayloadGet) DeserialiseFromTTLV(in ttlv.Item) error { 96 | if err := DecodeStructItem(in, TagResponsePayload, TagObjectType, &getPayload.EObjectType); err != nil { 97 | return err 98 | } else if err := DecodeStructItem(in, TagResponsePayload, TagUniqueID, &getPayload.TUniqueID); err != nil { 99 | return err 100 | } else if err := DecodeStructItem(in, TagResponsePayload, TagSymmetricKey, &getPayload.SSymmetricKey); err != nil { 101 | return err 102 | } 103 | return nil 104 | } 105 | 106 | // 42008f 107 | type SSymmetricKey struct { 108 | SKeyBlock SKeyBlock 109 | } 110 | 111 | func (symKey *SSymmetricKey) SerialiseToTTLV() ttlv.Item { 112 | return ttlv.NewStructure(TagSymmetricKey, symKey.SKeyBlock.SerialiseToTTLV()) 113 | } 114 | func (symKey *SSymmetricKey) DeserialiseFromTTLV(in ttlv.Item) error { 115 | if err := DecodeStructItem(in, TagSymmetricKey, TagKeyBlock, &symKey.SKeyBlock); err != nil { 116 | return err 117 | } 118 | return nil 119 | } 120 | 121 | // 420040 122 | type SKeyBlock struct { 123 | EFormatType ttlv.Enumeration // 420042 124 | SKeyValue SKeyValue 125 | ECryptoAlgorithm ttlv.Enumeration // 420028 126 | ECryptoLen ttlv.Integer // 42002a 127 | } 128 | 129 | func (block *SKeyBlock) SerialiseToTTLV() ttlv.Item { 130 | block.EFormatType.Tag = TagFormatType 131 | block.ECryptoAlgorithm.Tag = TagCryptoAlgorithm 132 | block.ECryptoLen.Tag = TagCryptoLen 133 | return ttlv.NewStructure(TagKeyBlock, &block.EFormatType, block.SKeyValue.SerialiseToTTLV(), &block.ECryptoAlgorithm, &block.ECryptoLen) 134 | } 135 | func (block *SKeyBlock) DeserialiseFromTTLV(in ttlv.Item) error { 136 | if err := DecodeStructItem(in, TagKeyBlock, TagFormatType, &block.EFormatType); err != nil { 137 | return err 138 | } else if err := DecodeStructItem(in, TagKeyBlock, TagKeyValue, &block.SKeyValue); err != nil { 139 | return err 140 | } else if err := DecodeStructItem(in, TagKeyBlock, TagCryptoAlgorithm, &block.ECryptoAlgorithm); err != nil { 141 | return err 142 | } else if err := DecodeStructItem(in, TagKeyBlock, TagCryptoLen, &block.ECryptoLen); err != nil { 143 | return err 144 | } 145 | return nil 146 | } 147 | 148 | // 420045 - this is value of an encryption key, not to be confused with a key-value pair. 149 | type SKeyValue struct { 150 | BKeyMaterial ttlv.Bytes // 420043 151 | } 152 | 153 | func (key *SKeyValue) SerialiseToTTLV() ttlv.Item { 154 | key.BKeyMaterial.Tag = TagKeyMaterial 155 | return ttlv.NewStructure(TagKeyValue, &key.BKeyMaterial) 156 | } 157 | 158 | func (key *SKeyValue) DeserialiseFromTTLV(in ttlv.Item) error { 159 | if err := DecodeStructItem(in, TagKeyValue, TagKeyMaterial, &key.BKeyMaterial); err != nil { 160 | return err 161 | } 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /fs/file.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package fs 4 | 5 | import ( 6 | "crypto/rand" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "path" 14 | "path/filepath" 15 | "strings" 16 | "syscall" 17 | ) 18 | 19 | const ( 20 | BIN_RSYNC = "/usr/bin/rsync" 21 | ERASE_NUM_PASS = 10 22 | ERASE_BLOCK_SIZE = 128 * 1024 23 | ) 24 | 25 | // Test whether file at the specified path can be read and contains the string. Case is sensitive. 26 | func FileContains(paths, substr string) error { 27 | if paths == "" { 28 | return errors.New("Input path is empty") 29 | } 30 | if _, err := os.Stat(paths); os.IsNotExist(err) { 31 | return fmt.Errorf("File \"%s\" does not exist", paths) 32 | } 33 | content, err := ioutil.ReadFile(paths) 34 | if err != nil { 35 | return fmt.Errorf("Failed to read file \"%s\" - %v", paths, err) 36 | } 37 | if !strings.Contains(string(content), substr) { 38 | return fmt.Errorf("File \"%s\" does not contain keyword \"%s\"", paths, substr) 39 | } 40 | return nil 41 | } 42 | 43 | // Test whether the specified directory can be read. 44 | func IsDir(paths string) error { 45 | st, err := os.Stat(paths) 46 | if err == nil { 47 | if st.IsDir() { 48 | return nil 49 | } else { 50 | return fmt.Errorf("\"%s\" is not a directory", paths) 51 | } 52 | } else { 53 | if os.IsNotExist(err) { 54 | return fmt.Errorf("Directory \"%s\" does not exist", paths) 55 | } else { 56 | return fmt.Errorf("Failed to read directory \"%s\" - %v", paths, err) 57 | } 58 | } 59 | } 60 | 61 | /* 62 | Call rsync to recursively copy all files under source directory, including all file attributes/links, to the 63 | destination directory. If supplied, rsync progress output will be copied to the output stream. 64 | If some files already exist in the destination and the source files are newer, they will be overwritten. 65 | Both source directory and destination directory must be absolute. 66 | */ 67 | func MirrorFiles(srcDir, destDir string, progressOut io.Writer) error { 68 | // Validate input paths 69 | srcDir = path.Clean(srcDir) 70 | destDir = path.Clean(destDir) 71 | if len(srcDir) < 2 || srcDir[0] != '/' { 72 | return fmt.Errorf("MirrorFiles: source \"%s\" should not be / and must be an absolute path", srcDir) 73 | } else if len(destDir) < 2 || destDir[0] != '/' { 74 | return fmt.Errorf("MirrorFiles: destination \"%s\" should not be / and must be an absolute path", destDir) 75 | } else if srcDir == destDir { 76 | return fmt.Errorf("MirrorFiles: source and destination directories are both \"%s\"", destDir) 77 | } else if strings.HasPrefix(destDir, srcDir) || strings.HasPrefix(srcDir, destDir) { 78 | return fmt.Errorf("MirrorFiles: source \"%s\" and destination \"%s\" directory should not overlap", srcDir, destDir) 79 | } 80 | // Enhance storage persistence before and after the operation 81 | syscall.Sync() 82 | defer syscall.Sync() 83 | // Source must be a directory 84 | if st, err := os.Stat(srcDir); err != nil { 85 | return fmt.Errorf("MirrorFiles: cannot read source directory \"%s\" - %v", srcDir, err) 86 | } else if !st.IsDir() { 87 | return fmt.Errorf("MirrorFiles: source location \"%s\" is not a directory", srcDir) 88 | } 89 | // Make the destination directory if it does not exist 90 | makeDestDir := false 91 | if st, err := os.Stat(destDir); os.IsNotExist(err) { 92 | makeDestDir = true 93 | } else if err != nil { 94 | return fmt.Errorf("MirrorFiles: cannot read destination directory \"%s\" - %v", srcDir, err) 95 | } else if !st.IsDir() { 96 | return fmt.Errorf("MirrorFiles: destination location \"%s\" is not a directory", srcDir) 97 | } 98 | // Lint source and destination path parameters to fit into rsync convention 99 | if srcDir[len(srcDir)-1] != '/' { 100 | srcDir += "/" 101 | } 102 | if destDir[len(destDir)-1] == '/' { 103 | destDir = destDir[0 : len(destDir)-1] 104 | } 105 | if !makeDestDir { 106 | destDir += "/" 107 | } 108 | /* 109 | Start mirroring: 110 | - a - archive; recursive; preserve symlinks, permissions, modification times, group&owner, device and special files. 111 | - H - preserve hardlinks 112 | - A - preserve ACLs and permissions 113 | - X - preserve extended attributes 114 | - x - do not cross file system boundaries 115 | - S - handle sparse files 116 | - W - copy whole files without using delta algorithm 117 | - v - show copied files in standard output 118 | */ 119 | cmd := exec.Command(BIN_RSYNC, "-aHAXxSWv", srcDir, destDir) 120 | cmd.Stdout = progressOut 121 | if err := cmd.Start(); err != nil { 122 | return err 123 | } 124 | return cmd.Wait() 125 | } 126 | 127 | // Count the total space usage of the specified path; the path can be either a file or a directory. 128 | func FileSpaceUsage(paths string) (totalSize int64, err error) { 129 | err = filepath.Walk(paths, func(thisPath string, thisFile os.FileInfo, thisErr error) error { 130 | if thisErr != nil { 131 | return thisErr 132 | } 133 | if !thisFile.IsDir() { 134 | totalSize += thisFile.Size() 135 | } 136 | return nil 137 | }) 138 | return 139 | } 140 | 141 | /* 142 | Securely erase a file by overwriting it 10 times with random data, then delete the file. 143 | Needless to say this function is painfully slow. 144 | It relies on a vital assumption that the file system will overwrite data in place, bear in mind that many modern 145 | file systems and hardware designs do not satisfy the assumption, here are some examples: 146 | - Metadata/data journal. 147 | - Data compression. 148 | - Transient or persistent cache. 149 | - Additional redundancy mechanisms such as RAID. 150 | */ 151 | func SecureErase(filePath string, delete bool) error { 152 | fh, err := os.OpenFile(filePath, os.O_RDWR, 0000) 153 | if err != nil { 154 | return err 155 | } 156 | defer fh.Close() 157 | 158 | fileSize, err := fh.Seek(0, 2) 159 | if err != nil { 160 | return err 161 | } 162 | // Overwrite 10 passes 163 | for pass := 0; pass < ERASE_NUM_PASS; pass++ { 164 | if _, err := fh.Seek(0, 0); err != nil { 165 | return err 166 | } 167 | for i := int64(0); i < fileSize; i += ERASE_BLOCK_SIZE { 168 | // Generate a block of random data gathered from kernel's cryptographic entropy pool 169 | sizeOfBlock := int64(ERASE_BLOCK_SIZE) 170 | if fileSize-i < ERASE_BLOCK_SIZE { 171 | // Avoid writing beyond EOF 172 | sizeOfBlock = fileSize - i 173 | } 174 | randData := make([]byte, sizeOfBlock) 175 | if _, err := rand.Read(randData); err != nil { 176 | panic(fmt.Errorf("SecureErase: failed to read %d bytes from random source", ERASE_BLOCK_SIZE)) 177 | } 178 | // Write the block of random data and sync to storage 179 | if _, err := fh.Write(randData); err != nil { 180 | return err 181 | } else if err := fh.Sync(); err != nil { 182 | return err 183 | } 184 | } 185 | } 186 | if err := fh.Close(); err != nil { 187 | return err 188 | } else if delete { 189 | return os.Remove(filePath) 190 | } 191 | return nil 192 | } 193 | -------------------------------------------------------------------------------- /kmip/ttlv/sample.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package ttlv 4 | 5 | var SampleCreateRequest = WiresharkDumpToBytes(` 6 | 0000 42 00 78 01 00 00 01 a8 42 00 77 01 00 00 00 80 7 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 8 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 9 | 0030 00 00 00 02 00 00 00 00 42 00 0c 01 00 00 00 40 10 | 0040 42 00 23 01 00 00 00 38 42 00 24 05 00 00 00 04 11 | 0050 00 00 00 01 00 00 00 00 42 00 25 01 00 00 00 20 12 | 0060 42 00 99 07 00 00 00 08 74 65 73 74 75 73 65 72 13 | 0070 42 00 a1 07 00 00 00 08 74 65 73 74 70 61 73 73 14 | 0080 42 00 0d 02 00 00 00 04 00 00 00 01 00 00 00 00 15 | 0090 42 00 0f 01 00 00 01 18 42 00 5c 05 00 00 00 04 16 | 00a0 00 00 00 01 00 00 00 00 42 00 79 01 00 00 01 00 17 | 00b0 42 00 57 05 00 00 00 04 00 00 00 02 00 00 00 00 18 | 00c0 42 00 91 01 00 00 00 e8 42 00 08 01 00 00 00 30 19 | 00d0 42 00 0a 07 00 00 00 17 43 72 79 70 74 6f 67 72 20 | 00e0 61 70 68 69 63 20 41 6c 67 6f 72 69 74 68 6d 00 21 | 00f0 42 00 0b 05 00 00 00 04 00 00 00 03 00 00 00 00 22 | 0100 42 00 08 01 00 00 00 30 42 00 0a 07 00 00 00 14 23 | 0110 43 72 79 70 74 6f 67 72 61 70 68 69 63 20 4c 65 24 | 0120 6e 67 74 68 00 00 00 00 42 00 0b 02 00 00 00 04 25 | 0130 00 00 00 80 00 00 00 00 42 00 08 01 00 00 00 30 26 | 0140 42 00 0a 07 00 00 00 18 43 72 79 70 74 6f 67 72 27 | 0150 61 70 68 69 63 20 55 73 61 67 65 20 4d 61 73 6b 28 | 0160 42 00 0b 02 00 00 00 04 00 00 00 0c 00 00 00 00 29 | 0170 42 00 08 01 00 00 00 38 42 00 0a 07 00 00 00 04 30 | 0180 4e 61 6d 65 00 00 00 00 42 00 0b 01 00 00 00 20 31 | 0190 42 00 55 07 00 00 00 07 41 45 53 4b 65 79 31 00 32 | 01a0 42 00 54 05 00 00 00 04 00 00 00 01 00 00 00 00`) 33 | 34 | var SampleCreateResponseSuccess = WiresharkDumpToBytes(` 35 | 0000 42 00 7b 01 00 00 00 a0 42 00 7a 01 00 00 00 48 36 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 37 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 38 | 0030 00 00 00 02 00 00 00 00 42 00 92 09 00 00 00 08 39 | 0040 00 00 00 00 58 ef 52 8d 42 00 0d 02 00 00 00 04 40 | 0050 00 00 00 01 00 00 00 00 42 00 0f 01 00 00 00 48 41 | 0060 42 00 5c 05 00 00 00 04 00 00 00 01 00 00 00 00 42 | 0070 42 00 7f 05 00 00 00 04 00 00 00 00 00 00 00 00 43 | 0080 42 00 7c 01 00 00 00 20 42 00 57 05 00 00 00 04 44 | 0090 00 00 00 02 00 00 00 00 42 00 94 07 00 00 00 01 45 | 00a0 31 00 00 00 00 00 00 00`) 46 | 47 | var SampleGetRequest = WiresharkDumpToBytes(` 48 | 0000 42 00 78 01 00 00 00 b8 42 00 77 01 00 00 00 80 49 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 50 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 51 | 0030 00 00 00 02 00 00 00 00 42 00 0c 01 00 00 00 40 52 | 0040 42 00 23 01 00 00 00 38 42 00 24 05 00 00 00 04 53 | 0050 00 00 00 01 00 00 00 00 42 00 25 01 00 00 00 20 54 | 0060 42 00 99 07 00 00 00 08 74 65 73 74 75 73 65 72 55 | 0070 42 00 a1 07 00 00 00 08 74 65 73 74 70 61 73 73 56 | 0080 42 00 0d 02 00 00 00 04 00 00 00 01 00 00 00 00 57 | 0090 42 00 0f 01 00 00 00 28 42 00 5c 05 00 00 00 04 58 | 00a0 00 00 00 0a 00 00 00 00 42 00 79 01 00 00 00 10 59 | 00b0 42 00 94 07 00 00 00 01 31 00 00 00 00 00 00 00`) 60 | 61 | var SampleGetResponseSuccess = WiresharkDumpToBytes(` 62 | 0000 42 00 7b 01 00 00 01 00 42 00 7a 01 00 00 00 48 63 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 64 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 65 | 0030 00 00 00 02 00 00 00 00 42 00 92 09 00 00 00 08 66 | 0040 00 00 00 00 58 ef 52 8d 42 00 0d 02 00 00 00 04 67 | 0050 00 00 00 01 00 00 00 00 42 00 0f 01 00 00 00 a8 68 | 0060 42 00 5c 05 00 00 00 04 00 00 00 0a 00 00 00 00 69 | 0070 42 00 7f 05 00 00 00 04 00 00 00 00 00 00 00 00 70 | 0080 42 00 7c 01 00 00 00 80 42 00 57 05 00 00 00 04 71 | 0090 00 00 00 02 00 00 00 00 42 00 94 07 00 00 00 01 72 | 00a0 31 00 00 00 00 00 00 00 42 00 8f 01 00 00 00 58 73 | 00b0 42 00 40 01 00 00 00 50 42 00 42 05 00 00 00 04 74 | 00c0 00 00 00 01 00 00 00 00 42 00 45 01 00 00 00 18 75 | 00d0 42 00 43 08 00 00 00 10 40 4a 70 1c 3c ae ea af 76 | 00e0 04 74 25 69 cf 64 b2 7a 42 00 28 05 00 00 00 04 77 | 00f0 00 00 00 03 00 00 00 00 42 00 2a 02 00 00 00 04 78 | 0100 00 00 00 80 00 00 00 00`) 79 | 80 | var SampleGetResponseFailure = WiresharkDumpToBytes(` 81 | 0000 42 00 7b 01 00 00 00 b8 42 00 7a 01 00 00 00 48 82 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 83 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 84 | 0030 00 00 00 02 00 00 00 00 42 00 92 09 00 00 00 08 85 | 0040 00 00 00 00 59 09 e8 3a 42 00 0d 02 00 00 00 04 86 | 0050 00 00 00 01 00 00 00 00 42 00 0f 01 00 00 00 60 87 | 0060 42 00 5c 05 00 00 00 04 00 00 00 0a 00 00 00 00 88 | 0070 42 00 7f 05 00 00 00 04 00 00 00 01 00 00 00 00 89 | 0080 42 00 7e 05 00 00 00 04 00 00 00 01 00 00 00 00 90 | 0090 42 00 7d 07 00 00 00 25 43 6f 75 6c 64 20 6e 6f 91 | 00a0 74 20 6c 6f 63 61 74 65 20 6f 62 6a 65 63 74 3a 92 | 00b0 20 64 6f 65 73 6e 6f 74 65 78 69 73 74 00 00 00`) 93 | 94 | var SampleDestroyRequest = WiresharkDumpToBytes(` 95 | 0000 42 00 78 01 00 00 00 b8 42 00 77 01 00 00 00 80 96 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 97 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 98 | 0030 00 00 00 02 00 00 00 00 42 00 0c 01 00 00 00 40 99 | 0040 42 00 23 01 00 00 00 38 42 00 24 05 00 00 00 04 100 | 0050 00 00 00 01 00 00 00 00 42 00 25 01 00 00 00 20 101 | 0060 42 00 99 07 00 00 00 08 74 65 73 74 75 73 65 72 102 | 0070 42 00 a1 07 00 00 00 08 74 65 73 74 70 61 73 73 103 | 0080 42 00 0d 02 00 00 00 04 00 00 00 01 00 00 00 00 104 | 0090 42 00 0f 01 00 00 00 28 42 00 5c 05 00 00 00 04 105 | 00a0 00 00 00 14 00 00 00 00 42 00 79 01 00 00 00 10 106 | 00b0 42 00 94 07 00 00 00 01 31 00 00 00 00 00 00 00`) 107 | 108 | var SampleDestroyResponseSuccess = WiresharkDumpToBytes(` 109 | 0000 42 00 7b 01 00 00 00 90 42 00 7a 01 00 00 00 48 110 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 111 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 112 | 0030 00 00 00 02 00 00 00 00 42 00 92 09 00 00 00 08 113 | 0040 00 00 00 00 58 ef 52 8d 42 00 0d 02 00 00 00 04 114 | 0050 00 00 00 01 00 00 00 00 42 00 0f 01 00 00 00 38 115 | 0060 42 00 5c 05 00 00 00 04 00 00 00 14 00 00 00 00 116 | 0070 42 00 7f 05 00 00 00 04 00 00 00 00 00 00 00 00 117 | 0080 42 00 7c 01 00 00 00 10 42 00 94 07 00 00 00 01 118 | 0090 31 00 00 00 00 00 00 00`) 119 | 120 | var SampleDestroyResponseFailure = WiresharkDumpToBytes(` 121 | 0000 42 00 7b 01 00 00 00 b8 42 00 7a 01 00 00 00 48 122 | 0010 42 00 69 01 00 00 00 20 42 00 6a 02 00 00 00 04 123 | 0020 00 00 00 01 00 00 00 00 42 00 6b 02 00 00 00 04 124 | 0030 00 00 00 02 00 00 00 00 42 00 92 09 00 00 00 08 125 | 0040 00 00 00 00 59 09 e8 3a 42 00 0d 02 00 00 00 04 126 | 0050 00 00 00 01 00 00 00 00 42 00 0f 01 00 00 00 60 127 | 0060 42 00 5c 05 00 00 00 04 00 00 00 14 00 00 00 00 128 | 0070 42 00 7f 05 00 00 00 04 00 00 00 01 00 00 00 00 129 | 0080 42 00 7e 05 00 00 00 04 00 00 00 01 00 00 00 00 130 | 0090 42 00 7d 07 00 00 00 25 43 6f 75 6c 64 20 6e 6f 131 | 00a0 74 20 6c 6f 63 61 74 65 20 6f 62 6a 65 63 74 3a 132 | 00b0 20 64 6f 65 73 6e 6f 74 65 78 69 73 74 00 00 00`) 133 | -------------------------------------------------------------------------------- /sys/sysconfig.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package sys 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | var consecutiveSpaces = regexp.MustCompile("[[:space:]]+") // split fields by consecutive spaces 17 | 18 | // A single key-value pair in sysconfig file. 19 | type SysconfigEntry struct { 20 | LeadingComments []string // The comment lines leading to the key-value pair, including prefix '#', excluding end-of-line. 21 | Key string // The key. 22 | Value string // The value, excluding '=' character and double-quotes. Values will always come in double-quotes when converted to text. 23 | } 24 | 25 | // Key-value pairs of a sysconfig file. It is able to convert back to original text in the original key order. 26 | type Sysconfig struct { 27 | AllValues []*SysconfigEntry // All key-value pairs in the orignal order. 28 | KeyValue map[string]*SysconfigEntry 29 | } 30 | 31 | // Read sysconfig file and parse the file content into memory structures. 32 | func ParseSysconfigFile(fileName string, autoCreate bool) (*Sysconfig, error) { 33 | content, err := ioutil.ReadFile(fileName) 34 | if os.IsNotExist(err) && autoCreate { 35 | err = os.MkdirAll(path.Dir(fileName), 0755) 36 | if err != nil { 37 | return nil, err 38 | } 39 | err = ioutil.WriteFile(fileName, []byte{}, 0644) 40 | content = []byte{} 41 | if err != nil { 42 | return nil, err 43 | } 44 | } else if err != nil { 45 | return nil, err 46 | } 47 | return ParseSysconfig(string(content)) 48 | } 49 | 50 | // Read sysconfig text and parse the text into memory structures. 51 | func ParseSysconfig(input string) (*Sysconfig, error) { 52 | conf := &Sysconfig{ 53 | AllValues: make([]*SysconfigEntry, 0, 0), 54 | KeyValue: make(map[string]*SysconfigEntry), 55 | } 56 | leadingComments := make([]string, 0, 0) 57 | for _, line := range strings.Split(input, "\n") { 58 | line = strings.TrimSpace(line) 59 | if strings.HasPrefix(line, "#") { 60 | // Line is a comment 61 | leadingComments = append(leadingComments, line) 62 | } else if eqChar := strings.IndexRune(line, '='); eqChar != -1 { 63 | // Line is a key-value pair 64 | key := strings.TrimSpace(line[0:eqChar]) 65 | value := strings.Trim(strings.TrimSpace(line[eqChar+1:]), `"`) 66 | kv := &SysconfigEntry{ 67 | LeadingComments: leadingComments, 68 | Key: key, 69 | Value: value, 70 | } 71 | conf.AllValues = append(conf.AllValues, kv) 72 | conf.KeyValue[key] = kv 73 | // Clear comments to be ready for the next key-value pair 74 | leadingComments = make([]string, 0, 0) 75 | } else { 76 | // Consider other lines (such as blank lines) as comments 77 | leadingComments = append(leadingComments, line) 78 | } 79 | } 80 | return conf, nil 81 | } 82 | 83 | // Set value for a key. If the key does not yet exist, it is created. 84 | func (conf *Sysconfig) Set(key string, value interface{}) { 85 | kv, exists := conf.KeyValue[key] 86 | if exists { 87 | kv.Value = fmt.Sprint(value) 88 | } else { 89 | kv = &SysconfigEntry{ 90 | LeadingComments: nil, 91 | Key: key, 92 | Value: fmt.Sprint(value), 93 | } 94 | // When converted back into text, the new value will be appended at the end. 95 | conf.AllValues = append(conf.AllValues, kv) 96 | } 97 | conf.KeyValue[key] = kv 98 | } 99 | 100 | // Give a space-separated integer array value to a key. If the key does not yet exist, it is created. 101 | func (conf *Sysconfig) SetIntArray(key string, values []int) { 102 | strs := make([]string, len(values)) 103 | for i, val := range values { 104 | strs[i] = strconv.Itoa(val) 105 | } 106 | conf.Set(key, strings.Join(strs, " ")) 107 | } 108 | 109 | // Give a space-separated string array value to a key. If the key does not yet exist, it is created. 110 | func (conf *Sysconfig) SetStrArray(key string, values []string) { 111 | conf.Set(key, strings.Join(values, " ")) 112 | } 113 | 114 | // Return integer value that belongs to the key, or the default if the key does not exist or value is not an integer. 115 | func (conf *Sysconfig) GetInt(key string, defaultValue int) int { 116 | entry, exists := conf.KeyValue[key] 117 | if !exists { 118 | return defaultValue 119 | } 120 | intValue, err := strconv.Atoi(entry.Value) 121 | if err != nil { 122 | return defaultValue 123 | } 124 | return intValue 125 | } 126 | 127 | // Return uint64 value that belongs to the key, or the default if the key does not exist or value is not an integer. 128 | func (conf *Sysconfig) GetUint64(key string, defaultValue uint64) uint64 { 129 | entry, exists := conf.KeyValue[key] 130 | if !exists { 131 | return defaultValue 132 | } 133 | intValue, err := strconv.ParseUint(entry.Value, 10, 64) 134 | if err != nil { 135 | return defaultValue 136 | } 137 | return intValue 138 | } 139 | 140 | // Return string value that belongs to the key, or the default value if the key does not exist. 141 | func (conf *Sysconfig) GetString(key, defaultValue string) string { 142 | entry, exists := conf.KeyValue[key] 143 | if !exists || strings.TrimSpace(entry.Value) == "" { 144 | return defaultValue 145 | } 146 | return strings.TrimSpace(entry.Value) 147 | } 148 | 149 | // Assume the key carries a space-separated array value, return the value array. 150 | func (conf *Sysconfig) GetStringArray(key string, defaultValue []string) (ret []string) { 151 | entry, exists := conf.KeyValue[key] 152 | if !exists { 153 | return defaultValue 154 | } 155 | split := consecutiveSpaces.Split(strings.TrimSpace(entry.Value), -1) 156 | ret = make([]string, 0, len(split)) 157 | for _, val := range split { 158 | if val != "" { 159 | ret = append(ret, val) 160 | } 161 | } 162 | return 163 | } 164 | 165 | // Assume the key carries a space-separated array of integers, return the array. Discard malformed integers. 166 | func (conf *Sysconfig) GetIntArray(key string, defaultValue []int) (ret []int) { 167 | entry, exists := conf.KeyValue[key] 168 | if !exists { 169 | return defaultValue 170 | } 171 | split := consecutiveSpaces.Split(strings.TrimSpace(entry.Value), -1) 172 | ret = make([]int, 0, len(split)) 173 | for _, val := range split { 174 | iVal, err := strconv.Atoi(val) 175 | if err == nil { 176 | ret = append(ret, iVal) 177 | } 178 | } 179 | return 180 | } 181 | 182 | // Return bool value that belongs to the key, or the default value if key does not exist. 183 | // True values are "yes" or "true". 184 | func (conf *Sysconfig) GetBool(key string, defaultValue bool) bool { 185 | defaultValStr := "no" 186 | if defaultValue { 187 | defaultValStr = "yes" 188 | } 189 | value := strings.ToLower(conf.GetString(key, defaultValStr)) 190 | return value == "yes" || value == "true" 191 | } 192 | 193 | // Convert key-value pairs back into text. Values are always surrounded by double-quotes. 194 | func (conf *Sysconfig) ToText() string { 195 | var ret bytes.Buffer 196 | for _, kv := range conf.AllValues { 197 | if kv.LeadingComments != nil && len(kv.LeadingComments) > 0 { 198 | ret.WriteString(strings.Join(kv.LeadingComments, "\n")) 199 | ret.WriteRune('\n') 200 | } 201 | ret.WriteString(fmt.Sprintf("%s=\"%s\"\n", kv.Key, kv.Value)) 202 | } 203 | return ret.String() 204 | } 205 | -------------------------------------------------------------------------------- /fs/fs.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package fs 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/sys" 9 | "os" 10 | "os/exec" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | ) 16 | 17 | const ( 18 | BIN_MKFS = "/usr/sbin/mkfs" 19 | BIN_LSBLK = "/usr/bin/lsblk" 20 | BIN_MOUNT = "/usr/bin/mount" 21 | BIN_UMOUNT = "/usr/bin/umount" 22 | ) 23 | 24 | var lsblkFields = regexp.MustCompile(`"((?:\\"|[^"])*)"`) // extract values from lsblk output 25 | 26 | // Represent a block device currently detected on the system. 27 | type BlockDevice struct { 28 | UUID string 29 | Name string // Name is the device node name 30 | Path string // full path to the device node under /dev including the prefix 31 | Type string // device type can be: partition, disk, encrypted, etc.. 32 | FileSystem string 33 | MountPoint string 34 | SizeByte int64 35 | PKName string // PKName is the underlying block device's node name of a crypt block device 36 | } 37 | 38 | // Return true if the block device is LUKS encrypted. 39 | func (blkDev BlockDevice) IsLUKSEncrypted() bool { 40 | return blkDev.FileSystem == "crypto_LUKS" 41 | } 42 | 43 | // A list of block devices. 44 | type BlockDevices []BlockDevice 45 | 46 | // Find the first block device that satisfies the given criteria. If a criteria is empty, it is ignored. 47 | func (blkDevs BlockDevices) GetByCriteria(uuid, devPath, devType, fileSystem, mountPoint, pkName, name string) (BlockDevice, bool) { 48 | for _, blkDev := range blkDevs { 49 | if (uuid == "" || blkDev.UUID == uuid) && 50 | (devPath == "" || blkDev.Path == devPath) && 51 | (devType == "" || blkDev.Type == devType) && 52 | (fileSystem == "" || blkDev.FileSystem == fileSystem) && 53 | (mountPoint == "" || blkDev.MountPoint == mountPoint) && 54 | (pkName == "" || blkDev.PKName == pkName) && 55 | (name == "" || blkDev.Name == name) { 56 | return blkDev, true 57 | } 58 | } 59 | return BlockDevice{}, false 60 | } 61 | 62 | /* 63 | Return all block devices defined in the input text. 64 | The input text is presumed to be obtained from the following command's output: 65 | lsblk -P -b -o UUID,KNAME,TYPE,FSTYPE,MOUNTPOINT,SIZE 66 | */ 67 | func ParseBlockDevs(txt string) BlockDevices { 68 | ret := make([]BlockDevice, 0, 8) 69 | for _, line := range strings.Split(txt, "\n") { 70 | fields := lsblkFields.FindAllString(line, -1) 71 | if len(fields) < 7 { 72 | continue // skip empty lines 73 | } 74 | // Remove surrounding quotes from match 75 | for fi, field := range fields { 76 | fields[fi] = field[1 : len(field)-1] 77 | } 78 | devPath := "/dev/" + fields[1] 79 | devType := fields[2] 80 | if devType == "crypt" { 81 | devPath = "/dev/mapper/" + fields[1] 82 | } 83 | blkDev := BlockDevice{ 84 | UUID: fields[0], 85 | Name: fields[1], 86 | Path: devPath, 87 | Type: devType, 88 | FileSystem: fields[3], 89 | MountPoint: fields[4], 90 | PKName: fields[6], 91 | } 92 | // Block device size can be empty 93 | if fields[5] != "" { 94 | iByte, intErr := strconv.ParseUint(fields[5], 10, 64) 95 | if intErr != nil { 96 | panic(fmt.Errorf("ParseBlockDevs: failed to parse size number in line \"%s\"", line)) 97 | } 98 | blkDev.SizeByte = int64(iByte) 99 | } 100 | ret = append(ret, blkDev) 101 | } 102 | return ret 103 | } 104 | 105 | // Return all block devices currently detected on the system. 106 | func GetBlockDevices() BlockDevices { 107 | /* 108 | -P - generate output in a way processable by programs. 109 | -b - block device size is in bytes. 110 | -o - choose output columns. 111 | 112 | The parser reads NAME instead of KNAME because KNAME does not apply for names under /dev/mapper. 113 | */ 114 | _, stdout, stderr, err := sys.Exec(nil, nil, nil, BIN_LSBLK, "-P", "-b", "-o", "UUID,NAME,TYPE,FSTYPE,MOUNTPOINT,SIZE,PKNAME") 115 | if err != nil { 116 | panic(fmt.Errorf("GetBlockDevices: failed to execute lsblk - %v %s %s", err, stdout, stderr)) 117 | } 118 | return ParseBlockDevs(stdout) 119 | } 120 | 121 | // Return information about the specific block device. 122 | // The path of block device in return value will match the input device node path. 123 | func GetBlockDevice(node string) (blkDev BlockDevice, found bool) { 124 | if !strings.HasPrefix(node, "/dev/") { 125 | node = "/dev/" + node 126 | } 127 | /* 128 | -P - generate output in a way processable by programs. 129 | -b - block device size is in bytes. 130 | -o - choose output columns. 131 | */ 132 | _, stdout, _, _ := sys.Exec(nil, nil, nil, BIN_LSBLK, "-P", "-b", "-o", "UUID,NAME,TYPE,FSTYPE,MOUNTPOINT,SIZE,PKNAME", node) 133 | blkDevs := ParseBlockDevs(stdout) 134 | found = len(blkDevs) > 0 135 | if found { 136 | blkDev = blkDevs[0] 137 | blkDev.Path = node 138 | } 139 | return 140 | } 141 | 142 | // Return nil if the file specified is a block device. Return an error otherwise. 143 | func CheckBlockDevice(filePath string) error { 144 | if !strings.HasPrefix(filePath, "/dev/") { 145 | return fmt.Errorf("CheckBlockDevice: \"%s\" is not from /dev", filePath) 146 | } 147 | st, err := os.Stat(filePath) 148 | if err != nil { 149 | return fmt.Errorf("CheckBlockDevice: cannot read \"%s\"", filePath) 150 | } else if st.Sys().(*syscall.Stat_t).Mode&syscall.S_IFBLK != syscall.S_IFBLK { 151 | return fmt.Errorf("CheckBlockDevice: \"%s\" is not a block device", filePath) 152 | } 153 | return nil 154 | } 155 | 156 | // Call mkfs to make a new file system on the block device. 157 | func Format(blockDev, fsType string) error { 158 | if err := CheckBlockDevice(blockDev); err != nil { 159 | return err 160 | } 161 | cmd := exec.Command(BIN_MKFS, "-t", fsType, blockDev) 162 | if out, err := cmd.CombinedOutput(); err != nil { 163 | return fmt.Errorf("Format: failed to format \"%s\" - %v %s", blockDev, err, out) 164 | } 165 | return nil 166 | } 167 | 168 | // Call mount to mount a file system. The mounted file system will be exposed to all processes on the computer. 169 | func Mount(blockDev, fsType string, fsOptions []string, mountPoint string) error { 170 | if err := CheckBlockDevice(blockDev); err != nil { 171 | return err 172 | } 173 | var cmd *exec.Cmd 174 | params := make([]string, 0, 8) 175 | params = append(params, "--make-shared") 176 | if fsType != "" { 177 | params = append(params, "-t", fsType) 178 | } 179 | if fsOptions != nil && len(fsOptions) > 0 { 180 | params = append(params, "-o", strings.Join(fsOptions, ",")) 181 | } 182 | params = append(params, blockDev, mountPoint) 183 | cmd = exec.Command(BIN_MOUNT, params...) 184 | if out, err := cmd.CombinedOutput(); err != nil { 185 | return fmt.Errorf("Mount: failed to mount \"%s\" on \"%s\" using options \"%s\" - %v %s", blockDev, mountPoint, strings.Join(fsOptions, ","), err, out) 186 | } 187 | return nil 188 | } 189 | 190 | // GetSystemdMountNameForDir returns systemd's mount unit associated with the directory, supposedly a mount point. 191 | func GetSystemdMountNameForDir(dirPath string) string { 192 | var ret bytes.Buffer 193 | for i, ch := range dirPath { 194 | if i == 0 && ch == '/' { 195 | continue 196 | } else if ch == '/' { 197 | ret.WriteRune('-') 198 | } else if ch >= 48 && ch <= 57 || ch >= 65 && ch <= 90 || ch >= 97 && ch <= 122 { 199 | ret.WriteRune(ch) 200 | } else { 201 | ret.WriteString(fmt.Sprintf("\\x%x", ch)) 202 | } 203 | } 204 | return ret.String() + ".mount" 205 | } 206 | 207 | // Umount un-mounts a file system by interacting with systemd. 208 | func Umount(mountPoint string) error { 209 | err1 := sys.SystemctlStop(GetSystemdMountNameForDir(mountPoint)) 210 | out, err2 := exec.Command(BIN_UMOUNT, mountPoint).CombinedOutput() 211 | devs := GetBlockDevices() 212 | if _, found := devs.GetByCriteria("", "", "", "", mountPoint, "", ""); !found { 213 | return nil 214 | } 215 | return fmt.Errorf("Umount: first attempt failed with error \"%v\", and second attempt failed with output \"%s\" and error \"%v\"", err1, out, err2) 216 | } 217 | 218 | // Return amount of free space available on the disk where input paths is mounted on. 219 | func FreeSpace(paths string) (int64, error) { 220 | var stats syscall.Statfs_t 221 | if err := syscall.Statfs(paths, &stats); err != nil { 222 | return 0, fmt.Errorf("Failed to calculate free space on \"%s\" - %v", paths, err) 223 | } 224 | return stats.Bsize * int64(stats.Blocks), nil 225 | } 226 | -------------------------------------------------------------------------------- /keyserv/rpc_client.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keyserv 4 | 5 | import ( 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | "github.com/SUSE/cryptctl/sys" 12 | "io/ioutil" 13 | "net" 14 | "net/rpc" 15 | "os" 16 | "path" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | const ( 22 | RPC_DIAL_TIMEOUT_SEC = 10 23 | CLIENT_CONF_HOST = "KEY_SERVER_HOST" 24 | CLIENT_CONF_PORT = "KEY_SERVER_PORT" 25 | CLIENT_CONF_CA = "TLS_CA_PEM" 26 | CLIENT_CONF_CERT = "TLS_CERT_PEM" 27 | CLIENT_CONF_CERT_KEY = "TLS_CERT_KEY_PEM" 28 | TEST_RPC_PASS = "pass" 29 | ) 30 | 31 | // CryptClient implements an RPC client for CryptServer. 32 | type CryptClient struct { 33 | Address string // Address is the server address string, IP:port for TCP and file name for domain socket. 34 | Type string // Type is either "tcp" or "unix" depends on the connection address. 35 | TLSCert string // TLSCert is path to TLS certificate that is presented by client to server. 36 | TLSKey string // TLSKey is path to TLS key corresponding to the certificate. 37 | tlsConfig *tls.Config 38 | } 39 | 40 | /* 41 | Initialise an RPC client. 42 | The function does not immediately establish a connection to server, connection is only made along with each RPC call. 43 | */ 44 | func NewCryptClient(connType, address string, caCertPEM []byte, certPath, certKeyPath string) (*CryptClient, error) { 45 | client := &CryptClient{ 46 | Type: connType, 47 | Address: address, 48 | tlsConfig: new(tls.Config), 49 | } 50 | if caCertPEM != nil && len(caCertPEM) > 0 { 51 | // Use custom CA 52 | caCertPool := x509.NewCertPool() 53 | if !caCertPool.AppendCertsFromPEM(caCertPEM) { 54 | return nil, errors.New("NewCryptClient: failed to load custom CA certificates from PEM") 55 | } 56 | client.tlsConfig.RootCAs = caCertPool 57 | } 58 | if certPath != "" { 59 | // Tell client to present its identity to server 60 | clientCert, err := tls.LoadX509KeyPair(certPath, certKeyPath) 61 | if err != nil { 62 | return nil, err 63 | } 64 | client.tlsConfig.Certificates = []tls.Certificate{clientCert} 65 | } 66 | client.tlsConfig.BuildNameToCertificate() 67 | return client, nil 68 | } 69 | 70 | // Initialise an RPC client by reading settings from sysconfig file. 71 | func NewCryptClientFromSysconfig(sysconf *sys.Sysconfig) (*CryptClient, error) { 72 | host := sysconf.GetString(CLIENT_CONF_HOST, "") 73 | if host == "" { 74 | return nil, errors.New("NewCryptClientFromSysconfig: key server host is empty") 75 | } 76 | port := sysconf.GetInt(CLIENT_CONF_PORT, 3737) 77 | if port == 0 { 78 | return nil, errors.New("NewCryptClientFromSysconfig: key server port number is empty") 79 | } 80 | var caCertPEM []byte 81 | if ca := sysconf.GetString(CLIENT_CONF_CA, ""); ca != "" { 82 | var err error 83 | caCertPEM, err = ioutil.ReadFile(ca) 84 | if err != nil { 85 | return nil, fmt.Errorf("NewCryptClientFromSysconfig: failed to read CA PEM file at \"%s\" - %v", ca, err) 86 | } 87 | } 88 | return NewCryptClient("tcp", fmt.Sprintf("%s:%d", host, port), caCertPEM, sysconf.GetString(CLIENT_CONF_CERT, ""), sysconf.GetString(CLIENT_CONF_CERT_KEY, "")) 89 | } 90 | 91 | /* 92 | Establish a new TLS connection to RPC server and then invoke an RPC on the connection. 93 | The function deliberately establishes a new connection on each RPC call, in order to reduce complexity in managing 94 | the client connections, especially in the area of keep-alive. The client is not expected to make high volume of calls 95 | hence there is absolutely no performance concern. 96 | */ 97 | func (client *CryptClient) DoRPC(fun func(*rpc.Client) error) (err error) { 98 | var conn net.Conn 99 | if client.Type == "tcp" { 100 | conn, err = tls.DialWithDialer( 101 | &net.Dialer{Timeout: RPC_DIAL_TIMEOUT_SEC * time.Second}, 102 | "tcp", client.Address, client.tlsConfig) 103 | } else if client.Type == "unix" { 104 | // TLS is not involved in domain socket communication 105 | conn, err = net.Dial("unix", client.Address) 106 | } else { 107 | return fmt.Errorf("DoRPC: invalid client type \"%s\"", client.Type) 108 | } 109 | if err != nil { 110 | return fmt.Errorf("DoRPC: failed to connect to %s via %s - %v", client.Address, client.Type, err) 111 | } 112 | defer conn.Close() 113 | rpcClient := rpc.NewClient(conn) 114 | defer rpcClient.Close() 115 | if err := fun(rpcClient); err != nil { 116 | return fmt.Errorf("DoRPC: call failed - %v", err) 117 | } 118 | return nil 119 | } 120 | 121 | // Retrieve the salt that was used to hash server's access password. 122 | func (client *CryptClient) GetSalt() (salt PasswordSalt, err error) { 123 | err = client.DoRPC(func(rpcClient *rpc.Client) error { 124 | var dummy DummyAttr 125 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "GetSalt"), &dummy, &salt) 126 | }) 127 | return 128 | } 129 | 130 | // Ping RPC server. Return an error if there is a communication mishap or server has not undergone the initial setup. 131 | func (client *CryptClient) Ping(req PingRequest) error { 132 | return client.DoRPC(func(rpcClient *rpc.Client) error { 133 | var dummy DummyAttr 134 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "Ping"), req, &dummy) 135 | }) 136 | } 137 | 138 | // Create a new key record. 139 | func (client *CryptClient) CreateKey(req CreateKeyReq) (resp CreateKeyResp, err error) { 140 | err = client.DoRPC(func(rpcClient *rpc.Client) error { 141 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "CreateKey"), req, &resp) 142 | }) 143 | return 144 | } 145 | 146 | // Retrieve encryption keys without a password. 147 | func (client *CryptClient) AutoRetrieveKey(req AutoRetrieveKeyReq) (resp AutoRetrieveKeyResp, err error) { 148 | err = client.DoRPC(func(rpcClient *rpc.Client) error { 149 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "AutoRetrieveKey"), req, &resp) 150 | }) 151 | return 152 | } 153 | 154 | // Retrieve encryption keys using a password. All requested keys will be granted regardless of MaxActive restriction. 155 | func (client *CryptClient) ManualRetrieveKey(req ManualRetrieveKeyReq) (resp ManualRetrieveKeyResp, err error) { 156 | err = client.DoRPC(func(rpcClient *rpc.Client) error { 157 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "ManualRetrieveKey"), req, &resp) 158 | }) 159 | return 160 | } 161 | 162 | /* 163 | Submit a report that says the requester is still alive and holding the encryption keys. Return UUID of keys that are 164 | rejected - which means they previously lost contact with this host and no longer consider it eligible to hold the keys. 165 | */ 166 | func (client *CryptClient) ReportAlive(req ReportAliveReq) (rejectedUUIDs []string, err error) { 167 | err = client.DoRPC(func(rpcClient *rpc.Client) error { 168 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "ReportAlive"), req, &rejectedUUIDs) 169 | }) 170 | return 171 | } 172 | 173 | // Tell server to delete an encryption key. 174 | func (client *CryptClient) EraseKey(req EraseKeyReq) error { 175 | return client.DoRPC(func(rpcClient *rpc.Client) error { 176 | var dummy DummyAttr 177 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "EraseKey"), req, &dummy) 178 | }) 179 | } 180 | 181 | // Shut down server's listener. 182 | func (client *CryptClient) Shutdown(req ShutdownReq) error { 183 | return client.DoRPC(func(rpcClient *rpc.Client) error { 184 | var dummy DummyAttr 185 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "Shutdown"), req, &dummy) 186 | }) 187 | } 188 | 189 | // ReloadRecord tells server to reload exactly one database record. 190 | func (client *CryptClient) ReloadRecord(req ReloadRecordReq) error { 191 | return client.DoRPC(func(rpcClient *rpc.Client) error { 192 | var dummy DummyAttr 193 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "ReloadRecord"), req, &dummy) 194 | }) 195 | } 196 | 197 | func (client *CryptClient) PollCommand(req PollCommandReq) (resp PollCommandResp, err error) { 198 | err = client.DoRPC(func(rpcClient *rpc.Client) error { 199 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "PollCommand"), req, &resp) 200 | }) 201 | return 202 | } 203 | 204 | func (client *CryptClient) SaveCommandResult(req SaveCommandResultReq) error { 205 | return client.DoRPC(func(rpcClient *rpc.Client) error { 206 | var dummy DummyAttr 207 | return rpcClient.Call(fmt.Sprintf(RPCObjNameFmt, "SaveCommandResult"), req, &dummy) 208 | }) 209 | } 210 | 211 | // Start an RPC server in a testing configuration, return a client connected to the server and a teardown function. 212 | func StartTestServer(tb testing.TB) (*CryptClient, *CryptServer, func(testing.TB)) { 213 | keydbDir, err := ioutil.TempDir("", "cryptctl-rpctest") 214 | if err != nil { 215 | tb.Fatal(err) 216 | return nil, nil, nil 217 | } 218 | // Fill in configuration blanks (listen port is left at default) 219 | salt := NewSalt() 220 | passHash := HashPassword(salt, TEST_RPC_PASS) 221 | sysconf := GetDefaultKeySvcConf() 222 | sysconf.Set(SRV_CONF_KEYDB_DIR, keydbDir) 223 | sysconf.Set(SRV_CONF_TLS_CERT, path.Join(PkgInGopath, "keyserv", "rpc_test.crt")) 224 | sysconf.Set(SRV_CONF_TLS_KEY, path.Join(PkgInGopath, "keyserv", "rpc_test.key")) 225 | sysconf.Set(SRV_CONF_PASS_SALT, hex.EncodeToString(salt[:])) 226 | sysconf.Set(SRV_CONF_PASS_HASH, hex.EncodeToString(passHash[:])) 227 | // Start server 228 | srvConf := CryptServiceConfig{} 229 | srvConf.ReadFromSysconfig(sysconf) 230 | srv, err := NewCryptServer(srvConf, Mailer{}) 231 | if err != nil { 232 | tb.Fatal(err) 233 | return nil, nil, nil 234 | } 235 | if err := srv.ListenTCP(); err != nil { 236 | tb.Fatal(err) 237 | return nil, nil, nil 238 | } 239 | go srv.HandleTCPConnections() 240 | // The test certificate's CN is "localhost" 241 | caPath := path.Join(PkgInGopath, "keyrpc", "rpc_test.crt") 242 | certContent, err := ioutil.ReadFile(caPath) 243 | // Construct a client via function parameters 244 | client, err := NewCryptClient("tcp", "localhost:3737", certContent, "", "") 245 | if err != nil { 246 | tb.Fatal(err) 247 | return nil, nil, nil 248 | } 249 | client.tlsConfig.InsecureSkipVerify = true 250 | // Server should start within about 2 seconds 251 | serverReady := false 252 | for i := 0; i < 20; i++ { 253 | if err := client.Ping(PingRequest{PlainPassword: TEST_RPC_PASS}); err == nil { 254 | serverReady = true 255 | break 256 | } 257 | time.Sleep(100 * time.Millisecond) 258 | } 259 | if !serverReady { 260 | tb.Fatal("server did not start in time") 261 | return nil, nil, nil 262 | } 263 | tearDown := func(t testing.TB) { 264 | if err := client.Shutdown(ShutdownReq{Challenge: srv.AdminChallenge}); err != nil { 265 | t.Fatal(err) 266 | return 267 | } 268 | if err := client.Ping(PingRequest{PlainPassword: TEST_RPC_PASS}); err == nil { 269 | t.Fatal("server did not shutdown") 270 | return 271 | } 272 | if err := os.RemoveAll(keydbDir); err != nil { 273 | t.Fatal(err) 274 | return 275 | } 276 | } 277 | return client, srv, tearDown 278 | } 279 | -------------------------------------------------------------------------------- /kmip/ttlv/dencode.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package ttlv 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log" 13 | "reflect" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // Round input integer upward to be divisible by 8. 19 | func RoundUpTo8(in int) int { 20 | if in%8 != 0 { 21 | in += 8 - (in % 8) 22 | } 23 | return in 24 | } 25 | 26 | // Decode wireshark's hex dump of a network packet into byte array by removing its extra pieces. 27 | func WiresharkDumpToBytes(in string) []byte { 28 | var bufStr bytes.Buffer 29 | for _, line := range strings.Split(in, "\n") { 30 | if line == "" { 31 | continue 32 | } 33 | bufStr.WriteString(strings.Replace(line[7:], " ", "", -1)) 34 | } 35 | ret, err := hex.DecodeString(bufStr.String()) 36 | if err != nil { 37 | log.Printf("WiresharkDumpToBytes: failed to decode hex string - %v", err) 38 | return []byte{} 39 | } 40 | return ret 41 | } 42 | 43 | // Generate a string that describes an item (pointer) in very detail. If the item is a structure, the output descends into the child items too. 44 | func DebugTTLVItem(indent int, entity interface{}) string { 45 | var ret bytes.Buffer 46 | ret.WriteString(strings.Repeat(" ", indent)) 47 | if entity == nil { 48 | ret.WriteString("(nil)") 49 | } else { 50 | switch t := entity.(type) { 51 | case *Structure: 52 | ret.WriteString(t.TTL.TTLString()) 53 | ret.WriteRune('\n') 54 | for _, item := range t.Items { 55 | ret.WriteString(DebugTTLVItem(indent+4, item)) 56 | } 57 | case *Integer: 58 | ret.WriteString(fmt.Sprintf("%s - %d", t.TTL.TTLString(), t.Value)) 59 | ret.WriteRune('\n') 60 | case *LongInteger: 61 | ret.WriteString(fmt.Sprintf("%s - %d", t.TTL.TTLString(), t.Value)) 62 | ret.WriteRune('\n') 63 | case *DateTime: 64 | ret.WriteString(fmt.Sprintf("%s - %s", t.TTL.TTLString(), t.Time.Format(time.RFC3339))) 65 | ret.WriteRune('\n') 66 | case *Enumeration: 67 | ret.WriteString(fmt.Sprintf("%s - %d", t.TTL.TTLString(), t.Value)) 68 | ret.WriteRune('\n') 69 | case *Text: 70 | ret.WriteString(fmt.Sprintf("%s - %s", t.TTL.TTLString(), t.Value)) 71 | ret.WriteRune('\n') 72 | case *Bytes: 73 | ret.WriteString(fmt.Sprintf("%s - %s", t.TTL.TTLString(), hex.EncodeToString(t.Value))) 74 | ret.WriteRune('\n') 75 | default: 76 | ret.WriteString(fmt.Sprintf("(Unknown structure) %+v", t)) 77 | ret.WriteRune('\n') 78 | } 79 | } 80 | return ret.String() 81 | } 82 | 83 | // Encode any fixed-size integer into big endian byte array and return. 84 | func EncodeIntBigEndian(someInt interface{}) []byte { 85 | buf := new(bytes.Buffer) 86 | if err := binary.Write(buf, binary.BigEndian, someInt); err != nil { 87 | panic(err) 88 | } 89 | return buf.Bytes() 90 | } 91 | 92 | // Encode any TTLV item. Input must be pointer to item and must not be nil. 93 | func EncodeAny(thing Item) (ret []byte) { 94 | buf := new(bytes.Buffer) 95 | // Tolerate constructed TTLV items that did not carry a type byte 96 | switch t := thing.(type) { 97 | case *Structure: 98 | t.ResetTyp() 99 | t.TTL.WriteTTTo(buf) 100 | buf.Write(EncodeIntBigEndian(int32(t.GetLength()))) 101 | for _, item := range t.Items { 102 | buf.Write(EncodeAny(item)) 103 | } 104 | case *Integer: 105 | t.ResetTyp() 106 | t.TTL.WriteTTTo(buf) 107 | buf.Write(EncodeIntBigEndian(int32(t.GetLength()))) 108 | // Integer has length of 4 109 | buf.Write(EncodeIntBigEndian(t.Value)) 110 | // An additional 4 bytes of padding not counted against length 111 | buf.Write([]byte{0, 0, 0, 0}) 112 | case *LongInteger: 113 | t.ResetTyp() 114 | t.TTL.WriteTTTo(buf) 115 | buf.Write(EncodeIntBigEndian(int32(t.GetLength()))) 116 | // LongInteger has length of 8 117 | buf.Write(EncodeIntBigEndian(t.Value)) 118 | case *Enumeration: 119 | t.ResetTyp() 120 | t.TTL.WriteTTTo(buf) 121 | buf.Write(EncodeIntBigEndian(int32(t.GetLength()))) 122 | // Enumeration has length of 4 123 | buf.Write(EncodeIntBigEndian(t.Value)) 124 | // An additional 4 bytes of padding not counted against length 125 | buf.Write([]byte{0, 0, 0, 0}) 126 | case *DateTime: 127 | t.ResetTyp() 128 | t.TTL.WriteTTTo(buf) 129 | buf.Write(EncodeIntBigEndian(int32(t.GetLength()))) 130 | // DateTime has length of 8 131 | buf.Write(EncodeIntBigEndian(t.Time.Unix())) 132 | case *Text: 133 | t.ResetTyp() 134 | t.TTL.WriteTTTo(buf) 135 | buf.Write(EncodeIntBigEndian(int32(t.GetLength()))) 136 | buf.Write([]byte(t.Value)) 137 | // Pad with zero bytes to line up with 8 138 | padding := make([]byte, RoundUpTo8(len(t.Value))-len(t.Value)) 139 | buf.Write(padding) 140 | case *Bytes: 141 | t.ResetTyp() 142 | t.TTL.WriteTTTo(buf) 143 | buf.Write(EncodeIntBigEndian(int32(t.GetLength()))) 144 | buf.Write(t.Value) 145 | // Pad with zero bytes to line up with 8 146 | padding := make([]byte, RoundUpTo8(len(t.Value))-len(t.Value)) 147 | buf.Write(padding) 148 | default: 149 | log.Panicf("EncodeAny: input is nil or type \"%s\"'s encoder is not implemented", reflect.TypeOf(thing).String()) 150 | } 151 | return buf.Bytes() 152 | } 153 | 154 | // Decode tag, type, and original value length excluding padding from the first several bytes of input buffer. 155 | func DecodeTTL(in []byte) (tag Tag, typ byte, length int32, err error) { 156 | if len(in) < LenTTL { 157 | err = io.EOF 158 | return 159 | } 160 | copy(tag[:], in[:3]) 161 | typ = in[3] 162 | binary.Read(bytes.NewReader(in[4:8]), binary.BigEndian, &length) 163 | return 164 | } 165 | 166 | // Decode any TTLV item and return pointer to item. 167 | func DecodeAny(in []byte) (ret Item, length int, err error) { 168 | tag, typ, length32, err := DecodeTTL(in) 169 | length = int(length32) 170 | if err == io.EOF { 171 | // The condition of reaching end of buffer is not an error 172 | err = nil 173 | return 174 | } else if err != nil { 175 | return 176 | } 177 | if length <= 0 { 178 | return nil, length, fmt.Errorf("DecodeAny: length of type %d must be positive, but it is %d.", typ, length) 179 | } 180 | common := TTL{Tag: tag, Typ: typ, Length: length} 181 | in = in[LenTTL:] 182 | switch typ { 183 | case TypeEnum: 184 | // Value length is defined at 4, but representation uses 8 bytes. 185 | length = 8 186 | enum := &Enumeration{TTL: common} 187 | if err := binary.Read(bytes.NewReader(in[:4]), binary.BigEndian, &enum.Value); err != nil { 188 | return nil, length, fmt.Errorf("DecodeAny: failed to decode %s's value - %v", common.TTLString(), err) 189 | } 190 | ret = enum 191 | case TypInt: 192 | // Value length is defined at 4, but representation uses 8 bytes. 193 | length = 8 194 | integer := &Integer{TTL: common} 195 | if err := binary.Read(bytes.NewReader(in[:4]), binary.BigEndian, &integer.Value); err != nil { 196 | return nil, length, fmt.Errorf("DecodeAny: failed to decode %s's value - %v", common.TTLString(), err) 197 | } 198 | ret = integer 199 | case TypLong: 200 | length = 8 201 | long := &LongInteger{TTL: common} 202 | if err := binary.Read(bytes.NewReader(in[:8]), binary.BigEndian, &long.Value); err != nil { 203 | return nil, length, fmt.Errorf("DecodeAny: failed to decode %s's value - %v", common.TTLString(), err) 204 | } 205 | ret = long 206 | case TypStruct: 207 | in = in[:length] 208 | structure := &Structure{TTL: common, Items: make([]Item, 0, 4)} 209 | itemIndex := 0 210 | for { 211 | // Decode item at current index 212 | item, itemLength, err := DecodeAny(in) 213 | if err != nil { 214 | return nil, length, fmt.Errorf("DecodeAny: failed to decode structure %s's item - %v", common.TTLString(), err) 215 | } 216 | structure.Items = append(structure.Items, item) 217 | // Advance index by the length of decoded TTL plus newly decoded item 218 | itemIndex = LenTTL + itemLength 219 | if itemIndex >= len(in) { 220 | break 221 | } 222 | // Continue processing the next item 223 | in = in[itemIndex:] 224 | } 225 | ret = structure 226 | case TypeText: 227 | // Length in TTL is true string length, excluding padding. 228 | ret = &Text{TTL: common, Value: string(in[:length])} 229 | // Value length is true string length, but representation may contain padding to be divisible by 8. 230 | length = RoundUpTo8(length) 231 | case TypeBytes: 232 | // Length in TTL is true string length, excluding padding. 233 | ret = &Bytes{TTL: common, Value: in[:length]} 234 | // Value length is true string length, but representation may contain padding to be divisible by 8. 235 | length = RoundUpTo8(length) 236 | case TypeDateTime: 237 | length = 8 238 | var longInt int64 239 | if err := binary.Read(bytes.NewReader(in[:8]), binary.BigEndian, &longInt); err != nil { 240 | return nil, length, fmt.Errorf("DecodeAny: failed to decode %s's value - %v", common.TTLString(), err) 241 | } 242 | ret = &DateTime{TTL: common, Time: time.Unix(longInt, 0)} 243 | default: 244 | return nil, length, fmt.Errorf("DecodeAny: does not know how to decode %s's type", common.TTLString()) 245 | } 246 | // Type byte was not directly decoded from input buffer by the switch structure above, hence it is set here. 247 | ret.ResetTyp() 248 | return 249 | } 250 | 251 | // Copy tag, type, and value of a primitive TTLV item from src to dest. Both src and dest are pointers. 252 | func CopyPrimitive(dest, src Item) error { 253 | if src == nil { 254 | return errors.New("CopyPrimitive: source value may not be nil") 255 | } 256 | if dest == nil { 257 | return errors.New("CopyPrimitive: destination value may not be nil") 258 | } 259 | typeErr := fmt.Errorf("CopyPrimitive: was expecting destination to be of type %s, but it is %s.", reflect.TypeOf(src).String(), reflect.TypeOf(dest).String()) 260 | switch t := src.(type) { 261 | case *Integer: 262 | if tDest, yes := dest.(*Integer); yes { 263 | tDest.Tag = t.Tag 264 | tDest.Typ = t.Typ 265 | tDest.Value = t.Value 266 | } else { 267 | return typeErr 268 | } 269 | case *LongInteger: 270 | if tDest, yes := dest.(*LongInteger); yes { 271 | tDest.Tag = t.Tag 272 | tDest.Typ = t.Typ 273 | tDest.Value = t.Value 274 | } else { 275 | return typeErr 276 | } 277 | case *Enumeration: 278 | if tDest, yes := dest.(*Enumeration); yes { 279 | tDest.Tag = t.Tag 280 | tDest.Typ = t.Typ 281 | tDest.Value = t.Value 282 | } else { 283 | return typeErr 284 | } 285 | case *DateTime: 286 | if tDest, yes := dest.(*DateTime); yes { 287 | tDest.Tag = t.Tag 288 | tDest.Typ = t.Typ 289 | tDest.Time = t.Time 290 | } else { 291 | return typeErr 292 | } 293 | case *Text: 294 | if tDest, yes := dest.(*Text); yes { 295 | tDest.Tag = t.Tag 296 | tDest.Typ = t.Typ 297 | tDest.Value = t.Value 298 | } else { 299 | return typeErr 300 | } 301 | case *Bytes: 302 | if tDest, yes := dest.(*Bytes); yes { 303 | tDest.Tag = t.Tag 304 | tDest.Typ = t.Typ 305 | tDest.Value = make([]byte, len(t.Value)) 306 | copy(tDest.Value, t.Value) 307 | } else { 308 | return typeErr 309 | } 310 | default: 311 | return fmt.Errorf("CopyPrimitive: unknown source value type %s", reflect.TypeOf(src).String()) 312 | } 313 | return nil 314 | } 315 | -------------------------------------------------------------------------------- /keydb/db_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keydb 4 | 5 | import ( 6 | "os" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | const TestDBDir = "/tmp/cryptctl-dbtest" 13 | 14 | func TestRecordCRUD(t *testing.T) { 15 | defer os.RemoveAll(TestDBDir) 16 | os.RemoveAll(TestDBDir) 17 | db, err := OpenDB(TestDBDir) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | // Insert two records 22 | aliveMsg := AliveMessage{ 23 | Hostname: "host1", 24 | IP: "ip1", 25 | Timestamp: time.Now().Unix(), 26 | } 27 | rec1 := Record{ 28 | UUID: "1", 29 | Key: []byte{0, 1, 2, 3}, 30 | MountPoint: "/tmp/2", 31 | MountOptions: []string{"rw", "noatime"}, 32 | MaxActive: 1, 33 | AliveIntervalSec: 1, 34 | AliveCount: 4, 35 | AliveMessages: map[string][]AliveMessage{}, 36 | PendingCommands: make(map[string][]PendingCommand), 37 | } 38 | rec1Alive := rec1 39 | rec1Alive.LastRetrieval = aliveMsg 40 | rec1Alive.AliveMessages = map[string][]AliveMessage{aliveMsg.IP: []AliveMessage{aliveMsg}} 41 | rec2 := Record{ 42 | UUID: "2", 43 | Key: []byte{0, 1, 2, 3}, 44 | MountPoint: "/tmp/2", 45 | MountOptions: []string{"rw", "noatime"}, 46 | MaxActive: 1, 47 | AliveIntervalSec: 1, 48 | AliveCount: 4, 49 | AliveMessages: map[string][]AliveMessage{}, 50 | PendingCommands: make(map[string][]PendingCommand), 51 | } 52 | rec2Alive := rec2 53 | rec2Alive.LastRetrieval = aliveMsg 54 | rec2Alive.AliveMessages = map[string][]AliveMessage{aliveMsg.IP: []AliveMessage{aliveMsg}} 55 | if seq, err := db.Upsert(rec1); err != nil || seq != "1" { 56 | t.Fatal(err, seq) 57 | } 58 | if seq, err := db.Upsert(rec2); err != nil || seq != "2" { 59 | t.Fatal(err, seq) 60 | } 61 | // Match sequence number in my copy of records with their should-be ones 62 | rec1.ID = "1" 63 | rec1Alive.ID = "1" 64 | rec2.ID = "2" 65 | rec2Alive.ID = "2" 66 | // Select one record and then select both records 67 | if found, rejected, missing := db.Select(aliveMsg, true, "1", "doesnotexist"); !reflect.DeepEqual(found, map[string]Record{rec1.UUID: rec1Alive}) || 68 | !reflect.DeepEqual(rejected, []string{}) || 69 | !reflect.DeepEqual(missing, []string{"doesnotexist"}) { 70 | t.Fatalf("\n%+v\n%+v\n%+v\n%+v\n", found, map[string]Record{rec1.UUID: rec1Alive}, rejected, missing) 71 | } 72 | if found, rejected, missing := db.Select(aliveMsg, true, "1", "doesnotexist", "2"); !reflect.DeepEqual(found, map[string]Record{rec2.UUID: rec2Alive}) || 73 | !reflect.DeepEqual(rejected, []string{"1"}) || 74 | !reflect.DeepEqual(missing, []string{"doesnotexist"}) { 75 | t.Fatal(found, rejected, missing) 76 | } 77 | if found, rejected, missing := db.Select(aliveMsg, false, "1", "doesnotexist", "2"); !reflect.DeepEqual(found, map[string]Record{rec1.UUID: rec1Alive, rec2.UUID: rec2Alive}) || 78 | !reflect.DeepEqual(rejected, []string{}) || 79 | !reflect.DeepEqual(missing, []string{"doesnotexist"}) { 80 | t.Fatal(found, rejected, missing) 81 | } 82 | // Update alive message on both records 83 | newAlive := AliveMessage{ 84 | Hostname: "host1", 85 | IP: "ip1", 86 | Timestamp: time.Now().Unix(), 87 | } 88 | if rejected := db.UpdateAliveMessage(newAlive, "1", "2", "doesnotexist"); !reflect.DeepEqual(rejected, []string{"doesnotexist"}) { 89 | t.Fatal(rejected) 90 | } 91 | if len(db.RecordsByUUID["1"].AliveMessages["ip1"]) != 2 || len(db.RecordsByUUID["2"].AliveMessages["ip1"]) != 2 { 92 | t.Fatal(db.RecordsByUUID) 93 | } 94 | if len(db.RecordsByID["1"].AliveMessages["ip1"]) != 2 || len(db.RecordsByID["2"].AliveMessages["ip1"]) != 2 { 95 | t.Fatal(db.RecordsByUUID) 96 | } 97 | // Erase a record 98 | if err := db.Erase("doesnotexist"); err == nil { 99 | t.Fatal("did not error") 100 | } 101 | if err := db.Erase(rec1.UUID); err != nil { 102 | t.Fatal(err) 103 | } 104 | if found, rejected, missing := db.Select(aliveMsg, true, "1"); len(found) != 0 || 105 | !reflect.DeepEqual(rejected, []string{}) || 106 | !reflect.DeepEqual(missing, []string{"1"}) { 107 | t.Fatal(found, rejected, missing) 108 | } 109 | // Reload database and test query once more (2 is already retrieved and hence it shall be rejected) 110 | db, err = OpenDB(TestDBDir) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | if found, rejected, missing := db.Select(aliveMsg, true, "1", "2"); len(found) != 0 || 115 | !reflect.DeepEqual(rejected, []string{"2"}) || 116 | !reflect.DeepEqual(missing, []string{"1"}) { 117 | t.Fatal(found, missing) 118 | } 119 | } 120 | 121 | func TestOpenDBOneRecord(t *testing.T) { 122 | defer os.RemoveAll(TestDBDir) 123 | os.RemoveAll(TestDBDir) 124 | db, err := OpenDB(TestDBDir) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | rec := Record{ 129 | UUID: "a", 130 | Key: []byte{1, 2, 3}, 131 | MountPoint: "/a", 132 | MountOptions: []string{}, 133 | LastRetrieval: AliveMessage{ 134 | Hostname: "host1", 135 | IP: "ip1", 136 | Timestamp: 3, 137 | }, 138 | AliveMessages: make(map[string][]AliveMessage), 139 | PendingCommands: make(map[string][]PendingCommand), 140 | } 141 | if seq, err := db.Upsert(rec); err != nil || seq != "1" { 142 | t.Fatal(err) 143 | } 144 | dbOneRecord, err := OpenDBOneRecord(TestDBDir, "a") 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | if len(dbOneRecord.RecordsByUUID) != 1 { 149 | t.Fatal(dbOneRecord.RecordsByUUID) 150 | } 151 | rec.ID = "1" 152 | if recA, found := dbOneRecord.GetByUUID("a"); !found || !reflect.DeepEqual(recA, rec) { 153 | t.Fatal(recA, found) 154 | } 155 | if recA, found := dbOneRecord.GetByID("1"); !found || !reflect.DeepEqual(recA, rec) { 156 | t.Fatal(recA, found) 157 | } 158 | if _, found := dbOneRecord.GetByUUID("doesnotexist"); found { 159 | t.Fatal("false positive") 160 | } 161 | if _, found := dbOneRecord.GetByID("78598123"); found { 162 | t.Fatal("false positive") 163 | } 164 | } 165 | 166 | func TestList(t *testing.T) { 167 | defer os.RemoveAll(TestDBDir) 168 | db, err := OpenDB(TestDBDir) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | // Insert three records and get them back in sorted order 173 | rec1 := Record{ 174 | UUID: "a", 175 | Key: []byte{1, 2, 3}, 176 | MountPoint: "/a", 177 | MountOptions: []string{}, 178 | LastRetrieval: AliveMessage{ 179 | Hostname: "host1", 180 | IP: "ip1", 181 | Timestamp: 3, 182 | }, 183 | AliveMessages: make(map[string][]AliveMessage), 184 | PendingCommands: make(map[string][]PendingCommand), 185 | } 186 | rec1NoKey := rec1 187 | rec1NoKey.Key = nil 188 | rec2 := Record{ 189 | UUID: "b", 190 | Key: []byte{1, 2, 3}, 191 | MountPoint: "/b", 192 | MountOptions: []string{}, 193 | LastRetrieval: AliveMessage{ 194 | Hostname: "host1", 195 | IP: "ip1", 196 | Timestamp: 1, 197 | }, 198 | AliveMessages: make(map[string][]AliveMessage), 199 | PendingCommands: make(map[string][]PendingCommand), 200 | } 201 | rec2NoKey := rec2 202 | rec2NoKey.Key = nil 203 | rec3 := Record{ 204 | UUID: "c", 205 | Key: []byte{1, 2, 3}, 206 | MountPoint: "/c", 207 | MountOptions: []string{}, 208 | LastRetrieval: AliveMessage{ 209 | Hostname: "host1", 210 | IP: "ip1", 211 | Timestamp: 2, 212 | }, 213 | AliveMessages: make(map[string][]AliveMessage), 214 | PendingCommands: make(map[string][]PendingCommand), 215 | } 216 | rec3NoKey := rec3 217 | rec3NoKey.Key = nil 218 | if seq, err := db.Upsert(rec1); err != nil || seq != "1" { 219 | t.Fatal(err, seq) 220 | } 221 | if seq, err := db.Upsert(rec2); err != nil || seq != "2" { 222 | t.Fatal(err) 223 | } 224 | if seq, err := db.Upsert(rec3); err != nil || seq != "3" { 225 | t.Fatal(err) 226 | } 227 | rec1NoKey.ID = "1" 228 | rec2NoKey.ID = "2" 229 | rec3NoKey.ID = "3" 230 | recs := db.List() 231 | if !reflect.DeepEqual(recs[0], rec1NoKey) || 232 | !reflect.DeepEqual(recs[1], rec3NoKey) || 233 | !reflect.DeepEqual(recs[2], rec2NoKey) { 234 | t.Fatal(recs) 235 | } 236 | } 237 | 238 | func TestDB_LoadRecord(t *testing.T) { 239 | defer os.RemoveAll(TestDBDir) 240 | os.RemoveAll(TestDBDir) 241 | // Open identical directory in two database instances 242 | db, err := OpenDB(TestDBDir) 243 | if err != nil { 244 | t.Fatal(err) 245 | } 246 | db2, err := OpenDB(TestDBDir) 247 | if err != nil { 248 | t.Fatal(err) 249 | } 250 | // Create a record in the first database instance 251 | rec := Record{ 252 | UUID: "a", 253 | Key: []byte{1, 2, 3}, 254 | MountPoint: "/a", 255 | MountOptions: []string{}, 256 | LastRetrieval: AliveMessage{ 257 | Hostname: "host1", 258 | IP: "ip1", 259 | Timestamp: 3, 260 | }, 261 | AliveMessages: make(map[string][]AliveMessage), 262 | } 263 | if seq, err := db.Upsert(rec); err != nil || seq != "1" { 264 | t.Fatal(err) 265 | } 266 | // Load the newly created record in the second database instance 267 | if err := db2.ReloadRecord("a"); err != nil { 268 | t.Fatal(err) 269 | } 270 | if rec, found := db2.GetByID("1"); !found || rec.UUID != "a" { 271 | t.Fatal(rec, found) 272 | } 273 | if err := db2.ReloadRecord("doesnotexist"); err == nil { 274 | t.Fatal("did not error") 275 | } 276 | } 277 | 278 | func TestDB_UpdateSeenFlag(t *testing.T) { 279 | defer os.RemoveAll(TestDBDir) 280 | os.RemoveAll(TestDBDir) 281 | db, err := OpenDB(TestDBDir) 282 | if err != nil { 283 | t.Fatal(err) 284 | } 285 | if _, err := db.Upsert(Record{ID: "id1", UUID: "a", Key: []byte{}}); err != nil { 286 | t.Fatal(err) 287 | } 288 | 289 | start := time.Now() 290 | 291 | recA := db.RecordsByUUID["a"] 292 | // Record 1 is valid 293 | recA.AddPendingCommand("1.1.1.1", PendingCommand{ 294 | ValidFrom: start, 295 | Validity: 10 * time.Hour, 296 | IP: "1.1.1.1", 297 | Content: "1st command", 298 | }) 299 | // Record 2 is expired 300 | recA.AddPendingCommand("1.1.1.1", PendingCommand{ 301 | ValidFrom: start.Add(-11 * time.Hour), 302 | Validity: 10 * time.Hour, 303 | IP: "1.1.1.1", 304 | Content: "2nd command", 305 | }) 306 | // Record 3 is valid 307 | recA.AddPendingCommand("2.2.2.2", PendingCommand{ 308 | ValidFrom: start, 309 | Validity: 10 * time.Hour, 310 | IP: "2.2.2.2", 311 | Content: "3rd command", 312 | }) 313 | db.RecordsByUUID["a"] = recA 314 | 315 | db.UpdateSeenFlag("a", "1.1.1.1", "1st command") 316 | db.UpdateCommandResult("a", "1.1.1.1", "2nd command", "success") 317 | db.UpdateCommandResult("a", "2.2.2.2", "3rd command", "failure") 318 | 319 | expected := map[string][]PendingCommand{ 320 | "1.1.1.1": { 321 | { 322 | ValidFrom: start, 323 | Validity: 10 * time.Hour, 324 | IP: "1.1.1.1", 325 | Content: "1st command", 326 | SeenByClient: true, 327 | }, 328 | }, 329 | "2.2.2.2": { 330 | { 331 | ValidFrom: start, 332 | Validity: 10 * time.Hour, 333 | IP: "2.2.2.2", 334 | Content: "3rd command", 335 | SeenByClient: true, 336 | ClientResult: "failure", 337 | }, 338 | }, 339 | } 340 | if !reflect.DeepEqual(expected, db.RecordsByUUID["a"].PendingCommands) { 341 | t.Fatalf("\n%+v\n%+v\n", expected, db.RecordsByUUID["a"].PendingCommands) 342 | } 343 | if !reflect.DeepEqual(expected, db.RecordsByID["id1"].PendingCommands) { 344 | t.Fatalf("\n%+v\n%+v\n", expected, db.RecordsByID["id1"].PendingCommands) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /routine/unlock.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package routine 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "github.com/SUSE/cryptctl/fs" 9 | "github.com/SUSE/cryptctl/keydb" 10 | "github.com/SUSE/cryptctl/keyserv" 11 | "github.com/SUSE/cryptctl/sys" 12 | "io" 13 | "os" 14 | "path" 15 | "time" 16 | ) 17 | 18 | const ( 19 | AUTO_UNLOCK_RETRY_INTERVAL_SEC = 5 20 | REPORT_ALIVE_INTERVAL_SEC = 10 21 | ) 22 | 23 | // Forcibly unlock all file systems that have their keys on a key server. 24 | func ManOnlineUnlockFS(progressOut io.Writer, client *keyserv.CryptClient, password string) error { 25 | sys.LockMem() 26 | // Collect information about all encrypted file systems 27 | blockDevs := fs.GetBlockDevices() 28 | reqUUIDs := make([]string, 0, 0) 29 | reqDevs := make(map[string]fs.BlockDevice) 30 | for _, dev := range blockDevs { 31 | if dev.MountPoint == "" && dev.IsLUKSEncrypted() && dev.UUID != "" { 32 | reqUUIDs = append(reqUUIDs, dev.UUID) 33 | reqDevs[dev.UUID] = dev 34 | } 35 | } 36 | if len(reqUUIDs) == 0 { 37 | return errors.New("Cannot find any more encrypted file systems.") 38 | } 39 | hostname, _ := sys.GetHostnameAndIP() 40 | resp, err := client.ManualRetrieveKey(keyserv.ManualRetrieveKeyReq{ 41 | UUIDs: reqUUIDs, 42 | Hostname: hostname, 43 | PlainPassword: password, 44 | }) 45 | if err != nil { 46 | return err 47 | } 48 | hasErr := false 49 | if len(resp.Granted) > 0 { 50 | // Unlock and mount all disks that have keys on the server 51 | for uuid, rec := range resp.Granted { 52 | fmt.Fprintf(progressOut, "Mounting %s (%s) on %s...\n", reqDevs[uuid].Path, rec.GetMountOptionStr(), rec.MountPoint) 53 | blkDev := reqDevs[uuid].Path 54 | dmName := MakeDeviceMapperName(reqDevs[uuid].Path) 55 | dmDev := path.Join("/dev/mapper/", dmName) 56 | // Resume on error, in case some operations fail due to them being already carried out in previous runs. 57 | if err := fs.CryptOpen(rec.Key, blkDev, dmName); err != nil { 58 | fmt.Fprintf(progressOut, " *%v\n", err) 59 | } 60 | if err := os.MkdirAll(rec.MountPoint, 0755); err != nil { 61 | fmt.Fprintf(progressOut, " *failed to make mount point directory - %v\n", err) 62 | } 63 | // Intentionally ignore this error and let mount inform the user 64 | if err := fs.Mount(dmDev, "", rec.MountOptions, rec.MountPoint); err != nil { 65 | fmt.Fprintf(progressOut, " *%v\n", err) 66 | if blk, found := fs.GetBlockDevice(dmDev); !found || blk.MountPoint != rec.MountPoint { 67 | // Consider that an error has happened only if the encrypted block device is not mounted 68 | hasErr = true 69 | } 70 | } 71 | fmt.Fprintln(progressOut) 72 | } 73 | } 74 | if len(resp.Missing) > 0 { 75 | fmt.Fprintln(progressOut, "The following encrypted file systems do not have their keys on the server:") 76 | for _, uuid := range resp.Missing { 77 | fmt.Fprintf(progressOut, "- %s %s\n", reqDevs[uuid].Path, uuid) 78 | } 79 | } 80 | if hasErr { 81 | return errors.New("Failed to process some of the encrypted file systems. Check output for more details.") 82 | } 83 | return nil 84 | } 85 | 86 | // Unlock a single file systems using a key record file. 87 | func UnlockFS(progressOut io.Writer, rec keydb.Record, maxAttempts int) error { 88 | // Collect information from all encrypted file systems 89 | blockDevs := fs.GetBlockDevices() 90 | reqUUIDs := make([]string, 0, 0) 91 | reqDevs := make(map[string]fs.BlockDevice) 92 | for _, dev := range blockDevs { 93 | if dev.MountPoint == "" && dev.IsLUKSEncrypted() && dev.UUID != "" { 94 | reqUUIDs = append(reqUUIDs, dev.UUID) 95 | reqDevs[dev.UUID] = dev 96 | } 97 | } 98 | // See if the record can unlock any file system 99 | unlockDev, found := reqDevs[rec.UUID] 100 | if !found { 101 | return errors.New("The record does not belong to any encrypted file system on this computer (UUID mismatch).") 102 | } 103 | // Mount the encrypted file system 104 | // Resume on error, in case some operations fail due to them being already carried out in previous runs. 105 | dmName := MakeDeviceMapperName(unlockDev.Path) 106 | dmDev := path.Join("/dev/mapper/", dmName) 107 | /* 108 | Due to race conditions in kernel it is possible for an attempt to fail without apparent reason. 109 | The fs.GetBlockDevice function is especially fragile in this regard, sometimes it cannot see a freshly 110 | mounted file system. 111 | Sleep a second between retries. 112 | */ 113 | var succeeded bool 114 | for i := 0; i < maxAttempts; i++ { 115 | if err := fs.CryptOpen(rec.Key, unlockDev.Path, dmName); err != nil { 116 | fmt.Fprintf(progressOut, " *%v\n", err) 117 | } 118 | if err := os.MkdirAll(rec.MountPoint, 0755); err != nil { 119 | fmt.Fprintf(progressOut, " *failed to make mount point directory - %v\n", err) 120 | } 121 | if err := fs.Mount(dmDev, "", rec.MountOptions, rec.MountPoint); err != nil { 122 | fmt.Fprintf(progressOut, " *%v\n", err) 123 | } 124 | // Ultimate success is determined by the appearance of mount point instead of the commands above 125 | if blk, found := fs.GetBlockDevice(dmDev); found && blk.MountPoint == rec.MountPoint { 126 | succeeded = true 127 | break 128 | } 129 | time.Sleep(1 * time.Second) 130 | } 131 | if succeeded { 132 | fmt.Fprintf(progressOut, "The encrypted file system has been successfully mounted on \"%s\".\n", rec.MountPoint) 133 | } else { 134 | return errors.New("Failed to process the encrypted file system. Check output for more details.") 135 | } 136 | return nil 137 | } 138 | 139 | /* 140 | Make continuous attempts to retrieve encryption key from key server to unlock a file system specified by the UUID. 141 | If maxRetrySec is zero or negative, then only one attempt will be made to unlock the file system. 142 | */ 143 | func AutoOnlineUnlockFS(progressOut io.Writer, client *keyserv.CryptClient, uuid string, maxRetrySec int64) error { 144 | sys.LockMem() 145 | // Find out UUID of the block device 146 | blkDevs := fs.GetBlockDevices() 147 | blkDev, found := blkDevs.GetByCriteria(uuid, "", "", "", "", "", "") 148 | if !found { 149 | return fmt.Errorf("AutoOnlineUnlockFS: failed to get information of \"%s\"", uuid) 150 | } else if !blkDev.IsLUKSEncrypted() { 151 | fmt.Fprintf(progressOut, "AutoOnlineUnlockFS: skip \"%s\" as it is not a LUKS-encrypted block device\n", uuid) 152 | return nil 153 | } 154 | // Keep trying until maxRetrySec elapses 155 | numFailures := 0 156 | begin := time.Now().Unix() 157 | for { 158 | // Always send the up-to-date hostname in RPC request 159 | hostname, _ := sys.GetHostnameAndIP() 160 | resp, err := client.AutoRetrieveKey(keyserv.AutoRetrieveKeyReq{ 161 | Hostname: hostname, 162 | UUIDs: []string{blkDev.UUID}, 163 | }) 164 | if err == nil { 165 | rec, exists := resp.Granted[blkDev.UUID] 166 | if exists { 167 | // Key has been granted by server, proceed to unlock disk. 168 | return UnlockFS(progressOut, rec, 3) 169 | } 170 | if len(resp.Missing) > 0 { 171 | // Stop trying if the server does not even have the key 172 | return fmt.Errorf("AutoOnlineUnlockFS: server does not have encryption key for \"%s\"", blkDev.UUID) 173 | } 174 | } 175 | // Server may have rejected the key request due to MaxActive being exceeded 176 | if len(resp.Rejected) > 0 { 177 | err = errors.New("MaxActive is exceeded") 178 | } 179 | // Retry the operation for a while 180 | if time.Now().Unix() > begin+maxRetrySec { 181 | return fmt.Errorf("AutoOnlineUnlockFS: failed to unlock \"%s\" (%v) and have given up after %d seconds", 182 | blkDev.UUID, err, maxRetrySec) 183 | } 184 | // In case of failure, only report the first few occasions among consecutive failures. 185 | if err != nil { 186 | if numFailures == 5 { 187 | fmt.Fprint(progressOut, "AutoOnlineUnlockFS: suppress further failure messages until success\n") 188 | } else if numFailures < 5 { 189 | fmt.Fprintf(progressOut, "AutoOnlineUnlockFS: failed to unlock \"%s\", will retry in %d seconds - %v\n", 190 | blkDev.UUID, AUTO_UNLOCK_RETRY_INTERVAL_SEC, err) 191 | } 192 | numFailures++ 193 | } 194 | time.Sleep(AUTO_UNLOCK_RETRY_INTERVAL_SEC * time.Second) 195 | } 196 | } 197 | 198 | /* 199 | Continuously send alive reports to server to indicate that this computer is still holding onto the encrypted disk. 200 | Block caller until the program quits or server rejects this computer. 201 | */ 202 | func ReportAlive(progressOut io.Writer, client *keyserv.CryptClient, uuid string) error { 203 | fmt.Fprintf(progressOut, "ReportAlive: begin sending messages for encrypted disk \"%s\"\n", uuid) 204 | numFailures := 0 205 | for { 206 | // Always send the up-to-date hostname in RPC request 207 | hostname, _ := sys.GetHostnameAndIP() 208 | rejected, err := client.ReportAlive(keyserv.ReportAliveReq{ 209 | Hostname: hostname, 210 | UUIDs: []string{uuid}, 211 | }) 212 | if len(rejected) > 0 { 213 | return fmt.Errorf("ReportAlive: stop sending messages for disk \"%s\" because server has rejected it", uuid) 214 | } 215 | // In case of failure, only report the first few occasions among consecutive failures. 216 | if err == nil { 217 | if numFailures > 0 { 218 | fmt.Fprintf(progressOut, "ReportAlive: succeeded for disk \"%s\"\n", uuid) 219 | } 220 | numFailures = 0 221 | } else { 222 | if numFailures == 5 { 223 | fmt.Fprint(progressOut, "ReportAlive: suppress further failure messages until next success\n") 224 | } else if numFailures < 5 { 225 | fmt.Fprintf(progressOut, "ReportAlive: failed to send message for disk \"%s\" - %v\n", uuid, err) 226 | } 227 | numFailures++ 228 | } 229 | time.Sleep(REPORT_ALIVE_INTERVAL_SEC * time.Second) 230 | } 231 | } 232 | 233 | /* 234 | Erase encryption metadata on the specified disk, and then ask server to erase its key. 235 | This process renders all data on the disk irreversibly lost. 236 | */ 237 | func EraseKey(progressOut io.Writer, client *keyserv.CryptClient, password, uuid string) error { 238 | // Find the device node and erase the encryption metadata 239 | blkDevs := fs.GetBlockDevices() 240 | hostDev, foundHost := blkDevs.GetByCriteria(uuid, "", "", "", "", "", "") 241 | if !foundHost { 242 | return fmt.Errorf("EraseKey: cannot find a block device corresponding to UUID \"%s\"", uuid) 243 | } 244 | unlockedDevPath := MakeDeviceMapperName(hostDev.Path) 245 | unlockedDev, foundUnlocked := blkDevs.GetByCriteria("", path.Join("/dev/mapper", unlockedDevPath), "", "", "", "", "") 246 | if foundUnlocked { 247 | // Unmount and close it before erasing the data 248 | if unlockedDev.MountPoint != "" { 249 | fmt.Fprintf(progressOut, "Umounting \"%s\"...\n", unlockedDev.MountPoint) 250 | if err := fs.Umount(unlockedDev.MountPoint); err != nil { 251 | return err 252 | } 253 | } 254 | fmt.Fprintf(progressOut, "Closing \"%s\"...\n", unlockedDevPath) 255 | if err := fs.CryptClose(unlockedDevPath); err != nil { 256 | return err 257 | } 258 | } 259 | if err := fs.CryptErase(hostDev.Path); err != nil { 260 | return err 261 | } 262 | // After metadata is erased, ask server to remove its key record as well. 263 | hostname, _ := sys.GetHostnameAndIP() 264 | if err := client.EraseKey(keyserv.EraseKeyReq{ 265 | PlainPassword: password, 266 | Hostname: hostname, 267 | UUID: uuid}); err != nil { 268 | return err 269 | } 270 | fmt.Fprintf(progressOut, "Encryption header has been wiped successfully, data in \"%s\" (%s) is now irreversibly lost.\n", 271 | uuid, hostDev.Path) 272 | return nil 273 | } 274 | -------------------------------------------------------------------------------- /keyserv/rpc_client_test.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keyserv 4 | 5 | import ( 6 | "fmt" 7 | "github.com/SUSE/cryptctl/keydb" 8 | "github.com/SUSE/cryptctl/sys" 9 | "path" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestCreateKeyReq_Validate(t *testing.T) { 18 | req := CreateKeyReq{} 19 | if err := req.Validate(); err == nil || !strings.Contains(err.Error(), "UUID must not be empty") { 20 | t.Fatal(err) 21 | } 22 | req.UUID = "/root/../a-" 23 | if err := req.Validate(); err == nil || !strings.Contains(err.Error(), "illegal chara") { 24 | t.Fatal(err) 25 | } 26 | req.UUID = "abc-def-123-ghi" 27 | if err := req.Validate(); err == nil || !strings.Contains(err.Error(), "Mount point") { 28 | t.Fatal(err) 29 | } 30 | req.MountPoint = "/a" 31 | if err := req.Validate(); err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestRPCCalls(t *testing.T) { 37 | client, _, tearDown := StartTestServer(t) 38 | defer tearDown(t) 39 | if err := client.Ping(PingRequest{PlainPassword: "wrong password")); err == nil { 40 | t.Fatal("did not error") 41 | } 42 | if err := client.Ping(PingRequest{PlainPassword: TEST_RPC_PASS}); err != nil { 43 | t.Fatal(err) 44 | } 45 | // Construct a client via sysconfig 46 | scClientConf, _ := sys.ParseSysconfig("") 47 | scClientConf.Set(CLIENT_CONF_HOST, "localhost") 48 | scClientConf.Set(CLIENT_CONF_PORT, strconv.Itoa(SRV_DEFAULT_PORT)) 49 | scClientConf.Set(CLIENT_CONF_CA, path.Join(PkgInGopath, "keyserv", "rpc_test.crt")) 50 | scClient, err := NewCryptClientFromSysconfig(scClientConf) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | if err := scClient.Ping(PingRequest{PlainPassword: TEST_RPC_PASS}); err != nil { 55 | t.Fatal(err) 56 | } 57 | // Refuse to save a key if password is incorrect 58 | createResp, err := client.CreateKey(CreateKeyReq{ 59 | PlainPassword: "wrong password", 60 | Hostname: "localhost", 61 | UUID: "aaa", 62 | MountPoint: "/a", 63 | MountOptions: []string{"ro", "noatime"}, 64 | MaxActive: 1, 65 | AliveIntervalSec: 1, 66 | AliveCount: 4, 67 | }) 68 | if err == nil { 69 | t.Fatal("did not error") 70 | } 71 | // Save two good keys 72 | createResp, err = client.CreateKey(CreateKeyReq{ 73 | PlainPassword: TEST_RPC_PASS, 74 | Hostname: "localhost", 75 | UUID: "aaa", 76 | MountPoint: "/a", 77 | MountOptions: []string{"ro", "noatime"}, 78 | MaxActive: 1, 79 | AliveIntervalSec: 1, 80 | AliveCount: 4, 81 | }) 82 | if err != nil || len(createResp.KeyContent) != KMIPAESKeySizeBits/8 { 83 | t.Fatal(err) 84 | } 85 | createResp, err = client.CreateKey(CreateKeyReq{ 86 | PlainPassword: TEST_RPC_PASS, 87 | Hostname: "localhost", 88 | UUID: "bbb", 89 | MountPoint: "/b", 90 | MountOptions: []string{"ro", "noatime"}, 91 | MaxActive: 0, 92 | AliveIntervalSec: 1, 93 | AliveCount: 4, 94 | }) 95 | if err != nil || len(createResp.KeyContent) != KMIPAESKeySizeBits/8 { 96 | t.Fatal(err) 97 | } 98 | // Retrieve both keys via automated retrieval without password 99 | autoRetrieveResp, err := client.AutoRetrieveKey(AutoRetrieveKeyReq{ 100 | UUIDs: []string{"aaa", "bbb", "does_not_exist"}, 101 | Hostname: "localhost", 102 | }) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | if len(autoRetrieveResp.Granted) != 2 || len(autoRetrieveResp.Rejected) != 0 || !reflect.DeepEqual(autoRetrieveResp.Missing, []string{"does_not_exist"}) { 107 | t.Fatal(autoRetrieveResp.Granted, autoRetrieveResp.Rejected, autoRetrieveResp.Missing) 108 | } 109 | if len(autoRetrieveResp.Granted["aaa"].Key) != KMIPAESKeySizeBits/8 || len(autoRetrieveResp.Granted["bbb"].Key) != KMIPAESKeySizeBits/8 { 110 | t.Fatal(autoRetrieveResp.Granted) 111 | } 112 | verifyKeyA := func(recA keydb.Record) { 113 | if recA.UUID != "aaa" || recA.MountPoint != "/a" || 114 | !reflect.DeepEqual(recA.MountOptions, []string{"ro", "noatime"}) || recA.AliveIntervalSec != 1 || recA.AliveCount != 4 || 115 | recA.LastRetrieval.Timestamp == 0 || recA.LastRetrieval.Hostname == "" || recA.LastRetrieval.IP == "" || 116 | len(recA.AliveMessages) != 1 { 117 | t.Fatal(recA) 118 | } 119 | for _, hostAliveMessages := range recA.AliveMessages { 120 | if len(hostAliveMessages) != 1 { 121 | t.Fatal(recA) 122 | } 123 | } 124 | } 125 | verifyKeyB := func(recB keydb.Record) { 126 | if recB.UUID != "bbb" || recB.MountPoint != "/b" || 127 | !reflect.DeepEqual(recB.MountOptions, []string{"ro", "noatime"}) || recB.AliveIntervalSec != 1 || recB.AliveCount != 4 || 128 | recB.LastRetrieval.Timestamp == 0 || recB.LastRetrieval.Hostname == "" || recB.LastRetrieval.IP == "" || 129 | len(recB.AliveMessages) != 1 { 130 | t.Fatal(recB) 131 | } 132 | for _, hostAliveMessages := range recB.AliveMessages { 133 | if len(hostAliveMessages) != 1 { 134 | t.Fatal(recB) 135 | } 136 | } 137 | } 138 | verifyKeyA(autoRetrieveResp.Granted["aaa"]) 139 | verifyKeyB(autoRetrieveResp.Granted["bbb"]) 140 | 141 | // Retrieve a key for a second time should be checked against MaxActive limit 142 | autoRetrieveResp, err = client.AutoRetrieveKey(AutoRetrieveKeyReq{ 143 | UUIDs: []string{"aaa", "bbb", "does_not_exist"}, 144 | Hostname: "localhost", 145 | }) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | if len(autoRetrieveResp.Granted) != 1 || !reflect.DeepEqual(autoRetrieveResp.Rejected, []string{"aaa"}) || !reflect.DeepEqual(autoRetrieveResp.Missing, []string{"does_not_exist"}) { 150 | t.Fatal(autoRetrieveResp.Granted, autoRetrieveResp.Rejected, autoRetrieveResp.Missing) 151 | } 152 | verifyKeyB(autoRetrieveResp.Granted["bbb"]) 153 | 154 | // Forcibly retrieve both keys and verify 155 | if _, err := client.ManualRetrieveKey(ManualRetrieveKeyReq{ 156 | PlainPassword: "wrong password", 157 | UUIDs: []string{"aaa"}, 158 | Hostname: "localhost", 159 | }); err == nil { 160 | t.Fatal("did not error") 161 | } 162 | manResp, err := client.ManualRetrieveKey(ManualRetrieveKeyReq{ 163 | PlainPassword: TEST_RPC_PASS, 164 | UUIDs: []string{"aaa", "bbb", "does_not_exist"}, 165 | Hostname: "localhost", 166 | }) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | if len(manResp.Granted) != 2 || !reflect.DeepEqual(manResp.Missing, []string{"does_not_exist"}) { 171 | t.Fatal(manResp.Granted, manResp.Missing) 172 | } 173 | verifyKeyA(manResp.Granted["aaa"]) 174 | verifyKeyB(manResp.Granted["bbb"]) 175 | 176 | // Keep updating alive messages 177 | for i := 0; i < 8; i++ { 178 | if rejected, err := client.ReportAlive(ReportAliveReq{ 179 | Hostname: "localhost", 180 | UUIDs: []string{"aaa", "bbb"}, 181 | }); err != nil || len(rejected) > 0 { 182 | t.Fatal(err, rejected) 183 | } 184 | time.Sleep(1 * time.Second) 185 | } 186 | 187 | // Delete key 188 | if err := client.EraseKey(EraseKeyReq{ 189 | PlainPassword: "wrong password", 190 | Hostname: "localhost", 191 | UUID: "aaa", 192 | }); err == nil { 193 | t.Fatal("did not error") 194 | } 195 | // Erasing a non-existent key should not result in an error 196 | if err := client.EraseKey(EraseKeyReq{ 197 | PlainPassword: TEST_RPC_PASS, 198 | Hostname: "localhost", 199 | UUID: "doesnotexist", 200 | }); err != nil { 201 | t.Fatal(err) 202 | } 203 | if err := client.EraseKey(EraseKeyReq{ 204 | PlainPassword: TEST_RPC_PASS, 205 | Hostname: "localhost", 206 | UUID: "aaa", 207 | }); err != nil { 208 | t.Fatal(err) 209 | } 210 | // Erasing a non-existent key should not result in an error 211 | if err := client.EraseKey(EraseKeyReq{ 212 | PlainPassword: TEST_RPC_PASS, 213 | Hostname: "localhost", 214 | UUID: "aaa", 215 | }); err != nil { 216 | t.Fatal(err) 217 | } 218 | fmt.Println("About to run teardown") 219 | } 220 | 221 | func TestPendingCommands(t *testing.T) { 222 | client, server, tearDown := StartTestServer(t) 223 | defer tearDown(t) 224 | // Create a key that will host pending commands 225 | client.CreateKey(CreateKeyReq{ 226 | PlainPassword: TEST_RPC_PASS, 227 | Hostname: "localhost", 228 | UUID: "a-a-a-a", 229 | MountPoint: "/", 230 | MountOptions: []string{}, 231 | MaxActive: 1, 232 | AliveIntervalSec: 1, 233 | AliveCount: 1, 234 | }) 235 | // Initially, there are no pending commands to be polled. 236 | cmds, err := client.PollCommand(PollCommandReq{ 237 | UUIDs: []string{"a-a-a-a", "this-does-not-exist"}, 238 | }) 239 | if err != nil || len(cmds.Commands) > 0 { 240 | t.Fatal(err, cmds.Commands) 241 | } 242 | // Save four pending commands - first command is still valid and unseen 243 | rec, _ := server.KeyDB.GetByUUID("a-a-a-a") 244 | cmd1 := keydb.PendingCommand{ 245 | ValidFrom: time.Now(), 246 | Validity: 10 * time.Hour, 247 | IP: "127.0.0.1", 248 | Content: "1", 249 | } 250 | rec.AddPendingCommand("127.0.0.1", cmd1) 251 | // Second command is expired 252 | rec.AddPendingCommand("127.0.0.1", keydb.PendingCommand{ 253 | ValidFrom: time.Now().Add(-1 * time.Hour), 254 | Validity: 1 * time.Minute, 255 | IP: "127.0.0.1", 256 | Content: "2", 257 | }) 258 | // Third command is valid but already seen 259 | rec.AddPendingCommand("127.0.0.1", keydb.PendingCommand{ 260 | ValidFrom: time.Now(), 261 | Validity: 10 * time.Hour, 262 | IP: "127.0.0.1", 263 | Content: "3", 264 | SeenByClient: true, 265 | }) 266 | // Fouth command has nothing to do with this computer 267 | rec.AddPendingCommand("another-computer", keydb.PendingCommand{ 268 | ValidFrom: time.Now(), 269 | Validity: 10 * time.Hour, 270 | IP: "another-computer", 271 | Content: "4", 272 | }) 273 | if _, err := server.KeyDB.Upsert(rec); err != nil { 274 | t.Fatal(err) 275 | } 276 | // Poll action should only receive the first command - valid yet unseen 277 | cmds, err = client.PollCommand(PollCommandReq{ 278 | UUIDs: []string{"a-a-a-a", "this-does-not-exist"}, 279 | }) 280 | if err != nil || len(cmds.Commands) != 1 { 281 | t.Fatal(err, cmds.Commands) 282 | } 283 | if !reflect.DeepEqual(cmds.Commands["a-a-a-a"], []keydb.PendingCommand{cmd1}) { 284 | t.Fatalf("\n%+v\n%+v\n", []keydb.PendingCommand{cmd1}, cmds.Commands) 285 | } 286 | // Polled command is now marked as seen 287 | rec, _ = server.KeyDB.GetByUUID("a-a-a-a") 288 | if cmd1 := rec.PendingCommands["127.0.0.1"][0]; !cmd1.SeenByClient { 289 | t.Fatal(cmd1) 290 | } 291 | // There are no more commands to be polled 292 | cmds, err = client.PollCommand(PollCommandReq{ 293 | UUIDs: []string{"a-a-a-a", "this-does-not-exist"}, 294 | }) 295 | if err != nil || len(cmds.Commands) > 0 { 296 | t.Fatal(err, cmds.Commands) 297 | } 298 | // Record a result for a still valid command 299 | if err := client.SaveCommandResult(SaveCommandResultReq{ 300 | UUID: "a-a-a-a", 301 | CommandContent: "1", 302 | Result: "result 1", 303 | }); err != nil { 304 | t.Fatal(err) 305 | } 306 | rec, _ = server.KeyDB.GetByUUID("a-a-a-a") 307 | if cmd1 := rec.PendingCommands["127.0.0.1"][0]; !cmd1.SeenByClient || cmd1.ClientResult != "result 1" { 308 | t.Fatal(cmd1) 309 | } 310 | // Saving result for a non-existent command should not crash anything 311 | if err := client.SaveCommandResult(SaveCommandResultReq{ 312 | UUID: "a-a-a-a", 313 | CommandContent: "does-not-exist", 314 | Result: "dummy-result", 315 | }); err != nil { 316 | t.Fatal(err) 317 | } 318 | // All expired commands should have been cleared when a command result was saved, only cmd1 and cmd3 are left. 319 | if len(rec.PendingCommands["127.0.0.1"]) != 2 { 320 | t.Fatal(rec.PendingCommands) 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /keydb/record.go: -------------------------------------------------------------------------------- 1 | // cryptctl - Copyright (c) 2017 SUSE Linux GmbH, Germany 2 | // This source code is licensed under GPL version 3 that can be found in LICENSE file. 3 | package keydb 4 | 5 | import ( 6 | "bytes" 7 | "encoding/gob" 8 | "errors" 9 | "fmt" 10 | "regexp" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | CurrentRecordVersion = 2 // CurrentRecordVersion is the version of new database records to be created by cryptctl. 17 | ) 18 | 19 | var RegexUUID = regexp.MustCompile("^[a-zA-Z0-9-]+$") // RegexUUID matches characters that are allowed in a UUID 20 | 21 | /* 22 | ValidateUUID returns an error only if the input string is empty, or if there are illegal 23 | characters among the input. 24 | */ 25 | func ValidateUUID(in string) error { 26 | if in == "" { 27 | return errors.New("ValidateUUID: UUID must not be empty") 28 | } else if !RegexUUID.MatchString(in) { 29 | return errors.New("ValidateUUID: illegal characters appeared in UUID") 30 | } 31 | return nil 32 | } 33 | 34 | /* 35 | AliveMessage is a component of key database record, it represents a heartbeat sent by a computer who is actively 36 | using an encryption key - i.e. the encrypted disk is currently unlocked and online. 37 | */ 38 | type AliveMessage struct { 39 | Hostname string // Hostname is the host name reported by client computer itself. 40 | IP string // IP is the client computer's IP as seen by cryptctl server. 41 | Timestamp int64 // Timestamp is the moment the message arrived at cryptctl server. 42 | } 43 | 44 | // PendingCommand is a time-restricted command issued by cryptctl server administrator to be polled by a client. 45 | type PendingCommand struct { 46 | ValidFrom time.Time // ValidFrom is the timestamp at which moment the command was created. 47 | Validity time.Duration // Validity determines the point in time the command expires. Expired commands disappear almost immediately. 48 | IP string // IP is the client computer's IP the command is issued to. 49 | Content interface{} // Content is the command content, serialised and transmitted between server and client. 50 | SeenByClient bool // SeenByClient is updated to true via RPC once the client has seen this command. 51 | ClientResult string // ClientResult is updated via RPC once client has finished executing this command. 52 | } 53 | 54 | // IsValid returns true only if the command has not expired. 55 | func (cmd *PendingCommand) IsValid() bool { 56 | return cmd.ValidFrom.Add(cmd.Validity).Unix() > time.Now().Unix() 57 | } 58 | 59 | /* 60 | A key record that knows all about the encrypted file system, its mount point, and unlocking keys. 61 | When stored on disk, the record resides in a file encoded in gob. 62 | The binary encoding method is intentionally chosen to deter users from manually editing the files on disk. 63 | */ 64 | type Record struct { 65 | ID string // ID is assigned by KMIP server for the encryption key. 66 | Version int // Version is the version number of this record. Outdated records are automatically upgraded. 67 | CreationTime time.Time // CreationTime is the timestamp at which the record was created. 68 | Key []byte // Key is the disk encryption key if the key is not stored on an external KMIP server. 69 | 70 | UUID string // UUID is the block device UUID of the file system. 71 | MountPoint string // MountPoint is the location (directory) where this file system is expected to be mounted to. 72 | MountOptions []string // MountOptions is a string array of mount options specific to the file system. 73 | 74 | MaxActive int // MaxActive is the maximum simultaneous number of online users (computers) for the key, or <=0 for unlimited. 75 | AliveIntervalSec int // AliveIntervalSec is interval in seconds that all key users (computers) should report they're online. 76 | AliveCount int // AliveCount is number of times a key user (computer) can miss regular report and be considered offline. 77 | 78 | LastRetrieval AliveMessage // LastRetrieval is the computer who most recently successfully retrieved the key. 79 | AliveMessages map[string][]AliveMessage // AliveMessages are the most recent alive reports in IP - message array pairs. 80 | PendingCommands map[string][]PendingCommand // PendingCommands are some command to be periodcally polled by clients carrying the IP address (keys). 81 | } 82 | 83 | // Return mount options in a single string, as accepted by mount command. 84 | func (rec *Record) GetMountOptionStr() string { 85 | return strings.Join(rec.MountOptions, ",") 86 | } 87 | 88 | // Determine whether a host is still alive according to recent alive messages. 89 | func (rec *Record) IsHostAlive(hostIP string) (alive bool, finalMessage AliveMessage) { 90 | if beat, found := rec.AliveMessages[hostIP]; found { 91 | if len(beat) == 0 { 92 | // Should not happen 93 | return false, AliveMessage{} 94 | } 95 | finalMessage = beat[len(beat)-1] 96 | alive = finalMessage.Timestamp >= time.Now().Unix()-int64(rec.AliveIntervalSec*rec.AliveCount) 97 | } 98 | return 99 | } 100 | 101 | // Remove all dead hosts from alive message history, return each dead host's final alive . 102 | func (rec *Record) RemoveDeadHosts() (deadFinalMessage map[string]AliveMessage) { 103 | deadFinalMessage = make(map[string]AliveMessage) 104 | deadIPs := make([]string, 0, 8) 105 | for hostIP := range rec.AliveMessages { 106 | if alive, finalMessage := rec.IsHostAlive(hostIP); !alive { 107 | deadFinalMessage[hostIP] = finalMessage 108 | deadIPs = append(deadIPs, hostIP) 109 | } 110 | } 111 | // Remove dead IPs 112 | for _, deadIP := range deadIPs { 113 | delete(rec.AliveMessages, deadIP) 114 | } 115 | return 116 | } 117 | 118 | // RemoveDeadPendingCommands removes pending commands and results that were made 10x validity period in the past. 119 | func (rec *Record) RemoveExpiredPendingCommands() { 120 | ipToDelete := make([]string, 0, 0) 121 | for ip, commands := range rec.PendingCommands { 122 | remainingCommands := make([]PendingCommand, 0, len(commands)) 123 | for _, cmd := range commands { 124 | if cmd.IsValid() { 125 | remainingCommands = append(remainingCommands, cmd) 126 | } 127 | } 128 | if len(remainingCommands) > 0 { 129 | rec.PendingCommands[ip] = remainingCommands 130 | } else { 131 | ipToDelete = append(ipToDelete, ip) 132 | } 133 | } 134 | for _, ip := range ipToDelete { 135 | delete(rec.PendingCommands, ip) 136 | } 137 | } 138 | 139 | // AddPendingCommand stores a command associated to the input IP address, and clears expired pending commands along the way. 140 | func (rec *Record) AddPendingCommand(ip string, cmd PendingCommand) { 141 | rec.RemoveExpiredPendingCommands() 142 | if _, found := rec.PendingCommands[ip]; !found { 143 | rec.PendingCommands[ip] = make([]PendingCommand, 0, 4) 144 | } 145 | rec.PendingCommands[ip] = append(rec.PendingCommands[ip], cmd) 146 | } 147 | 148 | // ClearPendingCommands removes all pending commands, and clears expired pending commands along the way. 149 | func (rec *Record) ClearPendingCommands() { 150 | rec.PendingCommands = make(map[string][]PendingCommand) 151 | } 152 | 153 | /* 154 | If number of maximum active users must be enforced, determine number of active key users from alive message history - 155 | if the maximum number is not yet exceeded, update last retrieval information and alive message history for the host; 156 | if maximum number is already met, the last retrieval information and alive message history are left untouched. 157 | 158 | If number of maximum active users is not enforced, the last retrieval information and alive message history are 159 | unconditionally updated. 160 | */ 161 | func (rec *Record) UpdateLastRetrieval(latestBeat AliveMessage, checkMaxActive bool) (updateOK bool, 162 | deadFinalMessage map[string]AliveMessage) { 163 | // Remove dead hosts before checking number of active key users 164 | deadFinalMessage = rec.RemoveDeadHosts() 165 | if checkMaxActive && rec.MaxActive > 0 && len(rec.AliveMessages) >= rec.MaxActive { 166 | updateOK = false 167 | return 168 | } 169 | rec.LastRetrieval = latestBeat 170 | rec.AliveMessages[latestBeat.IP] = make([]AliveMessage, 0, rec.AliveCount) 171 | rec.AliveMessages[latestBeat.IP] = append(rec.AliveMessages[latestBeat.IP], latestBeat) 172 | updateOK = true 173 | return 174 | } 175 | 176 | // Record the latest alive message in message history. 177 | func (rec *Record) UpdateAliveMessage(latestBeat AliveMessage) bool { 178 | if beats, found := rec.AliveMessages[latestBeat.IP]; found { 179 | if len(beats) >= rec.AliveCount { 180 | // Remove the oldest message and push the latest one to the end 181 | rec.AliveMessages[latestBeat.IP] = append(beats[len(beats)-rec.AliveCount+1:], latestBeat) 182 | } else { 183 | // Simply append the latest one to the end 184 | rec.AliveMessages[latestBeat.IP] = append(beats, latestBeat) 185 | } 186 | return true 187 | } 188 | return false 189 | } 190 | 191 | // Return an error if a record attribute does not make sense. 192 | func (rec *Record) Validate() error { 193 | if len(rec.UUID) < 3 { 194 | return fmt.Errorf("UUID \"%s\" looks too short", rec.UUID) 195 | } 196 | if len(rec.Key) < 3 { 197 | return fmt.Errorf("Key looks too short (%d bytes)", len(rec.Key)) 198 | } 199 | if len(rec.MountPoint) < 2 { 200 | return fmt.Errorf("Mount point \"%s\" looks too short", rec.MountPoint) 201 | } 202 | if rec.AliveIntervalSec < 1 { 203 | return fmt.Errorf("AliveIntervalSec is %d but it should be a positive integer", rec.AliveIntervalSec) 204 | } 205 | if rec.AliveCount < 1 { 206 | return fmt.Errorf("AliveCount is %d but it should be a positive integer", rec.AliveCount) 207 | } 208 | return nil 209 | } 210 | 211 | // Initialise all nil attributes. 212 | func (rec *Record) FillBlanks() { 213 | if rec.Key == nil { 214 | rec.Key = []byte{} 215 | } 216 | if rec.MountOptions == nil { 217 | rec.MountOptions = []string{} 218 | } 219 | if rec.AliveMessages == nil { 220 | rec.AliveMessages = make(map[string][]AliveMessage) 221 | } 222 | } 223 | 224 | // Serialise the record into binary content using gob encoding. 225 | func (rec *Record) Serialise() []byte { 226 | rec.FillBlanks() 227 | var buf bytes.Buffer 228 | if err := gob.NewEncoder(&buf).Encode(rec); err != nil { 229 | // Shall not happen 230 | panic(fmt.Errorf("Serialise: failed to encode gob for record %s - %v", rec.UUID, err)) 231 | } 232 | return buf.Bytes() 233 | } 234 | 235 | // Deserialise record from input binary content using gob encoding. 236 | func (rec *Record) Deserialise(in []byte) error { 237 | rec.FillBlanks() 238 | if err := gob.NewDecoder(bytes.NewReader(in)).Decode(&rec); err != nil { 239 | return fmt.Errorf("Deserialise: failed to decode record - %v", err) 240 | } 241 | return nil 242 | } 243 | 244 | // Format all attributes (except the binary key) for pretty printing, using the specified separator. 245 | func (rec *Record) FormatAttrs(separator string) string { 246 | return fmt.Sprintf(`Timestamp="%d"%sIP="%s"%sHostname="%s"%sFileSystemUUID="%s"%sKMIPID="%s"%sMountPoint="%s"%sMountOptions="%s"`, 247 | rec.LastRetrieval.Timestamp, separator, 248 | rec.LastRetrieval.IP, separator, 249 | rec.LastRetrieval.Hostname, separator, 250 | rec.UUID, separator, 251 | rec.ID, separator, 252 | strings.Replace(rec.MountPoint, `"`, `\"`, -1), separator, 253 | rec.GetMountOptionStr()) 254 | } 255 | 256 | type RecordSlice []Record // a slice of key database records that can be sorted by latest usage. 257 | 258 | func (r RecordSlice) Len() int { 259 | return len(r) 260 | } 261 | 262 | func (r RecordSlice) Less(i, j int) bool { 263 | // Largest (latest) timestamp is to first appear in sorted list 264 | return r[i].LastRetrieval.Timestamp != 0 && r[i].LastRetrieval.Timestamp > r[j].LastRetrieval.Timestamp 265 | } 266 | 267 | func (r RecordSlice) Swap(i, j int) { 268 | r[i], r[j] = r[j], r[i] 269 | } 270 | --------------------------------------------------------------------------------