Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
halo
system_vold
Commits
c3e4cc29
Commit
c3e4cc29
authored
10 years ago
by
Paul Lawrence
Committed by
Gerrit Code Review
10 years ago
Browse files
Options
Download
Plain Diff
Merge "Securely encrypt the master key"
parents
bf9dcad7
707fd6c7
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
458 additions
and
4 deletions
+458
-4
Android.mk
Android.mk
+1
-0
CommandListener.cpp
CommandListener.cpp
+8
-0
Ext4Crypt.cpp
Ext4Crypt.cpp
+377
-0
Ext4Crypt.h
Ext4Crypt.h
+16
-0
cryptfs.c
cryptfs.c
+48
-4
cryptfs.h
cryptfs.h
+8
-0
No files found.
Android.mk
View file @
c3e4cc29
...
...
@@ -15,6 +15,7 @@ common_src_files := \
Devmapper.cpp
\
ResponseCode.cpp
\
CheckBattery.cpp
\
Ext4Crypt.cpp
\
VoldUtil.c
\
fstrim.c
\
cryptfs.c
...
...
This diff is collapsed.
Click to expand it.
CommandListener.cpp
View file @
c3e4cc29
...
...
@@ -619,6 +619,14 @@ int CommandListener::CryptfsCmd::runCommand(SocketClient *cli,
Process
::
killProcessesWithOpenFiles
(
DATA_MNT_POINT
,
2
);
}
}
}
else
if
(
!
strcmp
(
argv
[
1
],
"enablefilecrypto"
))
{
const
char
*
syntax
=
"Usage: cryptfs enablefilecrypto"
;
if
(
argc
!=
2
)
{
cli
->
sendMsg
(
ResponseCode
::
CommandSyntaxError
,
syntax
,
false
);
return
0
;
}
dumpArgs
(
argc
,
argv
,
-
1
);
rc
=
cryptfs_enable_file
();
}
else
if
(
!
strcmp
(
argv
[
1
],
"changepw"
))
{
const
char
*
syntax
=
"Usage: cryptfs changepw "
"default|password|pin|pattern [newpasswd]"
;
...
...
This diff is collapsed.
Click to expand it.
Ext4Crypt.cpp
0 → 100644
View file @
c3e4cc29
#include "Ext4Crypt.h"
#include <string>
#include <fstream>
#include <map>
#include <errno.h>
#include <sys/mount.h>
#include <cutils/properties.h>
#include "unencrypted_properties.h"
#include "key_control.h"
#include "cryptfs.h"
#define LOG_TAG "Ext4Crypt"
#include "cutils/log.h"
#include <cutils/klog.h>
namespace
{
// Key length in bits
const
int
key_length
=
128
;
// How is device encrypted
struct
keys
{
std
::
string
master_key
;
std
::
string
password
;
};
std
::
map
<
std
::
string
,
keys
>
s_key_store
;
// ext4enc:TODO Include structure from somewhere sensible
// MUST be in sync with ext4_crypto.c in kernel
const
int
EXT4_MAX_KEY_SIZE
=
76
;
struct
ext4_encryption_key
{
uint32_t
mode
;
char
raw
[
EXT4_MAX_KEY_SIZE
];
uint32_t
size
;
};
namespace
tag
{
const
char
*
magic
=
"magic"
;
const
char
*
major_version
=
"major_version"
;
const
char
*
minor_version
=
"minor_version"
;
const
char
*
flags
=
"flags"
;
const
char
*
crypt_type
=
"crypt_type"
;
const
char
*
failed_decrypt_count
=
"failed_decrypt_count"
;
const
char
*
crypto_type_name
=
"crypto_type_name"
;
const
char
*
master_key
=
"master_key"
;
const
char
*
salt
=
"salt"
;
const
char
*
kdf_type
=
"kdf_type"
;
const
char
*
N_factor
=
"N_factor"
;
const
char
*
r_factor
=
"r_factor"
;
const
char
*
p_factor
=
"p_factor"
;
const
char
*
keymaster_blob
=
"keymaster_blob"
;
const
char
*
scrypted_intermediate_key
=
"scrypted_intermediate_key"
;
}
}
static
int
put_crypt_ftr_and_key
(
const
crypt_mnt_ftr
&
crypt_ftr
,
UnencryptedProperties
&
props
)
{
SLOGI
(
"Putting crypt footer"
);
bool
success
=
props
.
Set
<
int
>
(
tag
::
magic
,
crypt_ftr
.
magic
)
&&
props
.
Set
<
int
>
(
tag
::
major_version
,
crypt_ftr
.
major_version
)
&&
props
.
Set
<
int
>
(
tag
::
minor_version
,
crypt_ftr
.
minor_version
)
&&
props
.
Set
<
int
>
(
tag
::
flags
,
crypt_ftr
.
flags
)
&&
props
.
Set
<
int
>
(
tag
::
crypt_type
,
crypt_ftr
.
crypt_type
)
&&
props
.
Set
<
int
>
(
tag
::
failed_decrypt_count
,
crypt_ftr
.
failed_decrypt_count
)
&&
props
.
Set
<
std
::
string
>
(
tag
::
crypto_type_name
,
std
::
string
(
reinterpret_cast
<
const
char
*>
(
crypt_ftr
.
crypto_type_name
)))
&&
props
.
Set
<
std
::
string
>
(
tag
::
master_key
,
std
::
string
((
const
char
*
)
crypt_ftr
.
master_key
,
crypt_ftr
.
keysize
))
&&
props
.
Set
<
std
::
string
>
(
tag
::
salt
,
std
::
string
((
const
char
*
)
crypt_ftr
.
salt
,
SALT_LEN
))
&&
props
.
Set
<
int
>
(
tag
::
kdf_type
,
crypt_ftr
.
kdf_type
)
&&
props
.
Set
<
int
>
(
tag
::
N_factor
,
crypt_ftr
.
N_factor
)
&&
props
.
Set
<
int
>
(
tag
::
r_factor
,
crypt_ftr
.
r_factor
)
&&
props
.
Set
<
int
>
(
tag
::
p_factor
,
crypt_ftr
.
p_factor
)
&&
props
.
Set
<
std
::
string
>
(
tag
::
keymaster_blob
,
std
::
string
((
const
char
*
)
crypt_ftr
.
keymaster_blob
,
crypt_ftr
.
keymaster_blob_size
))
&&
props
.
Set
<
std
::
string
>
(
tag
::
scrypted_intermediate_key
,
std
::
string
((
const
char
*
)
crypt_ftr
.
scrypted_intermediate_key
,
SCRYPT_LEN
));
return
success
?
0
:
-
1
;
}
static
int
get_crypt_ftr_and_key
(
crypt_mnt_ftr
&
crypt_ftr
,
const
UnencryptedProperties
&
props
)
{
memset
(
&
crypt_ftr
,
0
,
sizeof
(
crypt_ftr
));
crypt_ftr
.
magic
=
props
.
Get
<
int
>
(
tag
::
magic
);
crypt_ftr
.
major_version
=
props
.
Get
<
int
>
(
tag
::
major_version
);
crypt_ftr
.
minor_version
=
props
.
Get
<
int
>
(
tag
::
minor_version
);
crypt_ftr
.
flags
=
props
.
Get
<
int
>
(
tag
::
flags
);
crypt_ftr
.
crypt_type
=
props
.
Get
<
int
>
(
tag
::
crypt_type
);
crypt_ftr
.
failed_decrypt_count
=
props
.
Get
<
int
>
(
tag
::
failed_decrypt_count
);
std
::
string
crypto_type_name
=
props
.
Get
<
std
::
string
>
(
tag
::
crypto_type_name
);
strlcpy
(
reinterpret_cast
<
char
*>
(
crypt_ftr
.
crypto_type_name
),
crypto_type_name
.
c_str
(),
sizeof
(
crypt_ftr
.
crypto_type_name
));
std
::
string
master_key
=
props
.
Get
<
std
::
string
>
(
tag
::
master_key
);
crypt_ftr
.
keysize
=
master_key
.
size
();
if
(
crypt_ftr
.
keysize
>
sizeof
(
crypt_ftr
.
master_key
))
{
SLOGE
(
"Master key size too long"
);
return
-
1
;
}
memcpy
(
crypt_ftr
.
master_key
,
&
master_key
[
0
],
crypt_ftr
.
keysize
);
std
::
string
salt
=
props
.
Get
<
std
::
string
>
(
tag
::
salt
);
if
(
salt
.
size
()
!=
SALT_LEN
)
{
SLOGE
(
"Salt wrong length"
);
return
-
1
;
}
memcpy
(
crypt_ftr
.
salt
,
&
salt
[
0
],
SALT_LEN
);
crypt_ftr
.
kdf_type
=
props
.
Get
<
int
>
(
tag
::
kdf_type
);
crypt_ftr
.
N_factor
=
props
.
Get
<
int
>
(
tag
::
N_factor
);
crypt_ftr
.
r_factor
=
props
.
Get
<
int
>
(
tag
::
r_factor
);
crypt_ftr
.
p_factor
=
props
.
Get
<
int
>
(
tag
::
p_factor
);
std
::
string
keymaster_blob
=
props
.
Get
<
std
::
string
>
(
tag
::
keymaster_blob
);
crypt_ftr
.
keymaster_blob_size
=
keymaster_blob
.
size
();
if
(
crypt_ftr
.
keymaster_blob_size
>
sizeof
(
crypt_ftr
.
keymaster_blob
))
{
SLOGE
(
"Keymaster blob too long"
);
return
-
1
;
}
memcpy
(
crypt_ftr
.
keymaster_blob
,
&
keymaster_blob
[
0
],
crypt_ftr
.
keymaster_blob_size
);
std
::
string
scrypted_intermediate_key
=
props
.
Get
<
std
::
string
>
(
tag
::
scrypted_intermediate_key
);
if
(
scrypted_intermediate_key
.
size
()
!=
SCRYPT_LEN
)
{
SLOGE
(
"scrypted intermediate key wrong length"
);
return
-
1
;
}
memcpy
(
crypt_ftr
.
scrypted_intermediate_key
,
&
scrypted_intermediate_key
[
0
],
SCRYPT_LEN
);
return
0
;
}
static
UnencryptedProperties
GetProps
(
const
char
*
path
)
{
return
UnencryptedProperties
(
path
);
}
static
UnencryptedProperties
GetAltProps
(
const
char
*
path
)
{
return
UnencryptedProperties
((
std
::
string
()
+
path
+
"/tmp_mnt"
).
c_str
());
}
static
UnencryptedProperties
GetPropsOrAltProps
(
const
char
*
path
)
{
UnencryptedProperties
props
=
GetProps
(
path
);
if
(
props
.
OK
())
{
return
props
;
}
return
GetAltProps
(
path
);
}
int
e4crypt_enable
(
const
char
*
path
)
{
// Already enabled?
if
(
s_key_store
.
find
(
path
)
!=
s_key_store
.
end
())
{
return
0
;
}
// Not an encryptable device?
UnencryptedProperties
key_props
=
GetProps
(
path
).
GetChild
(
properties
::
key
);
if
(
!
key_props
.
OK
())
{
return
0
;
}
if
(
key_props
.
Get
<
std
::
string
>
(
tag
::
master_key
).
empty
())
{
crypt_mnt_ftr
ftr
;
if
(
cryptfs_create_default_ftr
(
&
ftr
,
key_length
))
{
SLOGE
(
"Failed to create crypto footer"
);
return
-
1
;
}
if
(
put_crypt_ftr_and_key
(
ftr
,
key_props
))
{
SLOGE
(
"Failed to write crypto footer"
);
return
-
1
;
}
crypt_mnt_ftr
ftr2
;
if
(
get_crypt_ftr_and_key
(
ftr2
,
key_props
))
{
SLOGE
(
"Failed to read crypto footer back"
);
return
-
1
;
}
if
(
memcmp
(
&
ftr
,
&
ftr2
,
sizeof
(
ftr
))
!=
0
)
{
SLOGE
(
"Crypto footer not correctly written"
);
// ex4enc:TODO why is this failing?
//return -1;
}
}
if
(
!
UnencryptedProperties
(
path
).
Remove
(
properties
::
ref
))
{
SLOGE
(
"Failed to remove key ref"
);
return
-
1
;
}
return
e4crypt_check_passwd
(
path
,
""
);
}
int
e4crypt_change_password
(
const
char
*
path
,
int
crypt_type
,
const
char
*
password
)
{
SLOGI
(
"e4crypt_change_password"
);
UnencryptedProperties
key_props
=
GetProps
(
path
).
GetChild
(
properties
::
key
);
crypt_mnt_ftr
ftr
;
if
(
get_crypt_ftr_and_key
(
ftr
,
key_props
))
{
SLOGE
(
"Failed to read crypto footer back"
);
return
-
1
;
}
auto
mki
=
s_key_store
.
find
(
path
);
if
(
mki
==
s_key_store
.
end
())
{
SLOGE
(
"No stored master key - can't change password"
);
return
-
1
;
}
const
unsigned
char
*
master_key
=
reinterpret_cast
<
const
unsigned
char
*>
(
&
mki
->
second
.
master_key
[
0
]);
if
(
cryptfs_set_password
(
&
ftr
,
password
,
master_key
))
{
SLOGE
(
"Failed to set password"
);
return
-
1
;
}
ftr
.
crypt_type
=
crypt_type
;
if
(
put_crypt_ftr_and_key
(
ftr
,
key_props
))
{
SLOGE
(
"Failed to write crypto footer"
);
return
-
1
;
}
if
(
!
UnencryptedProperties
(
path
).
Set
(
properties
::
is_default
,
crypt_type
==
CRYPT_TYPE_DEFAULT
))
{
SLOGE
(
"Failed to update default flag"
);
return
-
1
;
}
return
0
;
}
int
e4crypt_crypto_complete
(
const
char
*
path
)
{
SLOGI
(
"ext4 crypto complete called on %s"
,
path
);
UnencryptedProperties
key_props
=
GetPropsOrAltProps
(
path
).
GetChild
(
properties
::
key
);
if
(
key_props
.
Get
<
std
::
string
>
(
tag
::
master_key
).
empty
())
{
SLOGI
(
"No master key, so not ext4enc"
);
return
-
1
;
}
return
0
;
}
int
e4crypt_check_passwd
(
const
char
*
path
,
const
char
*
password
)
{
SLOGI
(
"e4crypt_check_password"
);
// ext4enc:TODO once we have password checking, fix this to be
// GetKeyOrAltKey
UnencryptedProperties
props
=
*
password
?
GetAltProps
(
path
)
:
GetProps
(
path
);
UnencryptedProperties
key_props
=
props
.
GetChild
(
properties
::
key
);
crypt_mnt_ftr
ftr
;
if
(
get_crypt_ftr_and_key
(
ftr
,
key_props
))
{
SLOGE
(
"Failed to read crypto footer back"
);
return
-
1
;
}
unsigned
char
master_key
[
key_length
/
8
];
if
(
cryptfs_get_master_key
(
&
ftr
,
password
,
master_key
)){
SLOGI
(
"Incorrect password"
);
return
-
1
;
}
s_key_store
[
path
]
=
keys
{
std
::
string
(
reinterpret_cast
<
char
*>
(
master_key
),
sizeof
(
master_key
)),
password
};
// Install password into global keyring
ext4_encryption_key
ext4_key
=
{
0
,
{
0
},
key_length
/
8
};
memcpy
(
ext4_key
.
raw
,
master_key
,
ext4_key
.
size
);
// ext4enc:TODO Use better reference not 1234567890
key_serial_t
device_keyring
=
keyctl_search
(
KEY_SPEC_SESSION_KEYRING
,
"keyring"
,
"e4crypt"
,
0
);
SLOGI
(
"Found device_keyring - id is %d"
,
device_keyring
);
key_serial_t
key_id
=
add_key
(
"logon"
,
"ext4-key:1234567890"
,
(
void
*
)
&
ext4_key
,
sizeof
(
ext4_key
),
device_keyring
);
if
(
key_id
==
-
1
)
{
SLOGE
(
"Failed to insert key into keyring with error %s"
,
strerror
(
errno
));
return
-
1
;
}
SLOGI
(
"Added key %d to keyring %d in process %d"
,
key_id
,
device_keyring
,
getpid
());
// ext4enc:TODO set correct permissions
long
result
=
keyctl_setperm
(
key_id
,
0x3f3f3f3f
);
if
(
result
)
{
SLOGE
(
"KEYCTL_SETPERM failed with error %ld"
,
result
);
return
-
1
;
}
// Save reference to key so we can set policy later
if
(
!
props
.
Set
(
properties
::
ref
,
"@s.ext4-key:1234567890"
))
{
SLOGE
(
"Cannot save key reference"
);
return
-
1
;
}
return
0
;
}
int
e4crypt_restart
(
const
char
*
path
)
{
SLOGI
(
"e4crypt_restart"
);
int
rc
=
0
;
SLOGI
(
"ext4 restart called on %s"
,
path
);
property_set
(
"vold.decrypt"
,
"trigger_reset_main"
);
SLOGI
(
"Just asked init to shut down class main"
);
sleep
(
2
);
std
::
string
tmp_path
=
std
::
string
()
+
path
+
"/tmp_mnt"
;
// ext4enc:TODO add retry logic
rc
=
umount
(
tmp_path
.
c_str
());
if
(
rc
)
{
SLOGE
(
"umount %s failed with rc %d, msg %s"
,
tmp_path
.
c_str
(),
rc
,
strerror
(
errno
));
return
rc
;
}
// ext4enc:TODO add retry logic
rc
=
umount
(
path
);
if
(
rc
)
{
SLOGE
(
"umount %s failed with rc %d, msg %s"
,
path
,
rc
,
strerror
(
errno
));
return
rc
;
}
return
0
;
}
const
char
*
e4crypt_get_password
(
const
char
*
path
)
{
SLOGI
(
"e4crypt_get_password"
);
// ext4enc:TODO scrub password after timeout
auto
i
=
s_key_store
.
find
(
path
);
if
(
i
==
s_key_store
.
end
())
{
return
0
;
}
else
{
return
i
->
second
.
password
.
c_str
();
}
}
int
e4crypt_get_password_type
(
const
char
*
path
)
{
SLOGI
(
"e4crypt_get_password_type"
);
return
GetPropsOrAltProps
(
path
).
GetChild
(
properties
::
key
)
.
Get
<
int
>
(
tag
::
crypt_type
,
CRYPT_TYPE_DEFAULT
);
}
This diff is collapsed.
Click to expand it.
Ext4Crypt.h
0 → 100644
View file @
c3e4cc29
#include <sys/cdefs.h>
__BEGIN_DECLS
// General functions
int
e4crypt_enable
(
const
char
*
path
);
int
e4crypt_main
(
int
argc
,
char
*
argv
[]);
int
e4crypt_change_password
(
const
char
*
path
,
int
crypt_type
,
const
char
*
password
);
int
e4crypt_crypto_complete
(
const
char
*
path
);
int
e4crypt_check_passwd
(
const
char
*
path
,
const
char
*
password
);
int
e4crypt_get_password_type
(
const
char
*
path
);
const
char
*
e4crypt_get_password
(
const
char
*
path
);
int
e4crypt_restart
(
const
char
*
path
);
__END_DECLS
This diff is collapsed.
Click to expand it.
cryptfs.c
View file @
c3e4cc29
...
...
@@ -53,7 +53,8 @@
#include "VolumeManager.h"
#include "VoldUtil.h"
#include "crypto_scrypt.h"
#include "ext4_crypt.h"
#include "Ext4Crypt.h"
#include "ext4_crypt_init_extensions.h"
#include "ext4_utils.h"
#include "f2fs_sparseblock.h"
#include "CheckBattery.h"
...
...
@@ -1310,7 +1311,7 @@ static int encrypt_master_key(const char *passwd, const unsigned char *salt,
/* Encrypt the master key */
if
(
!
EVP_EncryptUpdate
(
&
e_ctx
,
encrypted_master_key
,
&
encrypted_len
,
decrypted_master_key
,
KEY_LEN_BYTES
))
{
decrypted_master_key
,
KEY_LEN_BYTES
))
{
SLOGE
(
"EVP_EncryptUpdate failed
\n
"
);
return
-
1
;
}
...
...
@@ -1345,7 +1346,7 @@ static int encrypt_master_key(const char *passwd, const unsigned char *salt,
return
0
;
}
static
int
decrypt_master_key_aux
(
char
*
passwd
,
unsigned
char
*
salt
,
static
int
decrypt_master_key_aux
(
const
char
*
passwd
,
unsigned
char
*
salt
,
unsigned
char
*
encrypted_master_key
,
unsigned
char
*
decrypted_master_key
,
kdf_func
kdf
,
void
*
kdf_params
,
...
...
@@ -1410,7 +1411,7 @@ static void get_kdf_func(struct crypt_mnt_ftr *ftr, kdf_func *kdf, void** kdf_pa
}
}
static
int
decrypt_master_key
(
char
*
passwd
,
unsigned
char
*
decrypted_master_key
,
static
int
decrypt_master_key
(
const
char
*
passwd
,
unsigned
char
*
decrypted_master_key
,
struct
crypt_mnt_ftr
*
crypt_ftr
,
unsigned
char
**
intermediate_key
,
size_t
*
intermediate_key_size
)
...
...
@@ -3804,3 +3805,46 @@ void cryptfs_clear_password()
password_expiry_time
=
0
;
}
}
int
cryptfs_enable_file
()
{
return
e4crypt_enable
(
DATA_MNT_POINT
);
}
int
cryptfs_create_default_ftr
(
struct
crypt_mnt_ftr
*
crypt_ftr
,
__attribute__
((
unused
))
int
key_length
)
{
if
(
cryptfs_init_crypt_mnt_ftr
(
crypt_ftr
))
{
SLOGE
(
"Failed to initialize crypt_ftr"
);
return
-
1
;
}
if
(
create_encrypted_random_key
(
DEFAULT_PASSWORD
,
crypt_ftr
->
master_key
,
crypt_ftr
->
salt
,
crypt_ftr
))
{
SLOGE
(
"Cannot create encrypted master key
\n
"
);
return
-
1
;
}
//crypt_ftr->keysize = key_length / 8;
return
0
;
}
int
cryptfs_get_master_key
(
struct
crypt_mnt_ftr
*
ftr
,
const
char
*
password
,
unsigned
char
*
master_key
)
{
int
rc
;
// ext4enc:TODO check intermediate_key to see if this is valid key
unsigned
char
*
intermediate_key
=
0
;
size_t
intermediate_key_size
=
0
;
rc
=
decrypt_master_key
(
password
,
master_key
,
ftr
,
&
intermediate_key
,
&
intermediate_key_size
);
return
rc
;
}
int
cryptfs_set_password
(
struct
crypt_mnt_ftr
*
ftr
,
const
char
*
password
,
const
unsigned
char
*
master_key
)
{
return
encrypt_master_key
(
password
,
ftr
->
salt
,
master_key
,
ftr
->
master_key
,
ftr
);
}
This diff is collapsed.
Click to expand it.
cryptfs.h
View file @
c3e4cc29
...
...
@@ -235,6 +235,7 @@ extern "C" {
int
cryptfs_enable
(
char
*
flag
,
int
type
,
char
*
passwd
,
int
allow_reboot
);
int
cryptfs_changepw
(
int
type
,
const
char
*
newpw
);
int
cryptfs_enable_default
(
char
*
flag
,
int
allow_reboot
);
int
cryptfs_enable_file
();
int
cryptfs_setup_volume
(
const
char
*
label
,
int
major
,
int
minor
,
char
*
crypto_dev_path
,
unsigned
int
max_pathlen
,
int
*
new_major
,
int
*
new_minor
);
...
...
@@ -245,6 +246,13 @@ extern "C" {
int
cryptfs_get_password_type
(
void
);
const
char
*
cryptfs_get_password
(
void
);
void
cryptfs_clear_password
(
void
);
// Functions for file encryption to use to inherit our encryption logic
int
cryptfs_create_default_ftr
(
struct
crypt_mnt_ftr
*
ftr
,
int
key_length
);
int
cryptfs_get_master_key
(
struct
crypt_mnt_ftr
*
ftr
,
const
char
*
password
,
unsigned
char
*
master_key
);
int
cryptfs_set_password
(
struct
crypt_mnt_ftr
*
ftr
,
const
char
*
password
,
const
unsigned
char
*
master_key
);
#ifdef __cplusplus
}
#endif
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment