Skip to content

Instantly share code, notes, and snippets.

@ggsubs
Last active August 17, 2023 22:05
Show Gist options
  • Save ggsubs/fbbc5ae8b02fda73a7c93a681a6381b7 to your computer and use it in GitHub Desktop.
Save ggsubs/fbbc5ae8b02fda73a7c93a681a6381b7 to your computer and use it in GitHub Desktop.
package testclient.cloudkit;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.TimeZone;
public class CloudKitRequest {
private String containerId;
private String keyId;
private String environment;
private PrivateKey privateKey;
private final String host = "api.apple-cloudkit.com";
private String cloudKitResource;
private String payload;
private String operation;
private String version = "1";
public CloudKitRequest(String containerId, String keyId, String environment, String privateKeyText) {
this.containerId = containerId;
this.keyId = keyId;
this.environment = environment;
this.privateKey = loadPrivateKey(privateKeyText);
}
public CloudKitRequest setResource(String cloudKitResource) {
this.cloudKitResource = cloudKitResource;
return this;
}
public CloudKitRequest setEntity(String payload) {
this.payload = payload;
return this;
}
public CloudKitRequest setOperation(String operation) {
this.operation = operation;
return this;
}
public CloseableHttpResponse execute() {
String dateString = getIsoDate();
String requestUrl = "/database/" + version + "/" + containerId + "/" + environment + cloudKitResource;
String concat = dateString+":"+hashRequestBody(payload)+":"+requestUrl;
CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpPost httpPostRequest = new HttpPost("https://" + host + requestUrl);
httpPostRequest.setHeader("X-Apple-CloudKit-Request-KeyID", keyId);
httpPostRequest.setHeader("X-Apple-CloudKit-Request-ISO8601Date", dateString);
httpPostRequest.setHeader("X-Apple-CloudKit-Request-SignatureV1", signRequest(concat));
httpPostRequest.setHeader("Content-Type", "text/plain");
try {
httpPostRequest.setEntity( new StringEntity(payload));
return httpclient.execute(httpPostRequest);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
} catch (ClientProtocolException e) {
throw new RuntimeException(e.getMessage());
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
private PrivateKey loadPrivateKey(String keyText) {
PEMParser pemPrivateKeyReader = new PEMParser(new StringReader(keyText));
Object pemObject = null;
try {
pemObject = pemPrivateKeyReader.readObject();
if (pemObject instanceof PEMKeyPair) {
KeyPair pair = new JcaPEMKeyConverter().setProvider("BC").getKeyPair((PEMKeyPair) pemObject);
return pair.getPrivate();
} else {
throw new RuntimeException("Unexpected key spec");
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
private String getIsoDate() {
TimeZone tz = TimeZone.getTimeZone("UTC");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
df.setTimeZone(tz);
return df.format(new Date())+"Z";
}
public static String hashRequestBody(String body) {
SHA256Digest digest = new SHA256Digest();
byte[] ret = new byte[digest.getDigestSize()];
byte[] data = body.getBytes();
digest.update(data, 0, data.length);
digest.doFinal(ret, 0);
return Base64.getEncoder().encodeToString(ret);
}
private String signRequest(String request) {
byte[] requestBytes = new byte[0];
try {
requestBytes = request.getBytes("UTF-8");
Signature ecdsaSignature = Signature.getInstance("SHA256withECDSA", "BC");
ecdsaSignature.initSign(privateKey);
ecdsaSignature.update(requestBytes);
byte[] signature = ecdsaSignature.sign();
return Base64.getEncoder().encodeToString(signature);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e.getMessage());
} catch (SignatureException e) {
throw new RuntimeException(e.getMessage());
} catch (NoSuchProviderException e) {
throw new RuntimeException(e.getMessage());
} catch (InvalidKeyException e) {
throw new RuntimeException(e.getMessage());
}
}
}
public class Main {
static String privateKeyText =
"-----BEGIN EC PRIVATE KEY-----\n" +
"your prive key is here as from openssl" +
"-----END EC PRIVATE KEY-----";
public static void main(String[] args) throws UnsupportedEncodingException {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
CloseableHttpResponse response = new CloudKitRequest("iCloud.your-id-here",
"your-key-id-here as created in the dashboard for serevr to server API",
"development",
privateKeyText)
.setOperation("POST")
.setEntity("{\"operations\":[{\"operationType\":\"create\",\"record\":{\"recordType\":\"Post\",\"fields\":{\"title\":{\"value\":\"A Post From The Server\"}}}}]}")
.setResource("/public/records/modify")
.execute();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment