@@ -16,8 +16,8 @@ public class TrustedClientDAO extends AccountOwnedEntityDAO<TrustedClient> { | |||||
return super.preCreate(trusted.setTrustId(randomUUID().toString())); | return super.preCreate(trusted.setTrustId(randomUUID().toString())); | ||||
} | } | ||||
@Override public TrustedClient postCreate(TrustedClient trusted, Object context) { | |||||
return super.postCreate(trusted, context); | |||||
public TrustedClient findByAccountAndDevice(String accountUuid, String deviceUuid) { | |||||
return findByUniqueFields("account", accountUuid, "device", deviceUuid); | |||||
} | } | ||||
} | } |
@@ -67,7 +67,9 @@ public class DeviceDAO extends AccountOwnedEntityDAO<Device> { | |||||
@Transactional | @Transactional | ||||
@Override public Device create(@NonNull final Device device) { | @Override public Device create(@NonNull final Device device) { | ||||
if (isRawMode() || device.uninitialized()) return super.create(device); | |||||
if (isRawMode() || device.uninitialized() || device.getDeviceType().isNonVpnDevice()) { | |||||
return super.create(device); | |||||
} | |||||
synchronized (createLock) { | synchronized (createLock) { | ||||
device.initDeviceType(); | device.initDeviceType(); | ||||
@@ -51,7 +51,7 @@ public class GenerateAlgoConfMain extends BaseMain<GenerateAlgoConfOptions> { | |||||
private List<String> loadDevices() { | private List<String> loadDevices() { | ||||
try { | try { | ||||
final String sqlResult = execScript("echo \"select uuid from device where enabled = TRUE\" | PGPASSWORD=\"$(cat /home/bubble/.BUBBLE_PG_PASSWORD)\" psql -U bubble -h 127.0.0.1 bubble -qt"); | |||||
final String sqlResult = execScript("echo \"select uuid from device where enabled = TRUE and device_type != 'non_vpn'\" | PGPASSWORD=\"$(cat /home/bubble/.BUBBLE_PG_PASSWORD)\" psql -U bubble -h 127.0.0.1 bubble -qt"); | |||||
final List<String> deviceUuids = Arrays.stream(sqlResult.split("\n")) | final List<String> deviceUuids = Arrays.stream(sqlResult.split("\n")) | ||||
.filter(device -> !empty(device)) | .filter(device -> !empty(device)) | ||||
.map(String::trim) | .map(String::trim) | ||||
@@ -5,6 +5,7 @@ | |||||
package bubble.model.account; | package bubble.model.account; | ||||
import bubble.model.device.Device; | |||||
import com.fasterxml.jackson.annotation.JsonIgnore; | import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.NoArgsConstructor; | import lombok.NoArgsConstructor; | ||||
@@ -33,6 +34,11 @@ public class TrustedClient extends IdentifiableBase implements HasAccount { | |||||
@Column(nullable=false, updatable=false, length=UUID_MAXLEN) | @Column(nullable=false, updatable=false, length=UUID_MAXLEN) | ||||
@Getter @Setter private String account; | @Getter @Setter private String account; | ||||
@ECSearchable @ECField(index=20) | |||||
@ECForeignKey(entity=Device.class) | |||||
@Column(nullable=false, updatable=false, length=UUID_MAXLEN) | |||||
@Getter @Setter private String device; | |||||
@ECField(index=20) | @ECField(index=20) | ||||
@Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(100+ENC_PAD)+") NOT NULL") | @Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(100+ENC_PAD)+") NOT NULL") | ||||
@JsonIgnore @Getter @Setter private String trustId; | @JsonIgnore @Getter @Setter private String trustId; | ||||
@@ -31,6 +31,10 @@ public class TrustedClientLoginRequest { | |||||
private String password; | private String password; | ||||
public boolean hasPassword () { return !empty(password); } | public boolean hasPassword () { return !empty(password); } | ||||
@HasValue(message="err.device.required") | |||||
@Getter @Setter private String device; | |||||
public boolean hasDevice () { return !empty(device); } | |||||
// require timestamp to begin with a '1'. | // require timestamp to begin with a '1'. | ||||
// note: this means this pattern will break on October 11, 2603 | // note: this means this pattern will break on October 11, 2603 | ||||
private static final String TRUST_HASH_REGEX = "^1[\\d]{10}-"+UUID_REGEX+"-"+UUID_REGEX+"$"; | private static final String TRUST_HASH_REGEX = "^1[\\d]{10}-"+UUID_REGEX+"-"+UUID_REGEX+"$"; | ||||
@@ -25,6 +25,7 @@ public enum BubbleDeviceType { | |||||
android (CertType.cer, true, DeviceSecurityLevel.basic), | android (CertType.cer, true, DeviceSecurityLevel.basic), | ||||
linux (CertType.crt, true, DeviceSecurityLevel.standard), | linux (CertType.crt, true, DeviceSecurityLevel.standard), | ||||
firefox (CertType.crt, false), | firefox (CertType.crt, false), | ||||
web_client (null, false, DeviceSecurityLevel.disabled), | |||||
other (null, true, DeviceSecurityLevel.basic); | other (null, true, DeviceSecurityLevel.basic); | ||||
@Getter private final CertType certType; | @Getter private final CertType certType; | ||||
@@ -36,6 +37,9 @@ public enum BubbleDeviceType { | |||||
@JsonCreator public static BubbleDeviceType fromString (String v) { return enumFromString(BubbleDeviceType.class, v); } | @JsonCreator public static BubbleDeviceType fromString (String v) { return enumFromString(BubbleDeviceType.class, v); } | ||||
public boolean isNonVpnDevice () { return this == web_client; } | |||||
public boolean isVpnDevice () { return !isNonVpnDevice(); } | |||||
@Getter(lazy=true) private static final List<BubbleDeviceType> selectableTypes = initSelectable(); | @Getter(lazy=true) private static final List<BubbleDeviceType> selectableTypes = initSelectable(); | ||||
private static List<BubbleDeviceType> initSelectable() { | private static List<BubbleDeviceType> initSelectable() { | ||||
return Arrays.stream(values()) | return Arrays.stream(values()) | ||||
@@ -4,7 +4,6 @@ | |||||
*/ | */ | ||||
package bubble.model.device; | package bubble.model.device; | ||||
import bubble.ApiConstants; | |||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.account.HasAccount; | import bubble.model.account.HasAccount; | ||||
import bubble.model.cloud.BubbleNetwork; | import bubble.model.cloud.BubbleNetwork; | ||||
@@ -22,10 +21,10 @@ import org.hibernate.annotations.Type; | |||||
import javax.persistence.*; | import javax.persistence.*; | ||||
import javax.validation.constraints.Size; | import javax.validation.constraints.Size; | ||||
import java.io.File; | import java.io.File; | ||||
import static bubble.ApiConstants.EP_DEVICES; | import static bubble.ApiConstants.EP_DEVICES; | ||||
import static bubble.ApiConstants.HOME_DIR; | |||||
import static bubble.model.device.BubbleDeviceType.other; | import static bubble.model.device.BubbleDeviceType.other; | ||||
import static bubble.model.device.BubbleDeviceType.uninitialized; | import static bubble.model.device.BubbleDeviceType.uninitialized; | ||||
import static java.util.UUID.randomUUID; | import static java.util.UUID.randomUUID; | ||||
@@ -51,10 +50,10 @@ public class Device extends IdentifiableBase implements HasAccount { | |||||
public static final String UNINITIALIZED_DEVICE = "__uninitialized_device__"; | public static final String UNINITIALIZED_DEVICE = "__uninitialized_device__"; | ||||
public static final String UNINITIALIZED_DEVICE_LIKE = UNINITIALIZED_DEVICE+"%"; | public static final String UNINITIALIZED_DEVICE_LIKE = UNINITIALIZED_DEVICE+"%"; | ||||
public static final String VPN_CONFIG_PATH = ApiConstants.HOME_DIR + "/configs/localhost/wireguard/"; | |||||
public static final String VPN_CONFIG_PATH = HOME_DIR + "/configs/localhost/wireguard/"; | |||||
public File qrFile () { return new File(Device.VPN_CONFIG_PATH+getUuid()+".png"); } | |||||
public File vpnConfFile () { return new File(Device.VPN_CONFIG_PATH+getUuid()+".conf"); } | |||||
public File qrFile () { return new File(VPN_CONFIG_PATH+getUuid()+".png"); } | |||||
public File vpnConfFile () { return new File(VPN_CONFIG_PATH+getUuid()+".conf"); } | |||||
public boolean configsOk () { return qrFile().exists() && vpnConfFile().exists(); } | public boolean configsOk () { return qrFile().exists() && vpnConfFile().exists(); } | ||||
public Device (Device other) { copy(this, other, CREATE_FIELDS); } | public Device (Device other) { copy(this, other, CREATE_FIELDS); } | ||||
@@ -8,8 +8,10 @@ import bubble.dao.SessionDAO; | |||||
import bubble.dao.account.AccountDAO; | import bubble.dao.account.AccountDAO; | ||||
import bubble.dao.account.AccountPolicyDAO; | import bubble.dao.account.AccountPolicyDAO; | ||||
import bubble.dao.account.TrustedClientDAO; | import bubble.dao.account.TrustedClientDAO; | ||||
import bubble.dao.device.DeviceDAO; | |||||
import bubble.model.account.*; | import bubble.model.account.*; | ||||
import bubble.model.account.message.ActionTarget; | import bubble.model.account.message.ActionTarget; | ||||
import bubble.model.device.Device; | |||||
import bubble.service.account.StandardAuthenticatorService; | import bubble.service.account.StandardAuthenticatorService; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
@@ -21,7 +23,6 @@ import javax.validation.Valid; | |||||
import javax.ws.rs.*; | import javax.ws.rs.*; | ||||
import javax.ws.rs.core.Context; | import javax.ws.rs.core.Context; | ||||
import javax.ws.rs.core.Response; | import javax.ws.rs.core.Response; | ||||
import java.util.List; | |||||
import static bubble.ApiConstants.EP_DELETE; | import static bubble.ApiConstants.EP_DELETE; | ||||
import static bubble.resources.account.AuthResource.newLoginSession; | import static bubble.resources.account.AuthResource.newLoginSession; | ||||
@@ -41,6 +42,7 @@ public class TrustedAuthResource { | |||||
@Autowired private AccountDAO accountDAO; | @Autowired private AccountDAO accountDAO; | ||||
@Autowired private AccountPolicyDAO policyDAO; | @Autowired private AccountPolicyDAO policyDAO; | ||||
@Autowired private DeviceDAO deviceDAO; | |||||
@Autowired private SessionDAO sessionDAO; | @Autowired private SessionDAO sessionDAO; | ||||
@Autowired private StandardAuthenticatorService authenticatorService; | @Autowired private StandardAuthenticatorService authenticatorService; | ||||
@Autowired private TrustedClientDAO trustedClientDAO; | @Autowired private TrustedClientDAO trustedClientDAO; | ||||
@@ -55,10 +57,20 @@ public class TrustedAuthResource { | |||||
final Account account = validateAccountLogin(request.getEmail(), request.getPassword()); | final Account account = validateAccountLogin(request.getEmail(), request.getPassword()); | ||||
if (!account.getUuid().equals(caller.getUuid())) return notFound(request.getEmail()); | if (!account.getUuid().equals(caller.getUuid())) return notFound(request.getEmail()); | ||||
final Device device = deviceDAO.findByAccountAndId(account.getUuid(), request.getDevice()); | |||||
if (device == null) return notFound(request.getDevice()); | |||||
// is there an existing trusted client for this device? | |||||
final TrustedClient existing = trustedClientDAO.findByAccountAndDevice(account.getUuid(), device.getUuid()); | |||||
if (existing != null) return invalid("err.device.alreadyTrusted"); | |||||
final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); | final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); | ||||
authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); | authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); | ||||
return ok(new TrustedClientResponse(trustedClientDAO.create(new TrustedClient().setAccount(account.getUuid())).getTrustId())); | |||||
final TrustedClient trusted = new TrustedClient() | |||||
.setAccount(account.getUuid()) | |||||
.setDevice(device.getUuid()); | |||||
return ok(new TrustedClientResponse(trustedClientDAO.create(trusted).getTrustId())); | |||||
} | } | ||||
@POST | @POST | ||||
@@ -75,15 +87,17 @@ public class TrustedAuthResource { | |||||
return ok(account.setToken(newLoginSession(account, accountDAO, sessionDAO))); | return ok(account.setToken(newLoginSession(account, accountDAO, sessionDAO))); | ||||
} | } | ||||
@POST @Path(EP_DELETE) | |||||
@DELETE @Path(EP_DELETE+"/{device}") | |||||
public Response removeTrustedClient(@Context ContainerRequest ctx, | public Response removeTrustedClient(@Context ContainerRequest ctx, | ||||
@Valid TrustedClientLoginRequest request) { | |||||
@PathParam("device") String deviceId) { | |||||
final Account caller = userPrincipal(ctx); | final Account caller = userPrincipal(ctx); | ||||
final Account validated = validateAccountLogin(request.getEmail(), request.getPassword()); | |||||
if (!validated.getUuid().equals(caller.getUuid())) return notFound(request.getEmail()); | |||||
final Account account = validateTrustedCall(request); | |||||
final TrustedClient trusted = findTrustedClient(account, request); | |||||
final Device device = deviceDAO.findByAccountAndId(caller.getUuid(), deviceId); | |||||
if (device == null) return notFound(deviceId); | |||||
final TrustedClient trusted = trustedClientDAO.findByAccountAndDevice(caller.getUuid(), device.getUuid()); | |||||
if (trusted == null) return notFound(deviceId); | |||||
trustedClientDAO.delete(trusted.getUuid()); | trustedClientDAO.delete(trusted.getUuid()); | ||||
return ok_empty(); | return ok_empty(); | ||||
} | } | ||||
@@ -116,9 +130,12 @@ public class TrustedAuthResource { | |||||
} | } | ||||
private TrustedClient findTrustedClient(Account account, TrustedClientLoginRequest request) { | private TrustedClient findTrustedClient(Account account, TrustedClientLoginRequest request) { | ||||
final List<TrustedClient> trustedClients = trustedClientDAO.findByAccount(account.getUuid()); | |||||
final TrustedClient trusted = trustedClients.stream().filter(c -> c.isValid(request)).findFirst().orElse(null); | |||||
final TrustedClient trusted = trustedClientDAO.findByAccountAndDevice(account.getUuid(), request.getDevice()); | |||||
if (trusted == null) { | if (trusted == null) { | ||||
log.warn("findTrustedClient: no TrustedClient found for device"); | |||||
throw notFoundEx(request.getDevice()); | |||||
} | |||||
if (!trusted.isValid(request)) { | |||||
log.warn("findTrustedClient: no TrustedClient found for salt/hash"); | log.warn("findTrustedClient: no TrustedClient found for salt/hash"); | ||||
throw notFoundEx(request.getTrustHash()); | throw notFoundEx(request.getTrustHash()); | ||||
} | } | ||||
@@ -46,7 +46,7 @@ | |||||
group: mitmproxy | group: mitmproxy | ||||
mode: 0600 | mode: 0600 | ||||
- name: Install mitmproxy_monitor supervisor conf file | |||||
- name: Install mitm_monitor supervisor conf file | |||||
copy: | copy: | ||||
src: supervisor_mitm_monitor.conf | src: supervisor_mitm_monitor.conf | ||||
dest: /etc/supervisor/conf.d/mitm_monitor.conf | dest: /etc/supervisor/conf.d/mitm_monitor.conf | ||||
@@ -0,0 +1,8 @@ | |||||
DELETE FROM trusted_client; | |||||
ALTER TABLE ONLY trusted_client ADD COLUMN device character varying(100) NOT NULL; | |||||
CREATE INDEX trusted_client_idx_device ON trusted_client USING btree (device); | |||||
CREATE UNIQUE INDEX trusted_client_uniq_account_device ON trusted_client USING btree (account, device); | |||||
ALTER TABLE ONLY trusted_client ADD CONSTRAINT trusted_client_fk_device FOREIGN KEY (device) REFERENCES device(uuid); |
@@ -842,10 +842,6 @@ err.suspended.cannotSuspendSelf=You cannot suspend yourself | |||||
err.tag.invalid=Tag is invalid | err.tag.invalid=Tag is invalid | ||||
err.tagsJson.length=Too many tags | err.tagsJson.length=Too many tags | ||||
err.tagString.length=Too many tags | err.tagString.length=Too many tags | ||||
err.trustHash.required=trustHash is required | |||||
err.trustHash.invalid=trustHash is not valid | |||||
err.trustSalt.required=trustSalt is required | |||||
err.trustSalt.invalid=trustSalt is not valid | |||||
err.tgzB64.invalid.noRolesDir=No roles directory found in tgz | err.tgzB64.invalid.noRolesDir=No roles directory found in tgz | ||||
err.tgzB64.invalid.wrongNumberOfFiles=Wrong number of files in tgz base directory | err.tgzB64.invalid.wrongNumberOfFiles=Wrong number of files in tgz base directory | ||||
err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in tgz | err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in tgz | ||||
@@ -411,6 +411,14 @@ message_resetPassword_sent=Password Reset Message Successfully Sent | |||||
# App Login | # App Login | ||||
message_authenticating_app_login=Authenticating session... | message_authenticating_app_login=Authenticating session... | ||||
# Trusted devices | |||||
err.trustHash.required=trustHash is required | |||||
err.trustHash.invalid=trustHash is not valid | |||||
err.trustSalt.required=trustSalt is required | |||||
err.trustSalt.invalid=trustSalt is not valid | |||||
err.device.required=Device is required | |||||
err.device.alreadyTrusted=Device is already trusted | |||||
# Payment methods | # Payment methods | ||||
payment_description_credit=Credit or Debit Card | payment_description_credit=Credit or Debit Card | ||||
payment_description_code=Invitation Code | payment_description_code=Invitation Code | ||||
@@ -123,6 +123,21 @@ | |||||
} | } | ||||
}, | }, | ||||
{ | |||||
"comment": "create web client device", | |||||
"request": { | |||||
"uri": "me/devices", | |||||
"method": "put", | |||||
"entity": { | |||||
"name": "firefox-{{rand 10}}", | |||||
"deviceType": "web_client" | |||||
} | |||||
}, | |||||
"response": { | |||||
"store": "device" | |||||
} | |||||
}, | |||||
{ | { | ||||
"comment": "login a third time with new session, TOTP still required", | "comment": "login a third time with new session, TOTP still required", | ||||
"request": { | "request": { | ||||
@@ -149,7 +164,8 @@ | |||||
"method": "put", | "method": "put", | ||||
"entity": { | "entity": { | ||||
"name": "{{userAccount.name}}", | "name": "{{userAccount.name}}", | ||||
"password": "foobar1!" | |||||
"password": "foobar1!", | |||||
"device": "{{device.uuid}}" | |||||
} | } | ||||
}, | }, | ||||
"response": { | "response": { | ||||
@@ -178,6 +194,7 @@ | |||||
"entity": { | "entity": { | ||||
"name": "{{userAccount.name}}", | "name": "{{userAccount.name}}", | ||||
"password": "foobar1!", | "password": "foobar1!", | ||||
"device": "{{device.uuid}}", | |||||
"trustHash": "{{sha256expr '[[serverTime]]-392f466c-cd17-11ea-bf46-0bb4a63a0769-[[trusted.id]]'}}", | "trustHash": "{{sha256expr '[[serverTime]]-392f466c-cd17-11ea-bf46-0bb4a63a0769-[[trusted.id]]'}}", | ||||
"trustSalt": "{{serverTime}}-392f466c-cd17-11ea-bf46-0bb4a63a0769" | "trustSalt": "{{serverTime}}-392f466c-cd17-11ea-bf46-0bb4a63a0769" | ||||
} | } | ||||
@@ -202,44 +219,11 @@ | |||||
} | } | ||||
}, | }, | ||||
{ | |||||
"comment": "remove trust for this device, fails because we used the same serverTime", | |||||
"request": { | |||||
"uri": "auth/trust/delete", | |||||
"entity": { | |||||
"name": "{{userAccount.name}}", | |||||
"password": "foobar1!", | |||||
"trustHash": "{{sha256expr '[[serverTime]]-392f466c-cd17-11ea-bf46-0bb4a63a0769-[[trusted.id]]'}}", | |||||
"trustSalt": "{{serverTime}}-392f466c-cd17-11ea-bf46-0bb4a63a0769" | |||||
} | |||||
}, | |||||
"response": { | |||||
"status": 422, | |||||
"check": [ | |||||
{"condition": "json.has('err.trustHash.invalid')"} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "get updated server time", | |||||
"request": { | |||||
"uri": "auth/time" | |||||
}, | |||||
"response": { "store": "serverTime" } | |||||
}, | |||||
{ | { | ||||
"comment": "remove trust for this device, succeeds", | "comment": "remove trust for this device, succeeds", | ||||
"request": { | "request": { | ||||
"uri": "auth/trust/delete", | |||||
"entity": { | |||||
"name": "{{userAccount.name}}", | |||||
"password": "foobar1!", | |||||
"trustHash": "{{sha256expr '[[serverTime]]-392f466c-cd17-11ea-bf46-0bb4a63a0769-[[trusted.id]]'}}", | |||||
"trustSalt": "{{serverTime}}-392f466c-cd17-11ea-bf46-0bb4a63a0769" | |||||
} | |||||
"uri": "auth/trust/delete/{{device.uuid}}", | |||||
"method": "delete" | |||||
} | } | ||||
}, | }, | ||||
@@ -1 +1 @@ | |||||
Subproject commit 360ea8067406a3babdf8cd6488a4f5c391ac36bf | |||||
Subproject commit 121fc1cc39ff487d74f9000658ba9138e18e9267 |